crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

InviteDeliveryStore.swift (3041B)


      1 import Foundation
      2 import Observation
      3 
      4 enum InviteDeliveryPhase: Equatable {
      5     case idle
      6     case queued
      7     case sent
      8     case failed
      9 }
     10 
     11 /// Per-recipient delivery state observed by the invite UI. Each row gets a
     12 /// stable, narrowly-scoped observable object, so one friend's acknowledgement
     13 /// does not invalidate every other friend row.
     14 @MainActor
     15 @Observable
     16 final class InviteDelivery {
     17     let gameID: UUID
     18     let friendAuthorID: String
     19     private(set) var phase: InviteDeliveryPhase = .idle
     20     private(set) var failure: InviteDeliveryFailure?
     21 
     22     init(gameID: UUID, friendAuthorID: String) {
     23         self.gameID = gameID
     24         self.friendAuthorID = friendAuthorID
     25     }
     26 
     27     fileprivate func setPhase(
     28         _ phase: InviteDeliveryPhase,
     29         failure: InviteDeliveryFailure? = nil
     30     ) {
     31         self.phase = phase
     32         self.failure = failure
     33     }
     34 }
     35 
     36 /// Bridges SyncEngine's record-level acknowledgements to stable invite-row
     37 /// models. The record-name index closes the race where CloudKit confirms a
     38 /// fast send before the inviting task itself has returned to the view.
     39 @MainActor
     40 final class InviteDeliveryStore {
     41     private struct Key: Hashable {
     42         let gameID: UUID
     43         let friendAuthorID: String
     44     }
     45 
     46     private var deliveries: [Key: InviteDelivery] = [:]
     47     private var keyByRecordName: [String: Key] = [:]
     48 
     49     static func failureAnnouncementID(gameID: UUID, friendAuthorID: String) -> String {
     50         "invite-delivery-failed-\(gameID.uuidString)-\(friendAuthorID)"
     51     }
     52 
     53     func delivery(gameID: UUID, friendAuthorID: String) -> InviteDelivery {
     54         let key = Key(gameID: gameID, friendAuthorID: friendAuthorID)
     55         if let existing = deliveries[key] { return existing }
     56         let delivery = InviteDelivery(gameID: gameID, friendAuthorID: friendAuthorID)
     57         deliveries[key] = delivery
     58         return delivery
     59     }
     60 
     61     func markQueued(recordName: String, gameID: UUID, friendAuthorID: String) {
     62         let key = Key(gameID: gameID, friendAuthorID: friendAuthorID)
     63         keyByRecordName[recordName] = key
     64         let delivery = delivery(gameID: gameID, friendAuthorID: friendAuthorID)
     65         guard delivery.phase != .sent else { return }
     66         delivery.setPhase(.queued)
     67     }
     68 
     69     func markSent(recordName: String, gameID: UUID, friendAuthorID: String) {
     70         let key = keyByRecordName[recordName]
     71             ?? Key(gameID: gameID, friendAuthorID: friendAuthorID)
     72         keyByRecordName.removeValue(forKey: recordName)
     73         delivery(gameID: key.gameID, friendAuthorID: key.friendAuthorID)
     74             .setPhase(.sent)
     75     }
     76 
     77     func markFailed(
     78         recordName: String?,
     79         gameID: UUID,
     80         friendAuthorID: String,
     81         failure: InviteDeliveryFailure = .other
     82     ) {
     83         if let recordName { keyByRecordName.removeValue(forKey: recordName) }
     84         let delivery = delivery(gameID: gameID, friendAuthorID: friendAuthorID)
     85         guard delivery.phase != .sent else { return }
     86         delivery.setPhase(.failed, failure: failure)
     87     }
     88 }