66 lines
2.4 KiB
Swift
66 lines
2.4 KiB
Swift
// ResumeCursor.swift
|
|
// Persists the last-seen IC-1 sequence number per session across app launches.
|
|
//
|
|
// Storage: UserDefaults (standard suite).
|
|
// Key schema: "ResumeCursor.<sessionId>"
|
|
//
|
|
// 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
|
|
}
|
|
}
|