doxum 0.1.2 → 0.1.3

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 (48) hide show
  1. package/README.md +59 -27
  2. package/dist/{contract-DGkZcTiP.d.cts → contract-BsY1XLiG.d.ts} +2 -1
  3. package/dist/{contract-Dh3az8aN.d.ts → contract-D8kjdfqT.d.cts} +2 -1
  4. package/dist/{access-5D4KKFm7.js → dependency-C5_R1tvQ.js} +40 -262
  5. package/dist/dependency-C5_R1tvQ.js.map +1 -0
  6. package/dist/{access-z56fVqZ2.cjs → dependency-Dfzs3khk.cjs} +62 -344
  7. package/dist/dependency-Dfzs3khk.cjs.map +1 -0
  8. package/dist/driver-CNxqMVFH.cjs +69 -0
  9. package/dist/driver-CNxqMVFH.cjs.map +1 -0
  10. package/dist/driver-CbzfW5MR.js +46 -0
  11. package/dist/driver-CbzfW5MR.js.map +1 -0
  12. package/dist/index.cjs +2636 -46
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +2594 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/integration.cjs +2 -3
  19. package/dist/integration.cjs.map +1 -1
  20. package/dist/integration.d.cts +1 -1
  21. package/dist/integration.d.ts +1 -1
  22. package/dist/integration.js +1 -2
  23. package/dist/integration.js.map +1 -1
  24. package/dist/local-sync.cjs +154 -329
  25. package/dist/local-sync.cjs.map +1 -1
  26. package/dist/local-sync.d.cts +17 -22
  27. package/dist/local-sync.d.ts +17 -22
  28. package/dist/local-sync.js +149 -326
  29. package/dist/local-sync.js.map +1 -1
  30. package/dist/ownership-B-VAPcce.js +242 -0
  31. package/dist/ownership-B-VAPcce.js.map +1 -0
  32. package/dist/ownership-CU-9DKJQ.cjs +301 -0
  33. package/dist/ownership-CU-9DKJQ.cjs.map +1 -0
  34. package/package.json +1 -1
  35. package/skills/doxum-runtime/references/guide.en.md +52 -1
  36. package/skills/doxum-runtime/references/guide.zh-CN.md +46 -1
  37. package/skills/doxum-runtime/references/invariants.en.md +14 -4
  38. package/skills/doxum-runtime/references/invariants.zh-CN.md +1 -1
  39. package/dist/access-5D4KKFm7.js.map +0 -1
  40. package/dist/access-z56fVqZ2.cjs.map +0 -1
  41. package/dist/dependency-BRn1TRDi.js +0 -20
  42. package/dist/dependency-BRn1TRDi.js.map +0 -1
  43. package/dist/dependency-DmQXyj7q.cjs +0 -25
  44. package/dist/dependency-DmQXyj7q.cjs.map +0 -1
  45. package/dist/readable-B8E3FMq6.cjs +0 -2614
  46. package/dist/readable-B8E3FMq6.cjs.map +0 -1
  47. package/dist/readable-CIj02gIj.js +0 -2579
  48. package/dist/readable-CIj02gIj.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { f as isPlainObject, p as isRecord } from "./access-5D4KKFm7.js";
2
- import { a as decodeCommandFootprint, i as commandFootprint, n as createDocument, t as asReadable } from "./readable-CIj02gIj.js";
1
+ import { i as isRecord, r as isPlainObject } from "./ownership-B-VAPcce.js";
2
+ import { i as installRuntimeWriteDriver } from "./driver-CbzfW5MR.js";
3
3
  //#region core/src/local-sync/contract.ts
4
4
  var LocalSyncUnavailableError = class extends Error {
5
5
  constructor(capability) {
@@ -19,9 +19,21 @@ var LocalSyncConsistencyError = class extends Error {
19
19
  this.name = "LocalSyncConsistencyError";
20
20
  }
21
21
  };
22
+ var LocalSyncReadOnlyError = class extends Error {
23
+ constructor() {
24
+ super("This tab is following the local document and cannot write until it becomes the leader.");
25
+ this.name = "LocalSyncReadOnlyError";
26
+ }
27
+ };
28
+ var LocalSyncUnsupportedOperationError = class extends Error {
29
+ constructor() {
30
+ super("Local sync persists operation commands. runtime.replace() and externally supplied remote operations are unavailable while it is attached.");
31
+ this.name = "LocalSyncUnsupportedOperationError";
32
+ }
33
+ };
22
34
  var LocalSyncDisposedError = class extends Error {
23
35
  constructor() {
24
- super("Local sync document has been disposed.");
36
+ super("Local sync has been disposed.");
25
37
  this.name = "LocalSyncDisposedError";
26
38
  }
27
39
  };
@@ -96,7 +108,7 @@ const jsonArray = (value, label, input) => {
96
108
  };
97
109
  //#endregion
98
110
  //#region core/src/local-sync/timeline.ts
99
- const DATABASE_VERSION = 1;
111
+ const DATABASE_VERSION = 2;
100
112
  const DOCUMENTS = "documents";
101
113
  const COMMITS = "commits";
102
114
  const ACTORS = "actors";
@@ -121,34 +133,19 @@ const string = (value, label) => {
121
133
  if (typeof value === "string") return value;
122
134
  throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
123
135
  };
124
- const nonNegativeInteger$1 = (value, label) => {
136
+ const nonNegativeInteger = (value, label) => {
125
137
  if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
126
138
  throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
127
139
  };
128
140
  const positiveInteger$1 = (value, label) => {
129
- const parsed = nonNegativeInteger$1(value, label);
141
+ const parsed = nonNegativeInteger(value, label);
130
142
  if (parsed > 0) return parsed;
131
143
  throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
132
144
  };
133
- const historyEntry = (value, label) => {
134
- if (!isRecord(value)) throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
135
- const footprint = decodeCommandFootprint(value.footprint);
136
- if (!footprint) throw new LocalSyncConsistencyError(`${label}.footprint is malformed in IndexedDB.`);
137
- return Object.freeze({
138
- commandId: string(value.commandId, `${label}.commandId`),
139
- operations: jsonArray(value.operations, `${label}.operations`),
140
- inverse: jsonArray(value.inverse, `${label}.inverse`),
141
- footprint
142
- });
143
- };
144
- const historyEntries = (value, label) => {
145
- if (!Array.isArray(value)) throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
146
- return Object.freeze(value.map((entry, index) => historyEntry(entry, `${label}.${index}`)));
147
- };
148
145
  const documentRecord = (value) => {
149
146
  if (!isRecord(value)) throw new LocalSyncConsistencyError("Document record is malformed in IndexedDB.");
150
- const checkpointSeq = nonNegativeInteger$1(value.checkpointSeq, "document.checkpointSeq");
151
- const headSeq = nonNegativeInteger$1(value.headSeq, "document.headSeq");
147
+ const checkpointSeq = nonNegativeInteger(value.checkpointSeq, "document.checkpointSeq");
148
+ const headSeq = nonNegativeInteger(value.headSeq, "document.headSeq");
152
149
  if (checkpointSeq > headSeq) throw new LocalSyncConsistencyError("Document checkpoint exceeds its head sequence.");
153
150
  return Object.freeze({
154
151
  documentId: string(value.documentId, "document.documentId"),
@@ -160,81 +157,11 @@ const documentRecord = (value) => {
160
157
  };
161
158
  const commitRecord = (value) => {
162
159
  if (!isRecord(value)) throw new LocalSyncConsistencyError("Commit record is malformed in IndexedDB.");
163
- const kind = value.kind;
164
- if (kind !== "update" && kind !== "undo" && kind !== "redo") throw new LocalSyncConsistencyError("Commit kind is malformed in IndexedDB.");
165
- const footprint = decodeCommandFootprint(value.footprint);
166
- if (!footprint) throw new LocalSyncConsistencyError("Commit footprint is malformed in IndexedDB.");
167
160
  return Object.freeze({
168
161
  documentId: string(value.documentId, "commit.documentId"),
169
162
  seq: positiveInteger$1(value.seq, "commit.seq"),
170
- commandId: string(value.commandId, "commit.commandId"),
171
- actorId: string(value.actorId, "commit.actorId"),
172
- kind,
173
163
  operations: jsonArray(value.operations, "commit.operations"),
174
- inverse: jsonArray(value.inverse, "commit.inverse"),
175
- footprint,
176
- createdAt: nonNegativeInteger$1(value.createdAt, "commit.createdAt")
177
- });
178
- };
179
- const actorRecord = (value, documentId, actorId) => {
180
- if (value === void 0) return Object.freeze({
181
- documentId,
182
- actorId,
183
- undo: Object.freeze([]),
184
- redo: Object.freeze([])
185
- });
186
- if (!isRecord(value)) throw new LocalSyncConsistencyError("Actor history is malformed in IndexedDB.");
187
- const storedDocumentId = string(value.documentId, "actor.documentId");
188
- const storedActorId = string(value.actorId, "actor.actorId");
189
- if (storedDocumentId !== documentId || storedActorId !== actorId) throw new LocalSyncConsistencyError("Actor history key does not match its record.");
190
- return Object.freeze({
191
- documentId,
192
- actorId,
193
- undo: historyEntries(value.undo, "actor.undo"),
194
- redo: historyEntries(value.redo, "actor.redo")
195
- });
196
- };
197
- const nextHistory = (current, change) => {
198
- if (change.kind === "record") {
199
- const undo = [...current.undo, change.entry];
200
- const start = Math.max(0, undo.length - change.capacity);
201
- return Object.freeze({
202
- documentId: current.documentId,
203
- actorId: current.actorId,
204
- undo: Object.freeze(undo.slice(start)),
205
- redo: Object.freeze([])
206
- });
207
- }
208
- const source = change.kind === "undo" ? current.undo : current.redo;
209
- const latest = source[source.length - 1];
210
- if (!latest || latest.commandId !== change.expectedCommandId) throw new LocalSyncConsistencyError("Local undo history changed before it could be committed.");
211
- if (change.kind === "undo") return Object.freeze({
212
- documentId: current.documentId,
213
- actorId: current.actorId,
214
- undo: Object.freeze(current.undo.slice(0, -1)),
215
- redo: Object.freeze([...current.redo, change.entry])
216
- });
217
- return Object.freeze({
218
- documentId: current.documentId,
219
- actorId: current.actorId,
220
- undo: Object.freeze([...current.undo, change.entry]),
221
- redo: Object.freeze(current.redo.slice(0, -1))
222
- });
223
- };
224
- const deleteCommitsThrough = async (store, documentId, seq) => {
225
- const range = keyRange().bound([documentId, 0], [documentId, seq]);
226
- await new Promise((resolve, reject) => {
227
- const cursor = store.openCursor(range);
228
- cursor.onerror = () => reject(cursor.error ?? /* @__PURE__ */ new Error("IndexedDB cursor failed."));
229
- cursor.onsuccess = () => {
230
- const current = cursor.result;
231
- if (!current) {
232
- resolve();
233
- return;
234
- }
235
- current.delete();
236
- current.continue();
237
- };
164
+ createdAt: nonNegativeInteger(value.createdAt, "commit.createdAt")
238
165
  });
239
166
  };
240
167
  const openIndexedDbTimeline = async (databaseName) => {
@@ -246,7 +173,7 @@ const openIndexedDbTimeline = async (databaseName) => {
246
173
  const db = open.result;
247
174
  if (!db.objectStoreNames.contains(DOCUMENTS)) db.createObjectStore(DOCUMENTS, { keyPath: "documentId" });
248
175
  if (!db.objectStoreNames.contains(COMMITS)) db.createObjectStore(COMMITS, { keyPath: ["documentId", "seq"] });
249
- if (!db.objectStoreNames.contains(ACTORS)) db.createObjectStore(ACTORS, { keyPath: ["documentId", "actorId"] });
176
+ if (db.objectStoreNames.contains(ACTORS)) db.deleteObjectStore(ACTORS);
250
177
  };
251
178
  open.onsuccess = () => resolve(open.result);
252
179
  });
@@ -290,60 +217,25 @@ const openIndexedDbTimeline = async (databaseName) => {
290
217
  await complete(transaction);
291
218
  return Object.freeze(values.map(commitRecord).sort((left, right) => left.seq - right.seq));
292
219
  },
293
- history: async (documentId, actorId) => {
294
- const transaction = database.transaction(ACTORS, "readonly");
295
- const value = await request(transaction.objectStore(ACTORS).get([documentId, actorId]));
296
- await complete(transaction);
297
- return actorRecord(value, documentId, actorId);
298
- },
299
220
  append: async (input) => {
300
- const transaction = database.transaction([
301
- DOCUMENTS,
302
- COMMITS,
303
- ACTORS
304
- ], "readwrite");
221
+ const transaction = database.transaction([DOCUMENTS, COMMITS], "readwrite");
305
222
  const documents = transaction.objectStore(DOCUMENTS);
306
- const commits = transaction.objectStore(COMMITS);
307
- const actors = transaction.objectStore(ACTORS);
308
223
  const current = await readDocument(transaction, input.documentId);
309
- if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the writer lock was acquired.");
310
- const actor = actorRecord(await request(actors.get([input.documentId, input.actorId])), input.documentId, input.actorId);
311
- const seq = current.headSeq + 1;
224
+ if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the leader could append its command.");
312
225
  const commit = {
313
226
  documentId: input.documentId,
314
- seq,
315
- commandId: input.commandId,
316
- actorId: input.actorId,
317
- kind: input.kind,
227
+ seq: current.headSeq + 1,
318
228
  operations: input.operations,
319
- inverse: input.inverse,
320
- footprint: input.footprint,
321
229
  createdAt: Date.now()
322
230
  };
323
- const nextDocument = {
231
+ documents.put({
324
232
  ...current,
325
- headSeq: seq
326
- };
327
- commits.put(commit);
328
- documents.put(nextDocument);
329
- actors.put(nextHistory(actor, input.history));
233
+ headSeq: commit.seq
234
+ });
235
+ transaction.objectStore(COMMITS).put(commit);
330
236
  await complete(transaction);
331
237
  return Object.freeze(commit);
332
238
  },
333
- compact: async (documentId, seq, checkpoint) => {
334
- const transaction = database.transaction([DOCUMENTS, COMMITS], "readwrite");
335
- const current = await readDocument(transaction, documentId);
336
- if (seq < current.checkpointSeq || seq > current.headSeq) throw new LocalSyncConsistencyError("Local checkpoint sequence is outside the durable timeline.");
337
- const next = {
338
- ...current,
339
- checkpointSeq: seq,
340
- checkpoint
341
- };
342
- transaction.objectStore(DOCUMENTS).put(next);
343
- await deleteCommitsThrough(transaction.objectStore(COMMITS), documentId, seq);
344
- await complete(transaction);
345
- return Object.freeze(next);
346
- },
347
239
  close: () => database.close()
348
240
  };
349
241
  };
@@ -363,66 +255,40 @@ const requiredString = (value, label) => {
363
255
  if (value.length > 0) return value;
364
256
  throw new TypeError(`${label} must not be empty.`);
365
257
  };
366
- const nonNegativeInteger = (value, label) => {
367
- if (Number.isSafeInteger(value) && value >= 0) return value;
368
- throw new TypeError(`${label} must be a non-negative safe integer.`);
369
- };
370
258
  const positiveInteger = (value, label) => {
371
259
  if (Number.isSafeInteger(value) && value > 0) return value;
372
260
  throw new TypeError(`${label} must be a positive safe integer.`);
373
261
  };
374
- const identifier = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
375
262
  const notification = (value) => {
376
263
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
377
264
  const record = value;
378
- 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;
265
+ if (record.kind !== "commit" || typeof record.documentId !== "string" || typeof record.headSeq !== "number" || !Number.isSafeInteger(record.headSeq) || record.headSeq < 0) return void 0;
379
266
  return record;
380
267
  };
381
- const localCommit = (commit, stored) => Object.freeze({
382
- ...commit,
383
- seq: stored.seq,
384
- commandId: stored.commandId,
385
- actorId: stored.actorId
386
- });
387
- const operationResult = (result, stored) => {
388
- if (result.status !== "committed") throw new LocalSyncConsistencyError("A durable local command could not be applied.");
389
- return {
390
- status: "committed",
391
- commit: localCommit(result.commit, stored),
392
- observerErrors: result.observerErrors
393
- };
394
- };
395
- const openLocalDocument = async (input) => {
268
+ const attachLocalSync = async (input) => {
269
+ const runtime = input.runtime;
396
270
  const databaseName = requiredString(input.database, "database");
397
271
  const documentId = requiredString(input.documentId, "documentId");
398
- const actorId = requiredString(input.actorId ?? "local", "actorId");
399
272
  const schemaVersion = positiveInteger(input.schemaVersion ?? 1, "schemaVersion");
400
- const historyCapacity = nonNegativeInteger(input.historyCapacity ?? 100, "historyCapacity");
401
- const checkpointEvery = nonNegativeInteger(input.checkpointEvery ?? 500, "checkpointEvery");
402
- const commandLimits = input.commandLimits;
403
- const initial = json(input.initial, "initial document");
404
273
  const lockManager = locks();
405
274
  const BroadcastChannel = channelConstructor();
406
275
  const timeline = await openIndexedDbTimeline(databaseName);
407
276
  let channel;
408
- let runtime;
277
+ let unsubscribe;
278
+ let driver;
409
279
  try {
410
- const stored = await timeline.initialize(documentId, schemaVersion, initial);
411
- const documentRuntime = createDocument({
412
- schema: input.schema,
413
- initial: stored.checkpoint,
414
- history: false
415
- });
416
- runtime = documentRuntime;
280
+ const stored = await timeline.initialize(documentId, schemaVersion, json(runtime.snapshot(), "runtime snapshot"));
417
281
  let headSeq = stored.checkpointSeq;
418
282
  let checkpointSeq = stored.checkpointSeq;
419
- let queued = Promise.resolve();
283
+ let role = "follower";
420
284
  let fault;
421
285
  let closing = false;
422
286
  let disposed = false;
423
- const senderId = identifier();
287
+ let releaseLeadership;
288
+ let leadershipLease;
289
+ let queued = Promise.resolve();
424
290
  const fail = (error) => {
425
- if (closing || disposed || fault) return;
291
+ if (fault || disposed) return;
426
292
  fault = error;
427
293
  try {
428
294
  input.onError?.(error);
@@ -432,198 +298,152 @@ const openLocalDocument = async (input) => {
432
298
  if (closing || disposed) throw new LocalSyncDisposedError();
433
299
  if (fault) throw fault;
434
300
  };
301
+ const assertLeader = (intent) => {
302
+ assertOpen();
303
+ if (role !== "leader") throw new LocalSyncReadOnlyError();
304
+ if (intent.kind === "replace" || intent.kind === "apply" && intent.source === "remote") throw new LocalSyncUnsupportedOperationError();
305
+ };
306
+ const runRuntime = (run) => driver?.run(run) ?? run();
435
307
  const applyStored = (commit) => {
436
308
  if (commit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("Local commit log contains a sequence gap.");
437
- const local = operationResult(documentRuntime.apply(commit.operations, {
438
- source: "local",
309
+ if (runRuntime(() => runtime.apply(commit.operations, {
310
+ source: "remote",
439
311
  history: false
440
- }), commit);
441
- if (local.status !== "committed") throw new LocalSyncConsistencyError("A durable local command was not committed.");
312
+ })).status !== "committed") throw new LocalSyncConsistencyError("A stored local command could not be applied.");
442
313
  headSeq = commit.seq;
443
- return local.commit;
444
314
  };
445
- const catchUp = async () => {
315
+ const restore = async (reset = false) => {
446
316
  const current = await timeline.read(documentId);
447
- if (current.schemaVersion !== schemaVersion) throw new LocalSyncConsistencyError("Local document schema changed while this session was open.");
448
- if (headSeq < current.checkpointSeq) {
449
- const replaced = documentRuntime.replace(current.checkpoint, { source: "system" });
450
- if (replaced.status !== "committed" && replaced.status !== "unchanged") throw new LocalSyncConsistencyError("Local checkpoint could not be restored.");
317
+ if (current.schemaVersion !== schemaVersion) throw new LocalSyncConsistencyError("Local document schema changed while this attachment was open.");
318
+ if (headSeq > current.headSeq) throw new LocalSyncConsistencyError("The local timeline moved behind this attachment.");
319
+ if (reset || headSeq < current.checkpointSeq) {
320
+ if (runRuntime(() => runtime.replace(current.checkpoint, { source: "remote" })).status === "rejected") throw new LocalSyncConsistencyError("The local checkpoint could not be restored.");
451
321
  headSeq = current.checkpointSeq;
452
- checkpointSeq = current.checkpointSeq;
453
322
  }
454
323
  const tail = await timeline.tail(documentId, headSeq);
455
324
  for (const commit of tail) applyStored(commit);
456
325
  if (headSeq !== current.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
457
326
  checkpointSeq = current.checkpointSeq;
458
327
  };
459
- const execute = (run) => {
328
+ const enqueue = (run) => {
460
329
  const next = queued.then(run, run);
461
- queued = next.then(() => void 0, () => void 0);
330
+ queued = next.then(() => void 0, (error) => {
331
+ fail(error);
332
+ });
462
333
  return next;
463
334
  };
464
- const withWriter = (run) => lockManager.request(`doxum:${documentId}`, { mode: "exclusive" }, async () => {
465
- assertOpen();
335
+ const persist = async (operations) => {
336
+ const storedCommit = await timeline.append({
337
+ documentId,
338
+ expectedHeadSeq: headSeq,
339
+ operations
340
+ });
341
+ if (storedCommit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("A local command was assigned an unexpected sequence.");
342
+ headSeq = storedCommit.seq;
343
+ channel?.postMessage({
344
+ kind: "commit",
345
+ documentId,
346
+ headSeq
347
+ });
348
+ };
349
+ const record = (commit) => {
466
350
  try {
467
- await catchUp();
351
+ const operations = jsonArray(commit.operations, "local command operations", input.commandLimits);
352
+ enqueue(() => persist(operations)).catch(() => void 0);
468
353
  } catch (error) {
469
354
  fail(error);
470
- throw error;
471
355
  }
472
- return run();
356
+ };
357
+ const holdLeadership = () => new Promise((resolve) => {
358
+ releaseLeadership = resolve;
473
359
  });
474
- const persistAndApply = async (input) => {
360
+ const lead = async (activated) => {
361
+ if (closing || disposed || fault) return;
362
+ await restore();
363
+ if (closing || disposed || fault) return;
364
+ role = "leader";
365
+ activated?.();
475
366
  try {
476
- const storedCommit = await timeline.append({
477
- documentId,
478
- expectedHeadSeq: headSeq,
479
- commandId: input.commandId,
480
- actorId,
481
- kind: input.kind,
482
- operations: input.operations,
483
- inverse: input.inverse,
484
- footprint: input.footprint,
485
- history: input.history
486
- });
487
- const result = operationResult(documentRuntime.apply(storedCommit.operations, {
488
- source: "local",
489
- history: false
490
- }), storedCommit);
491
- headSeq = storedCommit.seq;
492
- if (checkpointEvery > 0 && headSeq - checkpointSeq >= checkpointEvery) {
493
- const checkpoint = json(documentRuntime.snapshot(), "local checkpoint");
494
- checkpointSeq = (await timeline.compact(documentId, headSeq, checkpoint)).checkpointSeq;
495
- }
496
- channel?.postMessage({
497
- kind: "commit",
498
- documentId,
499
- headSeq,
500
- senderId
501
- });
502
- return result;
503
- } catch (error) {
504
- fail(error);
505
- throw error;
367
+ await holdLeadership();
368
+ } finally {
369
+ releaseLeadership = void 0;
370
+ if (!closing && !disposed && !fault) role = "follower";
506
371
  }
507
372
  };
508
- const replay = (operations) => {
509
- const candidate = createDocument({
510
- schema: input.schema,
511
- initial: documentRuntime.snapshot(),
512
- history: false
373
+ const watchLeadership = () => {
374
+ leadershipLease = lockManager.request(`doxum:${documentId}`, { mode: "exclusive" }, async () => {
375
+ try {
376
+ await lead();
377
+ } catch (error) {
378
+ fail(error);
379
+ }
380
+ }).catch((error) => {
381
+ if (!closing && !disposed) fail(error);
513
382
  });
514
- try {
515
- return candidate.apply(operations, {
516
- source: "local",
517
- history: false
518
- });
519
- } finally {
520
- candidate.dispose();
521
- }
522
383
  };
523
- const applyHistory = async (history, kind) => {
524
- const source = kind === "undo" ? history.undo : history.redo;
525
- const entry = source[source.length - 1];
526
- if (!entry) return {
527
- status: "unchanged",
528
- revision: documentRuntime.revision()
529
- };
530
- const preview = replay(kind === "undo" ? entry.inverse : entry.operations);
531
- if (preview.status !== "committed") return preview;
532
- const operations = jsonArray(preview.commit.operations, `${kind} operations`, commandLimits);
533
- const inverse = jsonArray(preview.commit.inverse, `${kind} inverse`, commandLimits);
534
- return persistAndApply({
535
- commandId: identifier(),
536
- kind,
537
- operations,
538
- inverse,
539
- footprint: commandFootprint(preview.commit.operations),
540
- history: kind === "undo" ? {
541
- kind: "undo",
542
- expectedCommandId: entry.commandId,
543
- entry
544
- } : {
545
- kind: "redo",
546
- expectedCommandId: entry.commandId,
547
- entry
384
+ const claimInitialLeadership = async () => {
385
+ let resolve;
386
+ const claimed = new Promise((done) => {
387
+ resolve = done;
388
+ });
389
+ leadershipLease = lockManager.request(`doxum:${documentId}`, {
390
+ mode: "exclusive",
391
+ ifAvailable: true
392
+ }, async (lock) => {
393
+ if (lock === null) {
394
+ resolve(false);
395
+ return;
548
396
  }
397
+ try {
398
+ await lead(() => resolve(true));
399
+ } catch (error) {
400
+ fail(error);
401
+ resolve(false);
402
+ }
403
+ }).catch((error) => {
404
+ if (!closing && !disposed) fail(error);
405
+ resolve(false);
549
406
  });
407
+ if (!await claimed && !fault && !closing && !disposed) watchLeadership();
550
408
  };
551
- const initialTail = await timeline.tail(documentId, headSeq);
552
- for (const commit of initialTail) applyStored(commit);
553
- if (headSeq !== stored.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
409
+ await restore(true);
410
+ runtime.history.clear();
411
+ driver = installRuntimeWriteDriver(runtime, { assertWritable: assertLeader });
554
412
  channel = new BroadcastChannel(`doxum:${databaseName}:${documentId}`);
413
+ unsubscribe = runtime.subscribe((commit) => {
414
+ if ((commit.source === "local" || commit.source === "system" || commit.source === "history") && !closing && !disposed) record(commit);
415
+ });
555
416
  channel.onmessage = (event) => {
556
417
  const message = notification(event.data);
557
- if (!message || message.documentId !== documentId || message.senderId === senderId || message.headSeq <= headSeq || closing || disposed) return;
558
- execute(() => withWriter(async () => void 0)).catch(() => void 0);
418
+ if (!message || message.documentId !== documentId || message.headSeq <= headSeq || closing || disposed || fault) return;
419
+ enqueue(restore).catch(() => void 0);
559
420
  };
560
- const localDocument = {
561
- document: asReadable(documentRuntime),
421
+ await claimInitialLeadership();
422
+ if (fault) throw fault;
423
+ return Object.freeze({
562
424
  state: () => {
563
425
  if (disposed) return { status: "disposed" };
564
426
  if (fault) return {
565
- status: "failed",
427
+ status: "error",
428
+ headSeq,
429
+ checkpointSeq,
566
430
  error: fault
567
431
  };
568
432
  return {
569
- status: "ready",
433
+ status: role,
570
434
  headSeq,
571
435
  checkpointSeq
572
436
  };
573
437
  },
574
- sync: () => execute(() => withWriter(async () => void 0)),
575
- update: (run) => {
576
- assertOpen();
577
- return execute(() => withWriter(async () => {
578
- const prepared = documentRuntime.prepare(run);
579
- if (prepared.status === "unchanged") return {
580
- status: "unchanged",
581
- value: prepared.value,
582
- revision: documentRuntime.revision(),
583
- reports: prepared.reports
584
- };
585
- if (prepared.status === "rejected") return {
586
- status: "rejected",
587
- issues: prepared.issues,
588
- revision: documentRuntime.revision()
589
- };
590
- const operations = jsonArray(prepared.operations, "local command operations", commandLimits);
591
- const inverse = jsonArray(prepared.inverse, "local command inverse", commandLimits);
592
- const commandId = identifier();
593
- const result = await persistAndApply({
594
- commandId,
595
- kind: "update",
596
- operations,
597
- inverse,
598
- footprint: prepared.footprint,
599
- history: {
600
- kind: "record",
601
- entry: Object.freeze({
602
- commandId,
603
- operations,
604
- inverse,
605
- footprint: prepared.footprint
606
- }),
607
- capacity: historyCapacity
608
- }
609
- });
610
- if (result.status !== "committed") throw new LocalSyncConsistencyError("Prepared local command was not committed.");
611
- return {
612
- status: "committed",
613
- value: prepared.value,
614
- commit: result.commit,
615
- reports: prepared.reports,
616
- observerErrors: result.observerErrors
617
- };
618
- }));
619
- },
620
- undo: () => {
438
+ flush: async () => {
621
439
  assertOpen();
622
- return execute(() => withWriter(async () => applyHistory(await timeline.history(documentId, actorId), "undo")));
623
- },
624
- redo: () => {
440
+ try {
441
+ await enqueue(restore);
442
+ } catch (error) {
443
+ fail(error);
444
+ throw error;
445
+ }
625
446
  assertOpen();
626
- return execute(() => withWriter(async () => applyHistory(await timeline.history(documentId, actorId), "redo")));
627
447
  },
628
448
  dispose: async () => {
629
449
  if (disposed) return;
@@ -632,22 +452,25 @@ const openLocalDocument = async (input) => {
632
452
  return;
633
453
  }
634
454
  closing = true;
455
+ unsubscribe?.();
635
456
  await queued;
457
+ releaseLeadership?.();
458
+ if (role === "leader") await leadershipLease;
636
459
  disposed = true;
460
+ driver?.dispose();
637
461
  channel?.close();
638
462
  timeline.close();
639
- documentRuntime.dispose();
640
463
  }
641
- };
642
- return Object.freeze(localDocument);
464
+ });
643
465
  } catch (error) {
466
+ unsubscribe?.();
467
+ driver?.dispose();
644
468
  channel?.close();
645
- runtime?.dispose();
646
469
  timeline.close();
647
470
  throw error;
648
471
  }
649
472
  };
650
473
  //#endregion
651
- export { LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncSchemaError, LocalSyncUnavailableError, defaultJsonCommandLimits, openLocalDocument };
474
+ export { LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncReadOnlyError, LocalSyncSchemaError, LocalSyncUnavailableError, LocalSyncUnsupportedOperationError, attachLocalSync, defaultJsonCommandLimits };
652
475
 
653
476
  //# sourceMappingURL=local-sync.js.map