Files
Luma/Sources/Shared/Models/ArchiveSyncCheckpoint.swift
T
wt 65605ded95
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
mam fixes
2026-08-11 22:00:49 +07:00

117 lines
3.7 KiB
Swift

import Foundation
/// A MAM UID is only meaningful inside one archive. Account MAM and every MUC
/// therefore get their own namespace and durable checkpoint.
struct MAMArchiveKey: Codable, Hashable, Sendable {
enum Kind: String, Codable, Sendable {
case account
case muc
}
let kind: Kind
let jid: String
init(kind: Kind, jid: String) {
self.kind = kind
self.jid = jid.lowercased()
}
static func account(_ jid: String) -> Self {
Self(kind: .account, jid: jid)
}
static func muc(_ jid: String) -> Self {
Self(kind: .muc, jid: jid)
}
}
struct MAMArchiveCheckpoint: Codable, Equatable, Sendable {
let timestamp: Date
let cursor: String?
init(timestamp: Date, cursor: String? = nil) {
self.timestamp = timestamp
self.cursor = ArchiveSyncCheckpoint.normalizedCursor(cursor)
}
}
/// 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
}
}