// ResumeCursor.swift // Persists the last-seen IC-1 sequence number per session across app launches. // // Storage: UserDefaults (standard suite). // Key schema: "ResumeCursor." // // Thread-safety: UserDefaults is documented as thread-safe; this class adds no // additional synchronisation. All callers are expected to be on the main actor // in practice (alongside WebSocketClient), though no actor annotation is // imposed here so tests can call freely. import Foundation /// Stores and retrieves the last acknowledged IC-1 sequence number for each /// session, enabling resume-from-cursor on reconnect. public final class ResumeCursor { private let defaults: UserDefaults private let keyPrefix = "ResumeCursor." // MARK: - Init /// Creates a cursor backed by `defaults` (defaults to `.standard`). public init(defaults: UserDefaults = .standard) { self.defaults = defaults } // MARK: - Public API /// Returns the last persisted sequence number for `sessionId`, or `nil` /// if no sequence has been stored (i.e. first-ever connection). public func lastSeq(for sessionId: String) -> UInt64? { let key = storageKey(for: sessionId) // UserDefaults stores integer values as Int64 (signed). We use the raw // bit pattern to round-trip UInt64 without loss, since UInt64.max // exceeds Int.max on 64-bit platforms. guard defaults.object(forKey: key) != nil else { return nil } let raw = defaults.integer(forKey: key) return UInt64(bitPattern: Int64(raw)) } /// Persists `seq` as the latest acknowledged chunk for `sessionId`. /// /// Called after successfully processing a `BinaryFrame` so that the cursor /// always reflects a frame that the app has actually consumed. public func update(sessionId: String, seq: UInt64) { // Store as Int64 bit-pattern; see note in lastSeq(for:). let raw = Int(Int64(bitPattern: seq)) defaults.set(raw, forKey: storageKey(for: sessionId)) } /// Removes the stored cursor for `sessionId`. /// /// Call this when a session is deleted or a full snapshot has been received /// and the client no longer needs delta replay. public func clear(sessionId: String) { defaults.removeObject(forKey: storageKey(for: sessionId)) } // MARK: - Private private func storageKey(for sessionId: String) -> String { keyPrefix + sessionId } }