import Foundation /// A failure the person has to see and act on. public enum SyncFailure: Sendable, Equatable { /// An entry is held; the app owns the wording and the action. case held(Hold.Reason) /// Enqueueing failed (disk full, or the store is unreadable): input pauses. case cannotSaveOnDevice /// The app's per-lane byte ceiling was reached: input stops, and says why. case tooManyUnsent } /// What the save status says, computed from outbox state and never from HTTP /// activity. The cases are in priority order: the first that applies wins. /// /// "Offline" with nothing pending is `.saved`: status stays silent and the /// connectivity chrome says offline, so "Offline ยท 0 pending" never appears. public enum SyncStatus: Sendable, Equatable { /// 1. A held entry, or a local failure. Sticky until resolved. case failed(pending: Int, reason: SyncFailure) /// 2. The session ended with changes still to send. Sticky until sign-in. case needsSignIn(pending: Int) /// 3. Nothing to send and nothing in flight. Silent. case saved /// 4. A send is in flight (or about to start). case saving /// 5. Changes are waiting for a connection. case offline(pending: Int) /// 6. Backing off after a transient failure. case retrying(pending: Int) public enum SaveState: String, Sendable { case saved, saving, retrying, failed } public var saveState: SaveState { switch self { case .saved: return .saved case .saving: return .saving case .offline, .retrying: return .retrying case .failed, .needsSignIn: return .failed } } public var isSticky: Bool { saveState == .failed } public var isAnnounced: Bool { switch self { case .offline, .needsSignIn, .failed: return true; default: return false } } public init(summary: OutboxSummary, sending: Bool, sessionEnded: Bool, offline: Bool, backingOff: Bool, localFailure: SyncFailure? = nil, count: (OutboxSummary) -> Int = { $0.pendingLanes }) { let pending = count(summary) if let held = summary.lanes.sorted(by: { $0.key < $1.key }).compactMap({ $0.value.firstHold }).first { self = .failed(pending: pending, reason: .held(held)) } else if let localFailure { self = .failed(pending: pending, reason: localFailure) } else if summary.unavailable { self = .failed(pending: pending, reason: .cannotSaveOnDevice) } else if sessionEnded { self = .needsSignIn(pending: pending) } else if summary.isEmpty && summary.inFlight == 0 { self = .saved } else if sending || summary.inFlight > 0 { self = .saving } else if offline { self = .offline(pending: pending) } else if backingOff { self = .retrying(pending: pending) } else { self = .saving } } }