EngagementStore.swift (2117B)
1 import Foundation 2 import Observation 3 4 struct EngagementSelectionUpdate: Codable, Equatable, Sendable { 5 var gameID: UUID 6 var authorID: String 7 var deviceID: String 8 var row: Int 9 var col: Int 10 var directionRaw: Int 11 var updatedAt: Date 12 13 init( 14 gameID: UUID, 15 authorID: String, 16 deviceID: String, 17 selection: PlayerSelection, 18 updatedAt: Date = Date() 19 ) { 20 self.gameID = gameID 21 self.authorID = authorID 22 self.deviceID = deviceID 23 self.row = selection.row 24 self.col = selection.col 25 self.directionRaw = selection.direction.rawValue 26 self.updatedAt = updatedAt 27 } 28 29 var selection: PlayerSelection? { 30 guard let direction = Puzzle.Direction(rawValue: directionRaw) else { return nil } 31 return PlayerSelection(row: row, col: col, direction: direction) 32 } 33 } 34 35 @MainActor 36 @Observable 37 final class EngagementStore { 38 struct Entry: Equatable, Sendable { 39 let authorID: String 40 let row: Int 41 let col: Int 42 let direction: Puzzle.Direction 43 let updatedAt: Date 44 } 45 46 private var selectionsByGame: [UUID: [String: Entry]] = [:] 47 48 func set(_ update: EngagementSelectionUpdate) { 49 guard !update.authorID.isEmpty, 50 !update.deviceID.isEmpty, 51 update.deviceID != RecordSerializer.localDeviceID, 52 let selection = update.selection 53 else { return } 54 55 let entry = Entry( 56 authorID: update.authorID, 57 row: selection.row, 58 col: selection.col, 59 direction: selection.direction, 60 updatedAt: update.updatedAt 61 ) 62 let current = selectionsByGame[update.gameID]?[update.authorID] 63 guard current.map({ $0.updatedAt <= entry.updatedAt }) ?? true else { return } 64 selectionsByGame[update.gameID, default: [:]][update.authorID] = entry 65 } 66 67 func selections(for gameID: UUID) -> [String: Entry] { 68 selectionsByGame[gameID] ?? [:] 69 } 70 71 func clear(gameID: UUID) { 72 selectionsByGame[gameID] = nil 73 } 74 }