oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import RastrilloNative
3
4struct PersistedReplica: Codable, Sendable {
5 let format: Int
6 let sequence: Int64
7 let guid: String
8 let savedAt: Date
9 let state: Data
10}
11
12/// One document's durability boundary. The writer and editor share this
13/// watermark, but only a successful outbox write may advance it.
14@MainActor final class DocsPersistence {
15 let store: BlobStore
16 let outbox: Outbox
17 let key: String
18 let replica: CRDTReplica
19 private(set) var commandsDurable = 0
20 private var active = true
21 let cache: DocsReplicaCache
22 private let generation: Int
23 var lane: String { "doc:" + key }
24 var path: String { "cache/docs/" + key + "/replica" }
25 init(store: BlobStore, outbox: Outbox, key: String, replica: CRDTReplica, cache: DocsReplicaCache? = nil) {
26 self.store = store; self.outbox = outbox; self.key = key; self.replica = replica
27 self.cache = cache ?? DocsReplicaCache(store: store)
28 generation = self.cache.generation(key)
29 }
30 func reopen(fallback: Data, sequence: Int64) async throws {
31 try await outbox.load()
32 let entries = await outbox.entries(lane: lane)
33 let saved = try store.read(path).map { try JSONDecoder().decode(PersistedReplica.self, from: $0) }
34 if let saved, saved.format != 1 { throw BlobStoreError.unavailable }
35 _ = try await replica.open(update: saved?.state ?? fallback)
36 await replica.setSequence(saved?.sequence ?? sequence)
37 // Replay includes held entries: the editor must show everything kept
38 // here, even when it cannot currently send it.
39 for entry in entries where entry.kind == "update" { _ = try await replica.apply(entry.payload) }
40 commandsDurable = 0
41 }
42 func enqueue(_ update: Data) async throws {
43 try await outbox.enqueue(lane: lane, kind: "update", payload: update)
44 commandsDurable += 1
45 }
46 @discardableResult func persist() async throws -> Bool {
47 let captured = try await replica.persistentState()
48 // No suspension between checking the watermark and the atomic write.
49 // A writer running between command and enqueue must defer its capture.
50 guard active, captured.commandsApplied == commandsDurable else { return false }
51 let saved = PersistedReplica(format: 1, sequence: captured.sequence, guid: captured.guid, savedAt: Date(), state: captured.state)
52 return try await cache.commit(saved, key: key, generation: generation) {
53 self.active && captured.commandsApplied == self.commandsDurable
54 }
55 }
56 func compact() async throws {
57 let entries = await outbox.sendable(lane: lane, maxBytes: nil)
58 guard entries.count > 256 || entries.reduce(0, { $0 + $1.payload.count }) > 1024 * 1024 else { return }
59 var batch: [OutboxEntry] = []; var bytes = 0
60 for entry in entries {
61 if entry.kind != "update" { if batch.isEmpty { continue }; break }
62 if !batch.isEmpty && bytes + entry.payload.count > 512 * 1024 { break }
63 batch.append(entry); bytes += entry.payload.count
64 }
65 guard batch.count > 1 else { return }
66 let merged = try await replica.mergeUpdates(batch.map(\.payload))
67 guard merged.count <= 512 * 1024 else { return }
68 // A send can reserve the batch while mergeUpdates suspends. Refusing
69 // that compaction is expected; it must never delete the reservation.
70 do { try await outbox.replace(batch.map(\.id), with: merged) }
71 catch OutboxError.inFlight { }
72 catch OutboxError.unknownEntry { }
73 }
74 func discard() async throws {
75 active = false
76 cache.invalidate(key)
77 // Remove the replica first: a crash may replay lane entries again,
78 // but can never leave local text whose lane has already been removed.
79 try store.remove(path)
80 try await outbox.discard(lane: lane)
81 }
82}
83