doxum 0.1.2 → 0.1.4
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.
- package/README.md +59 -27
- package/dist/{contract-DGkZcTiP.d.cts → contract-BsY1XLiG.d.ts} +2 -1
- package/dist/{contract-Dh3az8aN.d.ts → contract-D8kjdfqT.d.cts} +2 -1
- package/dist/{access-5D4KKFm7.js → dependency-C5_R1tvQ.js} +40 -262
- package/dist/dependency-C5_R1tvQ.js.map +1 -0
- package/dist/{access-z56fVqZ2.cjs → dependency-Dfzs3khk.cjs} +62 -344
- package/dist/dependency-Dfzs3khk.cjs.map +1 -0
- package/dist/driver-CNxqMVFH.cjs +69 -0
- package/dist/driver-CNxqMVFH.cjs.map +1 -0
- package/dist/driver-CbzfW5MR.js +46 -0
- package/dist/driver-CbzfW5MR.js.map +1 -0
- package/dist/index.cjs +2636 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2594 -4
- package/dist/index.js.map +1 -1
- package/dist/integration.cjs +2 -3
- package/dist/integration.cjs.map +1 -1
- package/dist/integration.d.cts +1 -1
- package/dist/integration.d.ts +1 -1
- package/dist/integration.js +1 -2
- package/dist/integration.js.map +1 -1
- package/dist/local-sync.cjs +154 -329
- package/dist/local-sync.cjs.map +1 -1
- package/dist/local-sync.d.cts +17 -22
- package/dist/local-sync.d.ts +17 -22
- package/dist/local-sync.js +149 -326
- package/dist/local-sync.js.map +1 -1
- package/dist/ownership-B-VAPcce.js +242 -0
- package/dist/ownership-B-VAPcce.js.map +1 -0
- package/dist/ownership-CU-9DKJQ.cjs +301 -0
- package/dist/ownership-CU-9DKJQ.cjs.map +1 -0
- package/package.json +1 -1
- package/skills/doxum-runtime/references/guide.en.md +52 -1
- package/skills/doxum-runtime/references/guide.zh-CN.md +46 -1
- package/skills/doxum-runtime/references/invariants.en.md +14 -4
- package/skills/doxum-runtime/references/invariants.zh-CN.md +1 -1
- package/dist/access-5D4KKFm7.js.map +0 -1
- package/dist/access-z56fVqZ2.cjs.map +0 -1
- package/dist/dependency-BRn1TRDi.js +0 -20
- package/dist/dependency-BRn1TRDi.js.map +0 -1
- package/dist/dependency-DmQXyj7q.cjs +0 -25
- package/dist/dependency-DmQXyj7q.cjs.map +0 -1
- package/dist/readable-B8E3FMq6.cjs +0 -2614
- package/dist/readable-B8E3FMq6.cjs.map +0 -1
- package/dist/readable-CIj02gIj.js +0 -2579
- package/dist/readable-CIj02gIj.js.map +0 -1
package/dist/local-sync.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const require_ownership = require("./ownership-CU-9DKJQ.cjs");
|
|
3
|
+
const require_driver = require("./driver-CNxqMVFH.cjs");
|
|
4
4
|
//#region core/src/local-sync/contract.ts
|
|
5
5
|
var LocalSyncUnavailableError = class extends Error {
|
|
6
6
|
constructor(capability) {
|
|
@@ -20,9 +20,21 @@ var LocalSyncConsistencyError = class extends Error {
|
|
|
20
20
|
this.name = "LocalSyncConsistencyError";
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
var LocalSyncReadOnlyError = class extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("This tab is following the local document and cannot write until it becomes the leader.");
|
|
26
|
+
this.name = "LocalSyncReadOnlyError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var LocalSyncUnsupportedOperationError = class extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("Local sync persists operation commands. runtime.replace() and externally supplied remote operations are unavailable while it is attached.");
|
|
32
|
+
this.name = "LocalSyncUnsupportedOperationError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
23
35
|
var LocalSyncDisposedError = class extends Error {
|
|
24
36
|
constructor() {
|
|
25
|
-
super("Local sync
|
|
37
|
+
super("Local sync has been disposed.");
|
|
26
38
|
this.name = "LocalSyncDisposedError";
|
|
27
39
|
}
|
|
28
40
|
};
|
|
@@ -73,7 +85,7 @@ const validate = (value, path, limits, depth, ancestors) => {
|
|
|
73
85
|
}
|
|
74
86
|
return value;
|
|
75
87
|
}
|
|
76
|
-
if (
|
|
88
|
+
if (require_ownership.isPlainObject(value)) {
|
|
77
89
|
if (ancestors.has(value)) throw new LocalSyncDataError(`${describePath(path)} contains a cycle.`);
|
|
78
90
|
ancestors.add(value);
|
|
79
91
|
try {
|
|
@@ -97,7 +109,7 @@ const jsonArray = (value, label, input) => {
|
|
|
97
109
|
};
|
|
98
110
|
//#endregion
|
|
99
111
|
//#region core/src/local-sync/timeline.ts
|
|
100
|
-
const DATABASE_VERSION =
|
|
112
|
+
const DATABASE_VERSION = 2;
|
|
101
113
|
const DOCUMENTS = "documents";
|
|
102
114
|
const COMMITS = "commits";
|
|
103
115
|
const ACTORS = "actors";
|
|
@@ -122,34 +134,19 @@ const string = (value, label) => {
|
|
|
122
134
|
if (typeof value === "string") return value;
|
|
123
135
|
throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
|
|
124
136
|
};
|
|
125
|
-
const nonNegativeInteger
|
|
137
|
+
const nonNegativeInteger = (value, label) => {
|
|
126
138
|
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
127
139
|
throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
|
|
128
140
|
};
|
|
129
141
|
const positiveInteger$1 = (value, label) => {
|
|
130
|
-
const parsed = nonNegativeInteger
|
|
142
|
+
const parsed = nonNegativeInteger(value, label);
|
|
131
143
|
if (parsed > 0) return parsed;
|
|
132
144
|
throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
|
|
133
145
|
};
|
|
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
146
|
const documentRecord = (value) => {
|
|
150
|
-
if (!
|
|
151
|
-
const checkpointSeq = nonNegativeInteger
|
|
152
|
-
const headSeq = nonNegativeInteger
|
|
147
|
+
if (!require_ownership.isRecord(value)) throw new LocalSyncConsistencyError("Document record is malformed in IndexedDB.");
|
|
148
|
+
const checkpointSeq = nonNegativeInteger(value.checkpointSeq, "document.checkpointSeq");
|
|
149
|
+
const headSeq = nonNegativeInteger(value.headSeq, "document.headSeq");
|
|
153
150
|
if (checkpointSeq > headSeq) throw new LocalSyncConsistencyError("Document checkpoint exceeds its head sequence.");
|
|
154
151
|
return Object.freeze({
|
|
155
152
|
documentId: string(value.documentId, "document.documentId"),
|
|
@@ -160,82 +157,12 @@ const documentRecord = (value) => {
|
|
|
160
157
|
});
|
|
161
158
|
};
|
|
162
159
|
const commitRecord = (value) => {
|
|
163
|
-
if (!
|
|
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.");
|
|
160
|
+
if (!require_ownership.isRecord(value)) throw new LocalSyncConsistencyError("Commit record is malformed in IndexedDB.");
|
|
168
161
|
return Object.freeze({
|
|
169
162
|
documentId: string(value.documentId, "commit.documentId"),
|
|
170
163
|
seq: positiveInteger$1(value.seq, "commit.seq"),
|
|
171
|
-
commandId: string(value.commandId, "commit.commandId"),
|
|
172
|
-
actorId: string(value.actorId, "commit.actorId"),
|
|
173
|
-
kind,
|
|
174
164
|
operations: jsonArray(value.operations, "commit.operations"),
|
|
175
|
-
|
|
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
|
-
};
|
|
165
|
+
createdAt: nonNegativeInteger(value.createdAt, "commit.createdAt")
|
|
239
166
|
});
|
|
240
167
|
};
|
|
241
168
|
const openIndexedDbTimeline = async (databaseName) => {
|
|
@@ -247,7 +174,7 @@ const openIndexedDbTimeline = async (databaseName) => {
|
|
|
247
174
|
const db = open.result;
|
|
248
175
|
if (!db.objectStoreNames.contains(DOCUMENTS)) db.createObjectStore(DOCUMENTS, { keyPath: "documentId" });
|
|
249
176
|
if (!db.objectStoreNames.contains(COMMITS)) db.createObjectStore(COMMITS, { keyPath: ["documentId", "seq"] });
|
|
250
|
-
if (
|
|
177
|
+
if (db.objectStoreNames.contains(ACTORS)) db.deleteObjectStore(ACTORS);
|
|
251
178
|
};
|
|
252
179
|
open.onsuccess = () => resolve(open.result);
|
|
253
180
|
});
|
|
@@ -291,60 +218,25 @@ const openIndexedDbTimeline = async (databaseName) => {
|
|
|
291
218
|
await complete(transaction);
|
|
292
219
|
return Object.freeze(values.map(commitRecord).sort((left, right) => left.seq - right.seq));
|
|
293
220
|
},
|
|
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
221
|
append: async (input) => {
|
|
301
|
-
const transaction = database.transaction([
|
|
302
|
-
DOCUMENTS,
|
|
303
|
-
COMMITS,
|
|
304
|
-
ACTORS
|
|
305
|
-
], "readwrite");
|
|
222
|
+
const transaction = database.transaction([DOCUMENTS, COMMITS], "readwrite");
|
|
306
223
|
const documents = transaction.objectStore(DOCUMENTS);
|
|
307
|
-
const commits = transaction.objectStore(COMMITS);
|
|
308
|
-
const actors = transaction.objectStore(ACTORS);
|
|
309
224
|
const current = await readDocument(transaction, input.documentId);
|
|
310
|
-
if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the
|
|
311
|
-
const actor = actorRecord(await request(actors.get([input.documentId, input.actorId])), input.documentId, input.actorId);
|
|
312
|
-
const seq = current.headSeq + 1;
|
|
225
|
+
if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the leader could append its command.");
|
|
313
226
|
const commit = {
|
|
314
227
|
documentId: input.documentId,
|
|
315
|
-
seq,
|
|
316
|
-
commandId: input.commandId,
|
|
317
|
-
actorId: input.actorId,
|
|
318
|
-
kind: input.kind,
|
|
228
|
+
seq: current.headSeq + 1,
|
|
319
229
|
operations: input.operations,
|
|
320
|
-
inverse: input.inverse,
|
|
321
|
-
footprint: input.footprint,
|
|
322
230
|
createdAt: Date.now()
|
|
323
231
|
};
|
|
324
|
-
|
|
232
|
+
documents.put({
|
|
325
233
|
...current,
|
|
326
|
-
headSeq: seq
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
documents.put(nextDocument);
|
|
330
|
-
actors.put(nextHistory(actor, input.history));
|
|
234
|
+
headSeq: commit.seq
|
|
235
|
+
});
|
|
236
|
+
transaction.objectStore(COMMITS).put(commit);
|
|
331
237
|
await complete(transaction);
|
|
332
238
|
return Object.freeze(commit);
|
|
333
239
|
},
|
|
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
240
|
close: () => database.close()
|
|
349
241
|
};
|
|
350
242
|
};
|
|
@@ -364,66 +256,40 @@ const requiredString = (value, label) => {
|
|
|
364
256
|
if (value.length > 0) return value;
|
|
365
257
|
throw new TypeError(`${label} must not be empty.`);
|
|
366
258
|
};
|
|
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
259
|
const positiveInteger = (value, label) => {
|
|
372
260
|
if (Number.isSafeInteger(value) && value > 0) return value;
|
|
373
261
|
throw new TypeError(`${label} must be a positive safe integer.`);
|
|
374
262
|
};
|
|
375
|
-
const identifier = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
376
263
|
const notification = (value) => {
|
|
377
264
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
378
265
|
const record = value;
|
|
379
|
-
if (record.kind !== "commit" || typeof record.documentId !== "string" || typeof record.
|
|
266
|
+
if (record.kind !== "commit" || typeof record.documentId !== "string" || typeof record.headSeq !== "number" || !Number.isSafeInteger(record.headSeq) || record.headSeq < 0) return void 0;
|
|
380
267
|
return record;
|
|
381
268
|
};
|
|
382
|
-
const
|
|
383
|
-
|
|
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) => {
|
|
269
|
+
const attachLocalSync = async (input) => {
|
|
270
|
+
const runtime = input.runtime;
|
|
397
271
|
const databaseName = requiredString(input.database, "database");
|
|
398
272
|
const documentId = requiredString(input.documentId, "documentId");
|
|
399
|
-
const actorId = requiredString(input.actorId ?? "local", "actorId");
|
|
400
273
|
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
274
|
const lockManager = locks();
|
|
406
275
|
const BroadcastChannel = channelConstructor();
|
|
407
276
|
const timeline = await openIndexedDbTimeline(databaseName);
|
|
408
277
|
let channel;
|
|
409
|
-
let
|
|
278
|
+
let unsubscribe;
|
|
279
|
+
let driver;
|
|
410
280
|
try {
|
|
411
|
-
const stored = await timeline.initialize(documentId, schemaVersion,
|
|
412
|
-
const documentRuntime = require_readable.createDocument({
|
|
413
|
-
schema: input.schema,
|
|
414
|
-
initial: stored.checkpoint,
|
|
415
|
-
history: false
|
|
416
|
-
});
|
|
417
|
-
runtime = documentRuntime;
|
|
281
|
+
const stored = await timeline.initialize(documentId, schemaVersion, json(runtime.snapshot(), "runtime snapshot"));
|
|
418
282
|
let headSeq = stored.checkpointSeq;
|
|
419
283
|
let checkpointSeq = stored.checkpointSeq;
|
|
420
|
-
let
|
|
284
|
+
let role = "follower";
|
|
421
285
|
let fault;
|
|
422
286
|
let closing = false;
|
|
423
287
|
let disposed = false;
|
|
424
|
-
|
|
288
|
+
let releaseLeadership;
|
|
289
|
+
let leadershipLease;
|
|
290
|
+
let queued = Promise.resolve();
|
|
425
291
|
const fail = (error) => {
|
|
426
|
-
if (
|
|
292
|
+
if (fault || disposed) return;
|
|
427
293
|
fault = error;
|
|
428
294
|
try {
|
|
429
295
|
input.onError?.(error);
|
|
@@ -433,198 +299,152 @@ const openLocalDocument = async (input) => {
|
|
|
433
299
|
if (closing || disposed) throw new LocalSyncDisposedError();
|
|
434
300
|
if (fault) throw fault;
|
|
435
301
|
};
|
|
302
|
+
const assertLeader = (intent) => {
|
|
303
|
+
assertOpen();
|
|
304
|
+
if (role !== "leader") throw new LocalSyncReadOnlyError();
|
|
305
|
+
if (intent.kind === "replace" || intent.kind === "apply" && intent.source === "remote") throw new LocalSyncUnsupportedOperationError();
|
|
306
|
+
};
|
|
307
|
+
const runRuntime = (run) => driver?.run(run) ?? run();
|
|
436
308
|
const applyStored = (commit) => {
|
|
437
309
|
if (commit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("Local commit log contains a sequence gap.");
|
|
438
|
-
|
|
439
|
-
source: "
|
|
310
|
+
if (runRuntime(() => runtime.apply(commit.operations, {
|
|
311
|
+
source: "remote",
|
|
440
312
|
history: false
|
|
441
|
-
})
|
|
442
|
-
if (local.status !== "committed") throw new LocalSyncConsistencyError("A durable local command was not committed.");
|
|
313
|
+
})).status !== "committed") throw new LocalSyncConsistencyError("A stored local command could not be applied.");
|
|
443
314
|
headSeq = commit.seq;
|
|
444
|
-
return local.commit;
|
|
445
315
|
};
|
|
446
|
-
const
|
|
316
|
+
const restore = async (reset = false) => {
|
|
447
317
|
const current = await timeline.read(documentId);
|
|
448
|
-
if (current.schemaVersion !== schemaVersion) throw new LocalSyncConsistencyError("Local document schema changed while this
|
|
449
|
-
if (headSeq
|
|
450
|
-
|
|
451
|
-
if (
|
|
318
|
+
if (current.schemaVersion !== schemaVersion) throw new LocalSyncConsistencyError("Local document schema changed while this attachment was open.");
|
|
319
|
+
if (headSeq > current.headSeq) throw new LocalSyncConsistencyError("The local timeline moved behind this attachment.");
|
|
320
|
+
if (reset || headSeq < current.checkpointSeq) {
|
|
321
|
+
if (runRuntime(() => runtime.replace(current.checkpoint, { source: "remote" })).status === "rejected") throw new LocalSyncConsistencyError("The local checkpoint could not be restored.");
|
|
452
322
|
headSeq = current.checkpointSeq;
|
|
453
|
-
checkpointSeq = current.checkpointSeq;
|
|
454
323
|
}
|
|
455
324
|
const tail = await timeline.tail(documentId, headSeq);
|
|
456
325
|
for (const commit of tail) applyStored(commit);
|
|
457
326
|
if (headSeq !== current.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
|
|
458
327
|
checkpointSeq = current.checkpointSeq;
|
|
459
328
|
};
|
|
460
|
-
const
|
|
329
|
+
const enqueue = (run) => {
|
|
461
330
|
const next = queued.then(run, run);
|
|
462
|
-
queued = next.then(() => void 0, () =>
|
|
331
|
+
queued = next.then(() => void 0, (error) => {
|
|
332
|
+
fail(error);
|
|
333
|
+
});
|
|
463
334
|
return next;
|
|
464
335
|
};
|
|
465
|
-
const
|
|
466
|
-
|
|
336
|
+
const persist = async (operations) => {
|
|
337
|
+
const storedCommit = await timeline.append({
|
|
338
|
+
documentId,
|
|
339
|
+
expectedHeadSeq: headSeq,
|
|
340
|
+
operations
|
|
341
|
+
});
|
|
342
|
+
if (storedCommit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("A local command was assigned an unexpected sequence.");
|
|
343
|
+
headSeq = storedCommit.seq;
|
|
344
|
+
channel?.postMessage({
|
|
345
|
+
kind: "commit",
|
|
346
|
+
documentId,
|
|
347
|
+
headSeq
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
const record = (commit) => {
|
|
467
351
|
try {
|
|
468
|
-
|
|
352
|
+
const operations = jsonArray(commit.operations, "local command operations", input.commandLimits);
|
|
353
|
+
enqueue(() => persist(operations)).catch(() => void 0);
|
|
469
354
|
} catch (error) {
|
|
470
355
|
fail(error);
|
|
471
|
-
throw error;
|
|
472
356
|
}
|
|
473
|
-
|
|
357
|
+
};
|
|
358
|
+
const holdLeadership = () => new Promise((resolve) => {
|
|
359
|
+
releaseLeadership = resolve;
|
|
474
360
|
});
|
|
475
|
-
const
|
|
361
|
+
const lead = async (activated) => {
|
|
362
|
+
if (closing || disposed || fault) return;
|
|
363
|
+
await restore();
|
|
364
|
+
if (closing || disposed || fault) return;
|
|
365
|
+
role = "leader";
|
|
366
|
+
activated?.();
|
|
476
367
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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;
|
|
368
|
+
await holdLeadership();
|
|
369
|
+
} finally {
|
|
370
|
+
releaseLeadership = void 0;
|
|
371
|
+
if (!closing && !disposed && !fault) role = "follower";
|
|
507
372
|
}
|
|
508
373
|
};
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
374
|
+
const watchLeadership = () => {
|
|
375
|
+
leadershipLease = lockManager.request(`doxum:${documentId}`, { mode: "exclusive" }, async () => {
|
|
376
|
+
try {
|
|
377
|
+
await lead();
|
|
378
|
+
} catch (error) {
|
|
379
|
+
fail(error);
|
|
380
|
+
}
|
|
381
|
+
}).catch((error) => {
|
|
382
|
+
if (!closing && !disposed) fail(error);
|
|
514
383
|
});
|
|
515
|
-
try {
|
|
516
|
-
return candidate.apply(operations, {
|
|
517
|
-
source: "local",
|
|
518
|
-
history: false
|
|
519
|
-
});
|
|
520
|
-
} finally {
|
|
521
|
-
candidate.dispose();
|
|
522
|
-
}
|
|
523
384
|
};
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
385
|
+
const claimInitialLeadership = async () => {
|
|
386
|
+
let resolve;
|
|
387
|
+
const claimed = new Promise((done) => {
|
|
388
|
+
resolve = done;
|
|
389
|
+
});
|
|
390
|
+
leadershipLease = lockManager.request(`doxum:${documentId}`, {
|
|
391
|
+
mode: "exclusive",
|
|
392
|
+
ifAvailable: true
|
|
393
|
+
}, async (lock) => {
|
|
394
|
+
if (lock === null) {
|
|
395
|
+
resolve(false);
|
|
396
|
+
return;
|
|
549
397
|
}
|
|
398
|
+
try {
|
|
399
|
+
await lead(() => resolve(true));
|
|
400
|
+
} catch (error) {
|
|
401
|
+
fail(error);
|
|
402
|
+
resolve(false);
|
|
403
|
+
}
|
|
404
|
+
}).catch((error) => {
|
|
405
|
+
if (!closing && !disposed) fail(error);
|
|
406
|
+
resolve(false);
|
|
550
407
|
});
|
|
408
|
+
if (!await claimed && !fault && !closing && !disposed) watchLeadership();
|
|
551
409
|
};
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
410
|
+
await restore(true);
|
|
411
|
+
runtime.history.clear();
|
|
412
|
+
driver = require_driver.installRuntimeWriteDriver(runtime, { assertWritable: assertLeader });
|
|
555
413
|
channel = new BroadcastChannel(`doxum:${databaseName}:${documentId}`);
|
|
414
|
+
unsubscribe = runtime.subscribe((commit) => {
|
|
415
|
+
if ((commit.source === "local" || commit.source === "system" || commit.source === "history") && !closing && !disposed) record(commit);
|
|
416
|
+
});
|
|
556
417
|
channel.onmessage = (event) => {
|
|
557
418
|
const message = notification(event.data);
|
|
558
|
-
if (!message || message.documentId !== documentId || message.
|
|
559
|
-
|
|
419
|
+
if (!message || message.documentId !== documentId || message.headSeq <= headSeq || closing || disposed || fault) return;
|
|
420
|
+
enqueue(restore).catch(() => void 0);
|
|
560
421
|
};
|
|
561
|
-
|
|
562
|
-
|
|
422
|
+
await claimInitialLeadership();
|
|
423
|
+
if (fault) throw fault;
|
|
424
|
+
return Object.freeze({
|
|
563
425
|
state: () => {
|
|
564
426
|
if (disposed) return { status: "disposed" };
|
|
565
427
|
if (fault) return {
|
|
566
|
-
status: "
|
|
428
|
+
status: "error",
|
|
429
|
+
headSeq,
|
|
430
|
+
checkpointSeq,
|
|
567
431
|
error: fault
|
|
568
432
|
};
|
|
569
433
|
return {
|
|
570
|
-
status:
|
|
434
|
+
status: role,
|
|
571
435
|
headSeq,
|
|
572
436
|
checkpointSeq
|
|
573
437
|
};
|
|
574
438
|
},
|
|
575
|
-
|
|
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: () => {
|
|
439
|
+
flush: async () => {
|
|
622
440
|
assertOpen();
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
441
|
+
try {
|
|
442
|
+
await enqueue(restore);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
fail(error);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
626
447
|
assertOpen();
|
|
627
|
-
return execute(() => withWriter(async () => applyHistory(await timeline.history(documentId, actorId), "redo")));
|
|
628
448
|
},
|
|
629
449
|
dispose: async () => {
|
|
630
450
|
if (disposed) return;
|
|
@@ -633,17 +453,20 @@ const openLocalDocument = async (input) => {
|
|
|
633
453
|
return;
|
|
634
454
|
}
|
|
635
455
|
closing = true;
|
|
456
|
+
unsubscribe?.();
|
|
636
457
|
await queued;
|
|
458
|
+
releaseLeadership?.();
|
|
459
|
+
if (role === "leader") await leadershipLease;
|
|
637
460
|
disposed = true;
|
|
461
|
+
driver?.dispose();
|
|
638
462
|
channel?.close();
|
|
639
463
|
timeline.close();
|
|
640
|
-
documentRuntime.dispose();
|
|
641
464
|
}
|
|
642
|
-
};
|
|
643
|
-
return Object.freeze(localDocument);
|
|
465
|
+
});
|
|
644
466
|
} catch (error) {
|
|
467
|
+
unsubscribe?.();
|
|
468
|
+
driver?.dispose();
|
|
645
469
|
channel?.close();
|
|
646
|
-
runtime?.dispose();
|
|
647
470
|
timeline.close();
|
|
648
471
|
throw error;
|
|
649
472
|
}
|
|
@@ -652,9 +475,11 @@ const openLocalDocument = async (input) => {
|
|
|
652
475
|
exports.LocalSyncConsistencyError = LocalSyncConsistencyError;
|
|
653
476
|
exports.LocalSyncDataError = LocalSyncDataError;
|
|
654
477
|
exports.LocalSyncDisposedError = LocalSyncDisposedError;
|
|
478
|
+
exports.LocalSyncReadOnlyError = LocalSyncReadOnlyError;
|
|
655
479
|
exports.LocalSyncSchemaError = LocalSyncSchemaError;
|
|
656
480
|
exports.LocalSyncUnavailableError = LocalSyncUnavailableError;
|
|
481
|
+
exports.LocalSyncUnsupportedOperationError = LocalSyncUnsupportedOperationError;
|
|
482
|
+
exports.attachLocalSync = attachLocalSync;
|
|
657
483
|
exports.defaultJsonCommandLimits = defaultJsonCommandLimits;
|
|
658
|
-
exports.openLocalDocument = openLocalDocument;
|
|
659
484
|
|
|
660
485
|
//# sourceMappingURL=local-sync.cjs.map
|