doxum 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +45 -2
  2. package/dist/{dependency-BdEMyquf.js → access-5D4KKFm7.js} +4 -21
  3. package/dist/access-5D4KKFm7.js.map +1 -0
  4. package/dist/{dependency-DLcCvNKq.cjs → access-z56fVqZ2.cjs} +21 -32
  5. package/dist/access-z56fVqZ2.cjs.map +1 -0
  6. package/dist/{contract-Otb5W6cQ.d.cts → contract-DGkZcTiP.d.cts} +63 -5
  7. package/dist/{contract-DwNiKioc.d.ts → contract-Dh3az8aN.d.ts} +63 -5
  8. package/dist/dependency-BRn1TRDi.js +20 -0
  9. package/dist/dependency-BRn1TRDi.js.map +1 -0
  10. package/dist/dependency-DmQXyj7q.cjs +25 -0
  11. package/dist/dependency-DmQXyj7q.cjs.map +1 -0
  12. package/dist/index.cjs +46 -2341
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +8 -5
  15. package/dist/index.d.ts +8 -5
  16. package/dist/index.js +5 -2304
  17. package/dist/index.js.map +1 -1
  18. package/dist/integration.cjs +3 -2
  19. package/dist/integration.cjs.map +1 -1
  20. package/dist/integration.d.cts +2 -2
  21. package/dist/integration.d.ts +2 -2
  22. package/dist/integration.js +2 -1
  23. package/dist/integration.js.map +1 -1
  24. package/dist/local-sync.cjs +660 -0
  25. package/dist/local-sync.cjs.map +1 -0
  26. package/dist/local-sync.d.cts +79 -0
  27. package/dist/local-sync.d.ts +79 -0
  28. package/dist/local-sync.js +653 -0
  29. package/dist/local-sync.js.map +1 -0
  30. package/dist/react.cjs.map +1 -1
  31. package/dist/react.d.cts +2 -2
  32. package/dist/react.d.ts +2 -2
  33. package/dist/react.js.map +1 -1
  34. package/dist/readable-B8E3FMq6.cjs +2614 -0
  35. package/dist/readable-B8E3FMq6.cjs.map +1 -0
  36. package/dist/readable-CIj02gIj.js +2579 -0
  37. package/dist/readable-CIj02gIj.js.map +1 -0
  38. package/package.json +14 -8
  39. package/skills/doxum-runtime/references/guide.en.md +9 -5
  40. package/skills/doxum-runtime/references/guide.zh-CN.md +3 -2
  41. package/skills/doxum-runtime/references/invariants.en.md +13 -8
  42. package/skills/doxum-runtime/references/invariants.zh-CN.md +2 -2
  43. package/dist/dependency-BdEMyquf.js.map +0 -1
  44. package/dist/dependency-DLcCvNKq.cjs.map +0 -1
@@ -0,0 +1,660 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_access = require("./access-z56fVqZ2.cjs");
3
+ const require_readable = require("./readable-B8E3FMq6.cjs");
4
+ //#region core/src/local-sync/contract.ts
5
+ var LocalSyncUnavailableError = class extends Error {
6
+ constructor(capability) {
7
+ super(`${capability} is required by doxum/local-sync in this environment.`);
8
+ this.name = "LocalSyncUnavailableError";
9
+ }
10
+ };
11
+ var LocalSyncSchemaError = class extends Error {
12
+ constructor(documentId, expected, actual) {
13
+ super(`Local document '${documentId}' uses schema version ${actual}, but version ${expected} was requested.`);
14
+ this.name = "LocalSyncSchemaError";
15
+ }
16
+ };
17
+ var LocalSyncConsistencyError = class extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "LocalSyncConsistencyError";
21
+ }
22
+ };
23
+ var LocalSyncDisposedError = class extends Error {
24
+ constructor() {
25
+ super("Local sync document has been disposed.");
26
+ this.name = "LocalSyncDisposedError";
27
+ }
28
+ };
29
+ //#endregion
30
+ //#region core/src/local-sync/json.ts
31
+ const defaultJsonCommandLimits = Object.freeze({
32
+ maxOperations: 1e3,
33
+ maxBytes: 1e6,
34
+ maxDepth: 64,
35
+ maxStringLength: 256e3
36
+ });
37
+ var LocalSyncDataError = class extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "LocalSyncDataError";
41
+ }
42
+ };
43
+ const describePath = (path) => path.length === 0 ? "value" : path.join(".");
44
+ const limit = (value, fallback, label) => {
45
+ if (value === void 0) return fallback;
46
+ if (Number.isSafeInteger(value) && value > 0) return value;
47
+ throw new TypeError(`${label} must be a positive safe integer.`);
48
+ };
49
+ const resolveLimits = (input) => ({
50
+ maxOperations: limit(input?.maxOperations, defaultJsonCommandLimits.maxOperations, "maxOperations"),
51
+ maxBytes: limit(input?.maxBytes, defaultJsonCommandLimits.maxBytes, "maxBytes"),
52
+ maxDepth: limit(input?.maxDepth, defaultJsonCommandLimits.maxDepth, "maxDepth"),
53
+ maxStringLength: limit(input?.maxStringLength, defaultJsonCommandLimits.maxStringLength, "maxStringLength")
54
+ });
55
+ const validate = (value, path, limits, depth, ancestors) => {
56
+ if (limits && depth > limits.maxDepth) throw new LocalSyncDataError(`${describePath(path)} exceeds the maximum JSON depth.`);
57
+ if (value === null || typeof value === "boolean") return value;
58
+ if (typeof value === "string") {
59
+ if (!limits || value.length <= limits.maxStringLength) return value;
60
+ throw new LocalSyncDataError(`${describePath(path)} exceeds the maximum string length.`);
61
+ }
62
+ if (typeof value === "number") {
63
+ if (Number.isFinite(value)) return value;
64
+ throw new LocalSyncDataError(`${describePath(path)} contains a non-finite number.`);
65
+ }
66
+ if (Array.isArray(value)) {
67
+ if (ancestors.has(value)) throw new LocalSyncDataError(`${describePath(path)} contains a cycle.`);
68
+ ancestors.add(value);
69
+ try {
70
+ for (let index = 0; index < value.length; index += 1) validate(value[index], [...path, String(index)], limits, depth + 1, ancestors);
71
+ } finally {
72
+ ancestors.delete(value);
73
+ }
74
+ return value;
75
+ }
76
+ if (require_access.isPlainObject(value)) {
77
+ if (ancestors.has(value)) throw new LocalSyncDataError(`${describePath(path)} contains a cycle.`);
78
+ ancestors.add(value);
79
+ try {
80
+ for (const [key, entry] of Object.entries(value)) validate(entry, [...path, key], limits, depth + 1, ancestors);
81
+ } finally {
82
+ ancestors.delete(value);
83
+ }
84
+ return value;
85
+ }
86
+ throw new LocalSyncDataError(`${describePath(path)} must be JSON data.`);
87
+ };
88
+ const json = (value, label) => validate(value, [label], void 0, 0, /* @__PURE__ */ new WeakSet());
89
+ const jsonArray = (value, label, input) => {
90
+ const limits = resolveLimits(input);
91
+ const parsed = validate(value, [label], limits, 0, /* @__PURE__ */ new WeakSet());
92
+ if (!Array.isArray(parsed)) throw new LocalSyncDataError(`${label} must be a JSON array.`);
93
+ if (parsed.length > limits.maxOperations) throw new LocalSyncDataError(`${label} exceeds the maximum operation count.`);
94
+ const serialized = JSON.stringify(parsed);
95
+ if (new TextEncoder().encode(serialized).byteLength > limits.maxBytes) throw new LocalSyncDataError(`${label} exceeds the maximum command size.`);
96
+ return parsed;
97
+ };
98
+ //#endregion
99
+ //#region core/src/local-sync/timeline.ts
100
+ const DATABASE_VERSION = 1;
101
+ const DOCUMENTS = "documents";
102
+ const COMMITS = "commits";
103
+ const ACTORS = "actors";
104
+ const request = (value) => new Promise((resolve, reject) => {
105
+ value.onsuccess = () => resolve(value.result);
106
+ value.onerror = () => reject(value.error ?? /* @__PURE__ */ new Error("IndexedDB request failed."));
107
+ });
108
+ const complete = (transaction) => new Promise((resolve, reject) => {
109
+ transaction.oncomplete = () => resolve();
110
+ transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction was aborted."));
111
+ transaction.onerror = () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
112
+ });
113
+ const factory = () => {
114
+ if (!globalThis.indexedDB) throw new LocalSyncUnavailableError("IndexedDB");
115
+ return globalThis.indexedDB;
116
+ };
117
+ const keyRange = () => {
118
+ if (!globalThis.IDBKeyRange) throw new LocalSyncUnavailableError("IndexedDB");
119
+ return globalThis.IDBKeyRange;
120
+ };
121
+ const string = (value, label) => {
122
+ if (typeof value === "string") return value;
123
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
124
+ };
125
+ const nonNegativeInteger$1 = (value, label) => {
126
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
127
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
128
+ };
129
+ const positiveInteger$1 = (value, label) => {
130
+ const parsed = nonNegativeInteger$1(value, label);
131
+ if (parsed > 0) return parsed;
132
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
133
+ };
134
+ const historyEntry = (value, label) => {
135
+ if (!require_access.isRecord(value)) throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
136
+ const footprint = require_readable.decodeCommandFootprint(value.footprint);
137
+ if (!footprint) throw new LocalSyncConsistencyError(`${label}.footprint is malformed in IndexedDB.`);
138
+ return Object.freeze({
139
+ commandId: string(value.commandId, `${label}.commandId`),
140
+ operations: jsonArray(value.operations, `${label}.operations`),
141
+ inverse: jsonArray(value.inverse, `${label}.inverse`),
142
+ footprint
143
+ });
144
+ };
145
+ const historyEntries = (value, label) => {
146
+ if (!Array.isArray(value)) throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
147
+ return Object.freeze(value.map((entry, index) => historyEntry(entry, `${label}.${index}`)));
148
+ };
149
+ const documentRecord = (value) => {
150
+ if (!require_access.isRecord(value)) throw new LocalSyncConsistencyError("Document record is malformed in IndexedDB.");
151
+ const checkpointSeq = nonNegativeInteger$1(value.checkpointSeq, "document.checkpointSeq");
152
+ const headSeq = nonNegativeInteger$1(value.headSeq, "document.headSeq");
153
+ if (checkpointSeq > headSeq) throw new LocalSyncConsistencyError("Document checkpoint exceeds its head sequence.");
154
+ return Object.freeze({
155
+ documentId: string(value.documentId, "document.documentId"),
156
+ schemaVersion: positiveInteger$1(value.schemaVersion, "document.schemaVersion"),
157
+ checkpointSeq,
158
+ headSeq,
159
+ checkpoint: json(value.checkpoint, "document.checkpoint")
160
+ });
161
+ };
162
+ const commitRecord = (value) => {
163
+ if (!require_access.isRecord(value)) throw new LocalSyncConsistencyError("Commit record is malformed in IndexedDB.");
164
+ const kind = value.kind;
165
+ if (kind !== "update" && kind !== "undo" && kind !== "redo") throw new LocalSyncConsistencyError("Commit kind is malformed in IndexedDB.");
166
+ const footprint = require_readable.decodeCommandFootprint(value.footprint);
167
+ if (!footprint) throw new LocalSyncConsistencyError("Commit footprint is malformed in IndexedDB.");
168
+ return Object.freeze({
169
+ documentId: string(value.documentId, "commit.documentId"),
170
+ seq: positiveInteger$1(value.seq, "commit.seq"),
171
+ commandId: string(value.commandId, "commit.commandId"),
172
+ actorId: string(value.actorId, "commit.actorId"),
173
+ kind,
174
+ operations: jsonArray(value.operations, "commit.operations"),
175
+ inverse: jsonArray(value.inverse, "commit.inverse"),
176
+ footprint,
177
+ createdAt: nonNegativeInteger$1(value.createdAt, "commit.createdAt")
178
+ });
179
+ };
180
+ const actorRecord = (value, documentId, actorId) => {
181
+ if (value === void 0) return Object.freeze({
182
+ documentId,
183
+ actorId,
184
+ undo: Object.freeze([]),
185
+ redo: Object.freeze([])
186
+ });
187
+ if (!require_access.isRecord(value)) throw new LocalSyncConsistencyError("Actor history is malformed in IndexedDB.");
188
+ const storedDocumentId = string(value.documentId, "actor.documentId");
189
+ const storedActorId = string(value.actorId, "actor.actorId");
190
+ if (storedDocumentId !== documentId || storedActorId !== actorId) throw new LocalSyncConsistencyError("Actor history key does not match its record.");
191
+ return Object.freeze({
192
+ documentId,
193
+ actorId,
194
+ undo: historyEntries(value.undo, "actor.undo"),
195
+ redo: historyEntries(value.redo, "actor.redo")
196
+ });
197
+ };
198
+ const nextHistory = (current, change) => {
199
+ if (change.kind === "record") {
200
+ const undo = [...current.undo, change.entry];
201
+ const start = Math.max(0, undo.length - change.capacity);
202
+ return Object.freeze({
203
+ documentId: current.documentId,
204
+ actorId: current.actorId,
205
+ undo: Object.freeze(undo.slice(start)),
206
+ redo: Object.freeze([])
207
+ });
208
+ }
209
+ const source = change.kind === "undo" ? current.undo : current.redo;
210
+ const latest = source[source.length - 1];
211
+ if (!latest || latest.commandId !== change.expectedCommandId) throw new LocalSyncConsistencyError("Local undo history changed before it could be committed.");
212
+ if (change.kind === "undo") return Object.freeze({
213
+ documentId: current.documentId,
214
+ actorId: current.actorId,
215
+ undo: Object.freeze(current.undo.slice(0, -1)),
216
+ redo: Object.freeze([...current.redo, change.entry])
217
+ });
218
+ return Object.freeze({
219
+ documentId: current.documentId,
220
+ actorId: current.actorId,
221
+ undo: Object.freeze([...current.undo, change.entry]),
222
+ redo: Object.freeze(current.redo.slice(0, -1))
223
+ });
224
+ };
225
+ const deleteCommitsThrough = async (store, documentId, seq) => {
226
+ const range = keyRange().bound([documentId, 0], [documentId, seq]);
227
+ await new Promise((resolve, reject) => {
228
+ const cursor = store.openCursor(range);
229
+ cursor.onerror = () => reject(cursor.error ?? /* @__PURE__ */ new Error("IndexedDB cursor failed."));
230
+ cursor.onsuccess = () => {
231
+ const current = cursor.result;
232
+ if (!current) {
233
+ resolve();
234
+ return;
235
+ }
236
+ current.delete();
237
+ current.continue();
238
+ };
239
+ });
240
+ };
241
+ const openIndexedDbTimeline = async (databaseName) => {
242
+ const database = await new Promise((resolve, reject) => {
243
+ const open = factory().open(databaseName, DATABASE_VERSION);
244
+ open.onerror = () => reject(open.error ?? /* @__PURE__ */ new Error("Unable to open IndexedDB."));
245
+ open.onblocked = () => reject(/* @__PURE__ */ new Error(`IndexedDB database '${databaseName}' is blocked.`));
246
+ open.onupgradeneeded = () => {
247
+ const db = open.result;
248
+ if (!db.objectStoreNames.contains(DOCUMENTS)) db.createObjectStore(DOCUMENTS, { keyPath: "documentId" });
249
+ if (!db.objectStoreNames.contains(COMMITS)) db.createObjectStore(COMMITS, { keyPath: ["documentId", "seq"] });
250
+ if (!db.objectStoreNames.contains(ACTORS)) db.createObjectStore(ACTORS, { keyPath: ["documentId", "actorId"] });
251
+ };
252
+ open.onsuccess = () => resolve(open.result);
253
+ });
254
+ const readDocument = async (transaction, documentId) => {
255
+ const value = await request(transaction.objectStore(DOCUMENTS).get(documentId));
256
+ if (value === void 0) throw new LocalSyncConsistencyError(`Local document '${documentId}' does not exist.`);
257
+ return documentRecord(value);
258
+ };
259
+ return {
260
+ initialize: async (documentId, schemaVersion, checkpoint) => {
261
+ const transaction = database.transaction(DOCUMENTS, "readwrite");
262
+ const store = transaction.objectStore(DOCUMENTS);
263
+ const existing = await request(store.get(documentId));
264
+ if (existing !== void 0) {
265
+ const record = documentRecord(existing);
266
+ if (record.schemaVersion !== schemaVersion) throw new LocalSyncSchemaError(documentId, schemaVersion, record.schemaVersion);
267
+ await complete(transaction);
268
+ return record;
269
+ }
270
+ const record = {
271
+ documentId,
272
+ schemaVersion,
273
+ checkpointSeq: 0,
274
+ headSeq: 0,
275
+ checkpoint
276
+ };
277
+ store.put(record);
278
+ await complete(transaction);
279
+ return Object.freeze(record);
280
+ },
281
+ read: async (documentId) => {
282
+ const transaction = database.transaction(DOCUMENTS, "readonly");
283
+ const record = await readDocument(transaction, documentId);
284
+ await complete(transaction);
285
+ return record;
286
+ },
287
+ tail: async (documentId, afterSeq) => {
288
+ const transaction = database.transaction(COMMITS, "readonly");
289
+ const range = keyRange().bound([documentId, afterSeq + 1], [documentId, Number.MAX_SAFE_INTEGER]);
290
+ const values = await request(transaction.objectStore(COMMITS).getAll(range));
291
+ await complete(transaction);
292
+ return Object.freeze(values.map(commitRecord).sort((left, right) => left.seq - right.seq));
293
+ },
294
+ history: async (documentId, actorId) => {
295
+ const transaction = database.transaction(ACTORS, "readonly");
296
+ const value = await request(transaction.objectStore(ACTORS).get([documentId, actorId]));
297
+ await complete(transaction);
298
+ return actorRecord(value, documentId, actorId);
299
+ },
300
+ append: async (input) => {
301
+ const transaction = database.transaction([
302
+ DOCUMENTS,
303
+ COMMITS,
304
+ ACTORS
305
+ ], "readwrite");
306
+ const documents = transaction.objectStore(DOCUMENTS);
307
+ const commits = transaction.objectStore(COMMITS);
308
+ const actors = transaction.objectStore(ACTORS);
309
+ const current = await readDocument(transaction, input.documentId);
310
+ if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the writer lock was acquired.");
311
+ const actor = actorRecord(await request(actors.get([input.documentId, input.actorId])), input.documentId, input.actorId);
312
+ const seq = current.headSeq + 1;
313
+ const commit = {
314
+ documentId: input.documentId,
315
+ seq,
316
+ commandId: input.commandId,
317
+ actorId: input.actorId,
318
+ kind: input.kind,
319
+ operations: input.operations,
320
+ inverse: input.inverse,
321
+ footprint: input.footprint,
322
+ createdAt: Date.now()
323
+ };
324
+ const nextDocument = {
325
+ ...current,
326
+ headSeq: seq
327
+ };
328
+ commits.put(commit);
329
+ documents.put(nextDocument);
330
+ actors.put(nextHistory(actor, input.history));
331
+ await complete(transaction);
332
+ return Object.freeze(commit);
333
+ },
334
+ compact: async (documentId, seq, checkpoint) => {
335
+ const transaction = database.transaction([DOCUMENTS, COMMITS], "readwrite");
336
+ const current = await readDocument(transaction, documentId);
337
+ if (seq < current.checkpointSeq || seq > current.headSeq) throw new LocalSyncConsistencyError("Local checkpoint sequence is outside the durable timeline.");
338
+ const next = {
339
+ ...current,
340
+ checkpointSeq: seq,
341
+ checkpoint
342
+ };
343
+ transaction.objectStore(DOCUMENTS).put(next);
344
+ await deleteCommitsThrough(transaction.objectStore(COMMITS), documentId, seq);
345
+ await complete(transaction);
346
+ return Object.freeze(next);
347
+ },
348
+ close: () => database.close()
349
+ };
350
+ };
351
+ //#endregion
352
+ //#region core/src/local-sync/session.ts
353
+ const locks = () => {
354
+ const value = globalThis.navigator?.locks;
355
+ if (typeof value !== "object" || value === null || !("request" in value) || typeof value.request !== "function") throw new LocalSyncUnavailableError("Web Locks");
356
+ return value;
357
+ };
358
+ const channelConstructor = () => {
359
+ const value = globalThis.BroadcastChannel;
360
+ if (typeof value !== "function") throw new LocalSyncUnavailableError("BroadcastChannel");
361
+ return value;
362
+ };
363
+ const requiredString = (value, label) => {
364
+ if (value.length > 0) return value;
365
+ throw new TypeError(`${label} must not be empty.`);
366
+ };
367
+ const nonNegativeInteger = (value, label) => {
368
+ if (Number.isSafeInteger(value) && value >= 0) return value;
369
+ throw new TypeError(`${label} must be a non-negative safe integer.`);
370
+ };
371
+ const positiveInteger = (value, label) => {
372
+ if (Number.isSafeInteger(value) && value > 0) return value;
373
+ throw new TypeError(`${label} must be a positive safe integer.`);
374
+ };
375
+ const identifier = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
376
+ const notification = (value) => {
377
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
378
+ const record = value;
379
+ if (record.kind !== "commit" || typeof record.documentId !== "string" || typeof record.senderId !== "string" || typeof record.headSeq !== "number" || !Number.isSafeInteger(record.headSeq) || record.headSeq < 0) return void 0;
380
+ return record;
381
+ };
382
+ const localCommit = (commit, stored) => Object.freeze({
383
+ ...commit,
384
+ seq: stored.seq,
385
+ commandId: stored.commandId,
386
+ actorId: stored.actorId
387
+ });
388
+ const operationResult = (result, stored) => {
389
+ if (result.status !== "committed") throw new LocalSyncConsistencyError("A durable local command could not be applied.");
390
+ return {
391
+ status: "committed",
392
+ commit: localCommit(result.commit, stored),
393
+ observerErrors: result.observerErrors
394
+ };
395
+ };
396
+ const openLocalDocument = async (input) => {
397
+ const databaseName = requiredString(input.database, "database");
398
+ const documentId = requiredString(input.documentId, "documentId");
399
+ const actorId = requiredString(input.actorId ?? "local", "actorId");
400
+ const schemaVersion = positiveInteger(input.schemaVersion ?? 1, "schemaVersion");
401
+ const historyCapacity = nonNegativeInteger(input.historyCapacity ?? 100, "historyCapacity");
402
+ const checkpointEvery = nonNegativeInteger(input.checkpointEvery ?? 500, "checkpointEvery");
403
+ const commandLimits = input.commandLimits;
404
+ const initial = json(input.initial, "initial document");
405
+ const lockManager = locks();
406
+ const BroadcastChannel = channelConstructor();
407
+ const timeline = await openIndexedDbTimeline(databaseName);
408
+ let channel;
409
+ let runtime;
410
+ try {
411
+ const stored = await timeline.initialize(documentId, schemaVersion, initial);
412
+ const documentRuntime = require_readable.createDocument({
413
+ schema: input.schema,
414
+ initial: stored.checkpoint,
415
+ history: false
416
+ });
417
+ runtime = documentRuntime;
418
+ let headSeq = stored.checkpointSeq;
419
+ let checkpointSeq = stored.checkpointSeq;
420
+ let queued = Promise.resolve();
421
+ let fault;
422
+ let closing = false;
423
+ let disposed = false;
424
+ const senderId = identifier();
425
+ const fail = (error) => {
426
+ if (closing || disposed || fault) return;
427
+ fault = error;
428
+ try {
429
+ input.onError?.(error);
430
+ } catch {}
431
+ };
432
+ const assertOpen = () => {
433
+ if (closing || disposed) throw new LocalSyncDisposedError();
434
+ if (fault) throw fault;
435
+ };
436
+ const applyStored = (commit) => {
437
+ if (commit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("Local commit log contains a sequence gap.");
438
+ const local = operationResult(documentRuntime.apply(commit.operations, {
439
+ source: "local",
440
+ history: false
441
+ }), commit);
442
+ if (local.status !== "committed") throw new LocalSyncConsistencyError("A durable local command was not committed.");
443
+ headSeq = commit.seq;
444
+ return local.commit;
445
+ };
446
+ const catchUp = async () => {
447
+ const current = await timeline.read(documentId);
448
+ if (current.schemaVersion !== schemaVersion) throw new LocalSyncConsistencyError("Local document schema changed while this session was open.");
449
+ if (headSeq < current.checkpointSeq) {
450
+ const replaced = documentRuntime.replace(current.checkpoint, { source: "system" });
451
+ if (replaced.status !== "committed" && replaced.status !== "unchanged") throw new LocalSyncConsistencyError("Local checkpoint could not be restored.");
452
+ headSeq = current.checkpointSeq;
453
+ checkpointSeq = current.checkpointSeq;
454
+ }
455
+ const tail = await timeline.tail(documentId, headSeq);
456
+ for (const commit of tail) applyStored(commit);
457
+ if (headSeq !== current.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
458
+ checkpointSeq = current.checkpointSeq;
459
+ };
460
+ const execute = (run) => {
461
+ const next = queued.then(run, run);
462
+ queued = next.then(() => void 0, () => void 0);
463
+ return next;
464
+ };
465
+ const withWriter = (run) => lockManager.request(`doxum:${documentId}`, { mode: "exclusive" }, async () => {
466
+ assertOpen();
467
+ try {
468
+ await catchUp();
469
+ } catch (error) {
470
+ fail(error);
471
+ throw error;
472
+ }
473
+ return run();
474
+ });
475
+ const persistAndApply = async (input) => {
476
+ try {
477
+ const storedCommit = await timeline.append({
478
+ documentId,
479
+ expectedHeadSeq: headSeq,
480
+ commandId: input.commandId,
481
+ actorId,
482
+ kind: input.kind,
483
+ operations: input.operations,
484
+ inverse: input.inverse,
485
+ footprint: input.footprint,
486
+ history: input.history
487
+ });
488
+ const result = operationResult(documentRuntime.apply(storedCommit.operations, {
489
+ source: "local",
490
+ history: false
491
+ }), storedCommit);
492
+ headSeq = storedCommit.seq;
493
+ if (checkpointEvery > 0 && headSeq - checkpointSeq >= checkpointEvery) {
494
+ const checkpoint = json(documentRuntime.snapshot(), "local checkpoint");
495
+ checkpointSeq = (await timeline.compact(documentId, headSeq, checkpoint)).checkpointSeq;
496
+ }
497
+ channel?.postMessage({
498
+ kind: "commit",
499
+ documentId,
500
+ headSeq,
501
+ senderId
502
+ });
503
+ return result;
504
+ } catch (error) {
505
+ fail(error);
506
+ throw error;
507
+ }
508
+ };
509
+ const replay = (operations) => {
510
+ const candidate = require_readable.createDocument({
511
+ schema: input.schema,
512
+ initial: documentRuntime.snapshot(),
513
+ history: false
514
+ });
515
+ try {
516
+ return candidate.apply(operations, {
517
+ source: "local",
518
+ history: false
519
+ });
520
+ } finally {
521
+ candidate.dispose();
522
+ }
523
+ };
524
+ const applyHistory = async (history, kind) => {
525
+ const source = kind === "undo" ? history.undo : history.redo;
526
+ const entry = source[source.length - 1];
527
+ if (!entry) return {
528
+ status: "unchanged",
529
+ revision: documentRuntime.revision()
530
+ };
531
+ const preview = replay(kind === "undo" ? entry.inverse : entry.operations);
532
+ if (preview.status !== "committed") return preview;
533
+ const operations = jsonArray(preview.commit.operations, `${kind} operations`, commandLimits);
534
+ const inverse = jsonArray(preview.commit.inverse, `${kind} inverse`, commandLimits);
535
+ return persistAndApply({
536
+ commandId: identifier(),
537
+ kind,
538
+ operations,
539
+ inverse,
540
+ footprint: require_readable.commandFootprint(preview.commit.operations),
541
+ history: kind === "undo" ? {
542
+ kind: "undo",
543
+ expectedCommandId: entry.commandId,
544
+ entry
545
+ } : {
546
+ kind: "redo",
547
+ expectedCommandId: entry.commandId,
548
+ entry
549
+ }
550
+ });
551
+ };
552
+ const initialTail = await timeline.tail(documentId, headSeq);
553
+ for (const commit of initialTail) applyStored(commit);
554
+ if (headSeq !== stored.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
555
+ channel = new BroadcastChannel(`doxum:${databaseName}:${documentId}`);
556
+ channel.onmessage = (event) => {
557
+ const message = notification(event.data);
558
+ if (!message || message.documentId !== documentId || message.senderId === senderId || message.headSeq <= headSeq || closing || disposed) return;
559
+ execute(() => withWriter(async () => void 0)).catch(() => void 0);
560
+ };
561
+ const localDocument = {
562
+ document: require_readable.asReadable(documentRuntime),
563
+ state: () => {
564
+ if (disposed) return { status: "disposed" };
565
+ if (fault) return {
566
+ status: "failed",
567
+ error: fault
568
+ };
569
+ return {
570
+ status: "ready",
571
+ headSeq,
572
+ checkpointSeq
573
+ };
574
+ },
575
+ sync: () => execute(() => withWriter(async () => void 0)),
576
+ update: (run) => {
577
+ assertOpen();
578
+ return execute(() => withWriter(async () => {
579
+ const prepared = documentRuntime.prepare(run);
580
+ if (prepared.status === "unchanged") return {
581
+ status: "unchanged",
582
+ value: prepared.value,
583
+ revision: documentRuntime.revision(),
584
+ reports: prepared.reports
585
+ };
586
+ if (prepared.status === "rejected") return {
587
+ status: "rejected",
588
+ issues: prepared.issues,
589
+ revision: documentRuntime.revision()
590
+ };
591
+ const operations = jsonArray(prepared.operations, "local command operations", commandLimits);
592
+ const inverse = jsonArray(prepared.inverse, "local command inverse", commandLimits);
593
+ const commandId = identifier();
594
+ const result = await persistAndApply({
595
+ commandId,
596
+ kind: "update",
597
+ operations,
598
+ inverse,
599
+ footprint: prepared.footprint,
600
+ history: {
601
+ kind: "record",
602
+ entry: Object.freeze({
603
+ commandId,
604
+ operations,
605
+ inverse,
606
+ footprint: prepared.footprint
607
+ }),
608
+ capacity: historyCapacity
609
+ }
610
+ });
611
+ if (result.status !== "committed") throw new LocalSyncConsistencyError("Prepared local command was not committed.");
612
+ return {
613
+ status: "committed",
614
+ value: prepared.value,
615
+ commit: result.commit,
616
+ reports: prepared.reports,
617
+ observerErrors: result.observerErrors
618
+ };
619
+ }));
620
+ },
621
+ undo: () => {
622
+ assertOpen();
623
+ return execute(() => withWriter(async () => applyHistory(await timeline.history(documentId, actorId), "undo")));
624
+ },
625
+ redo: () => {
626
+ assertOpen();
627
+ return execute(() => withWriter(async () => applyHistory(await timeline.history(documentId, actorId), "redo")));
628
+ },
629
+ dispose: async () => {
630
+ if (disposed) return;
631
+ if (closing) {
632
+ await queued;
633
+ return;
634
+ }
635
+ closing = true;
636
+ await queued;
637
+ disposed = true;
638
+ channel?.close();
639
+ timeline.close();
640
+ documentRuntime.dispose();
641
+ }
642
+ };
643
+ return Object.freeze(localDocument);
644
+ } catch (error) {
645
+ channel?.close();
646
+ runtime?.dispose();
647
+ timeline.close();
648
+ throw error;
649
+ }
650
+ };
651
+ //#endregion
652
+ exports.LocalSyncConsistencyError = LocalSyncConsistencyError;
653
+ exports.LocalSyncDataError = LocalSyncDataError;
654
+ exports.LocalSyncDisposedError = LocalSyncDisposedError;
655
+ exports.LocalSyncSchemaError = LocalSyncSchemaError;
656
+ exports.LocalSyncUnavailableError = LocalSyncUnavailableError;
657
+ exports.defaultJsonCommandLimits = defaultJsonCommandLimits;
658
+ exports.openLocalDocument = openLocalDocument;
659
+
660
+ //# sourceMappingURL=local-sync.cjs.map