import Foundation import RastrilloNative /// Editor captures and headless sends both commit here. Merging with the latest /// durable state prevents either writer from erasing an acknowledged batch. @MainActor final class DocsReplicaCache { let store: BlobStore private var generations: [String: Int] = [:] init(store: BlobStore) { self.store = store } func generation(_ key: String) -> Int { generations[key, default: 0] } func invalidate(_ key: String) { generations[key, default: 0] += 1 } @discardableResult func commit(_ candidate: PersistedReplica, key: String, generation: Int, valid: () -> Bool = { true }) async throws -> Bool { let path = "cache/docs/\(key)/replica" while self.generation(key) == generation { let before = try store.read(path) let previous = try before.map { try JSONDecoder().decode(PersistedReplica.self, from: $0) } let merger = CRDTReplica(guid: candidate.guid) do { _ = try await merger.open(update: previous?.state ?? Data()) _ = try await merger.apply(candidate.state) let state = try await merger.persistentState().state await merger.close() guard self.generation(key) == generation else { throw CancellationError() } // No suspension between the compare and write. A concurrent // editor or headless writer forces a fresh merge, never a loss. guard try store.read(path) == before else { continue } let merged = PersistedReplica(format: 1, sequence: max(previous?.sequence ?? 0, candidate.sequence), guid: candidate.guid, savedAt: Date(), state: state) guard valid() else { return false } try store.write(JSONEncoder().encode(merged), to: path) return true } catch { await merger.close() throw error } } throw CancellationError() } }