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.
- 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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
151
|
-
const headSeq = nonNegativeInteger
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
231
|
+
documents.put({
|
|
324
232
|
...current,
|
|
325
|
-
headSeq: seq
|
|
326
|
-
};
|
|
327
|
-
|
|
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.
|
|
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
|
|
382
|
-
|
|
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
|
|
277
|
+
let unsubscribe;
|
|
278
|
+
let driver;
|
|
409
279
|
try {
|
|
410
|
-
const stored = await timeline.initialize(documentId, schemaVersion,
|
|
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
|
|
283
|
+
let role = "follower";
|
|
420
284
|
let fault;
|
|
421
285
|
let closing = false;
|
|
422
286
|
let disposed = false;
|
|
423
|
-
|
|
287
|
+
let releaseLeadership;
|
|
288
|
+
let leadershipLease;
|
|
289
|
+
let queued = Promise.resolve();
|
|
424
290
|
const fail = (error) => {
|
|
425
|
-
if (
|
|
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
|
-
|
|
438
|
-
source: "
|
|
309
|
+
if (runRuntime(() => runtime.apply(commit.operations, {
|
|
310
|
+
source: "remote",
|
|
439
311
|
history: false
|
|
440
|
-
})
|
|
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
|
|
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
|
|
448
|
-
if (headSeq
|
|
449
|
-
|
|
450
|
-
if (
|
|
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
|
|
328
|
+
const enqueue = (run) => {
|
|
460
329
|
const next = queued.then(run, run);
|
|
461
|
-
queued = next.then(() => void 0, () =>
|
|
330
|
+
queued = next.then(() => void 0, (error) => {
|
|
331
|
+
fail(error);
|
|
332
|
+
});
|
|
462
333
|
return next;
|
|
463
334
|
};
|
|
464
|
-
const
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
+
};
|
|
357
|
+
const holdLeadership = () => new Promise((resolve) => {
|
|
358
|
+
releaseLeadership = resolve;
|
|
473
359
|
});
|
|
474
|
-
const
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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.
|
|
558
|
-
|
|
418
|
+
if (!message || message.documentId !== documentId || message.headSeq <= headSeq || closing || disposed || fault) return;
|
|
419
|
+
enqueue(restore).catch(() => void 0);
|
|
559
420
|
};
|
|
560
|
-
|
|
561
|
-
|
|
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: "
|
|
427
|
+
status: "error",
|
|
428
|
+
headSeq,
|
|
429
|
+
checkpointSeq,
|
|
566
430
|
error: fault
|
|
567
431
|
};
|
|
568
432
|
return {
|
|
569
|
-
status:
|
|
433
|
+
status: role,
|
|
570
434
|
headSeq,
|
|
571
435
|
checkpointSeq
|
|
572
436
|
};
|
|
573
437
|
},
|
|
574
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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,
|
|
474
|
+
export { LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncReadOnlyError, LocalSyncSchemaError, LocalSyncUnavailableError, LocalSyncUnsupportedOperationError, attachLocalSync, defaultJsonCommandLimits };
|
|
652
475
|
|
|
653
476
|
//# sourceMappingURL=local-sync.js.map
|