Files
Luma/Sources/Shared/Models/ArchiveSyncCheckpoint.swift
T
2026-07-30 20:07:51 +07:00

82 lines
2.8 KiB
Swift

import Foundation
/// Durable MAM position. The archive UID is the primary synchronization
/// anchor; the timestamp is retained as a compatibility fallback for legacy
/// snapshots and servers that have expired an old UID.
struct ArchiveSyncCheckpoint: Codable, Equatable, Sendable {
let timestamp: Date
let cursor: String?
init(timestamp: Date, cursor: String? = nil) {
self.timestamp = timestamp
self.cursor = Self.normalizedCursor(cursor)
}
func advances(over previous: ArchiveSyncCheckpoint?) -> Bool {
guard let previous else { return true }
return timestamp > previous.timestamp || cursor != previous.cursor
}
static func normalizedCursor(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty else { return nil }
return value
}
}
struct ArchiveSyncRequestPosition: Equatable, Sendable {
let start: Date?
let after: String?
}
/// Keeps cursor-based catch-up and the timestamp fallback deterministic and
/// independently testable from Martin's callback lifecycle.
enum ArchiveSyncCursorPolicy {
static func requestPosition(
checkpoint: ArchiveSyncCheckpoint?,
resumeAfter: String?,
overlap: TimeInterval
) -> ArchiveSyncRequestPosition {
let after = ArchiveSyncCheckpoint.normalizedCursor(resumeAfter)
?? checkpoint?.cursor
if let after {
return ArchiveSyncRequestPosition(start: nil, after: after)
}
return ArchiveSyncRequestPosition(
start: checkpoint?.timestamp.addingTimeInterval(-overlap),
after: nil
)
}
static func resolvedCheckpoint(
previous: ArchiveSyncCheckpoint?,
cursor: String?,
highWatermark: Date?,
queryStartedAt: Date,
caughtUp: Bool
) -> ArchiveSyncCheckpoint {
let previousTimestamp = previous?.timestamp ?? .distantPast
let boundedHighWatermark = min(highWatermark ?? previousTimestamp, queryStartedAt)
let timestamp = caughtUp
? max(previousTimestamp, queryStartedAt)
: max(previousTimestamp, boundedHighWatermark)
return ArchiveSyncCheckpoint(
timestamp: timestamp,
cursor: ArchiveSyncCheckpoint.normalizedCursor(cursor) ?? previous?.cursor
)
}
static func shouldFallbackFromStoredCursor(
requestedAfter: String?,
checkpoint: ArchiveSyncCheckpoint?,
hasCompletedPage: Bool,
fallbackAlreadyUsed: Bool
) -> Bool {
guard !fallbackAlreadyUsed,
!hasCompletedPage,
let requestedAfter = ArchiveSyncCheckpoint.normalizedCursor(requestedAfter),
requestedAfter == checkpoint?.cursor else { return false }
return true
}
}