pi-remote-ios/Sources/Core/Network/ResumeCursor.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
}
}