doxum 0.1.1 → 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 (44) hide show
  1. package/README.md +77 -2
  2. package/dist/{contract-DwNiKioc.d.ts → contract-BsY1XLiG.d.ts} +64 -5
  3. package/dist/{contract-Otb5W6cQ.d.cts → contract-D8kjdfqT.d.cts} +64 -5
  4. package/dist/{dependency-BdEMyquf.js → dependency-C5_R1tvQ.js} +24 -263
  5. package/dist/dependency-C5_R1tvQ.js.map +1 -0
  6. package/dist/{dependency-DLcCvNKq.cjs → dependency-Dfzs3khk.cjs} +48 -341
  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 +428 -133
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +8 -5
  15. package/dist/index.d.ts +8 -5
  16. package/dist/index.js +352 -61
  17. package/dist/index.js.map +1 -1
  18. package/dist/integration.cjs +1 -1
  19. package/dist/integration.cjs.map +1 -1
  20. package/dist/integration.d.cts +2 -2
  21. package/dist/integration.d.ts +2 -2
  22. package/dist/integration.js +1 -1
  23. package/dist/integration.js.map +1 -1
  24. package/dist/local-sync.cjs +485 -0
  25. package/dist/local-sync.cjs.map +1 -0
  26. package/dist/local-sync.d.cts +74 -0
  27. package/dist/local-sync.d.ts +74 -0
  28. package/dist/local-sync.js +476 -0
  29. package/dist/local-sync.js.map +1 -0
  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/dist/react.cjs.map +1 -1
  35. package/dist/react.d.cts +2 -2
  36. package/dist/react.d.ts +2 -2
  37. package/dist/react.js.map +1 -1
  38. package/package.json +14 -8
  39. package/skills/doxum-runtime/references/guide.en.md +60 -5
  40. package/skills/doxum-runtime/references/guide.zh-CN.md +48 -2
  41. package/skills/doxum-runtime/references/invariants.en.md +23 -8
  42. package/skills/doxum-runtime/references/invariants.zh-CN.md +2 -2
  43. package/dist/dependency-BdEMyquf.js.map +0 -1
  44. package/dist/dependency-DLcCvNKq.cjs.map +0 -1
@@ -0,0 +1,74 @@
1
+ import { c as DocumentRuntime, xt as DocumentSchema } from "./contract-D8kjdfqT.cjs";
2
+
3
+ //#region core/src/local-sync/json.d.ts
4
+ type JsonPrimitive = null | boolean | number | string;
5
+ type JsonValue = JsonPrimitive | readonly JsonValue[] | {
6
+ readonly [key: string]: JsonValue;
7
+ };
8
+ type JsonCommandLimits = {
9
+ readonly maxOperations?: number;
10
+ readonly maxBytes?: number;
11
+ readonly maxDepth?: number;
12
+ readonly maxStringLength?: number;
13
+ };
14
+ type ResolvedJsonLimits = {
15
+ readonly maxOperations: number;
16
+ readonly maxBytes: number;
17
+ readonly maxDepth: number;
18
+ readonly maxStringLength: number;
19
+ };
20
+ declare const defaultJsonCommandLimits: Readonly<ResolvedJsonLimits>;
21
+ declare class LocalSyncDataError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ //#endregion
25
+ //#region core/src/local-sync/contract.d.ts
26
+ declare class LocalSyncUnavailableError extends Error {
27
+ constructor(capability: 'IndexedDB' | 'Web Locks' | 'BroadcastChannel');
28
+ }
29
+ declare class LocalSyncSchemaError extends Error {
30
+ constructor(documentId: string, expected: number, actual: number);
31
+ }
32
+ declare class LocalSyncConsistencyError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ declare class LocalSyncReadOnlyError extends Error {
36
+ constructor();
37
+ }
38
+ declare class LocalSyncUnsupportedOperationError extends Error {
39
+ constructor();
40
+ }
41
+ declare class LocalSyncDisposedError extends Error {
42
+ constructor();
43
+ }
44
+ type LocalSyncState = {
45
+ readonly status: 'leader' | 'follower';
46
+ readonly headSeq: number;
47
+ readonly checkpointSeq: number;
48
+ } | {
49
+ readonly status: 'error';
50
+ readonly headSeq: number;
51
+ readonly checkpointSeq: number;
52
+ readonly error: unknown;
53
+ } | {
54
+ readonly status: 'disposed';
55
+ };
56
+ type LocalSync = {
57
+ state(): LocalSyncState;
58
+ flush(): Promise<void>;
59
+ dispose(): Promise<void>;
60
+ };
61
+ type AttachLocalSyncOptions<TSchema extends DocumentSchema> = {
62
+ readonly runtime: DocumentRuntime<TSchema>;
63
+ readonly database: string;
64
+ readonly documentId: string;
65
+ readonly schemaVersion?: number;
66
+ readonly commandLimits?: JsonCommandLimits;
67
+ readonly onError?: (error: unknown) => void;
68
+ };
69
+ //#endregion
70
+ //#region core/src/local-sync/session.d.ts
71
+ declare const attachLocalSync: <TSchema extends DocumentSchema>(input: AttachLocalSyncOptions<TSchema>) => Promise<LocalSync>;
72
+ //#endregion
73
+ export { type AttachLocalSyncOptions, type JsonCommandLimits, type JsonPrimitive, type JsonValue, type LocalSync, LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncReadOnlyError, LocalSyncSchemaError, type LocalSyncState, LocalSyncUnavailableError, LocalSyncUnsupportedOperationError, attachLocalSync, defaultJsonCommandLimits };
74
+ //# sourceMappingURL=local-sync.d.cts.map
@@ -0,0 +1,74 @@
1
+ import { c as DocumentRuntime, xt as DocumentSchema } from "./contract-BsY1XLiG.js";
2
+
3
+ //#region core/src/local-sync/json.d.ts
4
+ type JsonPrimitive = null | boolean | number | string;
5
+ type JsonValue = JsonPrimitive | readonly JsonValue[] | {
6
+ readonly [key: string]: JsonValue;
7
+ };
8
+ type JsonCommandLimits = {
9
+ readonly maxOperations?: number;
10
+ readonly maxBytes?: number;
11
+ readonly maxDepth?: number;
12
+ readonly maxStringLength?: number;
13
+ };
14
+ type ResolvedJsonLimits = {
15
+ readonly maxOperations: number;
16
+ readonly maxBytes: number;
17
+ readonly maxDepth: number;
18
+ readonly maxStringLength: number;
19
+ };
20
+ declare const defaultJsonCommandLimits: Readonly<ResolvedJsonLimits>;
21
+ declare class LocalSyncDataError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ //#endregion
25
+ //#region core/src/local-sync/contract.d.ts
26
+ declare class LocalSyncUnavailableError extends Error {
27
+ constructor(capability: 'IndexedDB' | 'Web Locks' | 'BroadcastChannel');
28
+ }
29
+ declare class LocalSyncSchemaError extends Error {
30
+ constructor(documentId: string, expected: number, actual: number);
31
+ }
32
+ declare class LocalSyncConsistencyError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ declare class LocalSyncReadOnlyError extends Error {
36
+ constructor();
37
+ }
38
+ declare class LocalSyncUnsupportedOperationError extends Error {
39
+ constructor();
40
+ }
41
+ declare class LocalSyncDisposedError extends Error {
42
+ constructor();
43
+ }
44
+ type LocalSyncState = {
45
+ readonly status: 'leader' | 'follower';
46
+ readonly headSeq: number;
47
+ readonly checkpointSeq: number;
48
+ } | {
49
+ readonly status: 'error';
50
+ readonly headSeq: number;
51
+ readonly checkpointSeq: number;
52
+ readonly error: unknown;
53
+ } | {
54
+ readonly status: 'disposed';
55
+ };
56
+ type LocalSync = {
57
+ state(): LocalSyncState;
58
+ flush(): Promise<void>;
59
+ dispose(): Promise<void>;
60
+ };
61
+ type AttachLocalSyncOptions<TSchema extends DocumentSchema> = {
62
+ readonly runtime: DocumentRuntime<TSchema>;
63
+ readonly database: string;
64
+ readonly documentId: string;
65
+ readonly schemaVersion?: number;
66
+ readonly commandLimits?: JsonCommandLimits;
67
+ readonly onError?: (error: unknown) => void;
68
+ };
69
+ //#endregion
70
+ //#region core/src/local-sync/session.d.ts
71
+ declare const attachLocalSync: <TSchema extends DocumentSchema>(input: AttachLocalSyncOptions<TSchema>) => Promise<LocalSync>;
72
+ //#endregion
73
+ export { type AttachLocalSyncOptions, type JsonCommandLimits, type JsonPrimitive, type JsonValue, type LocalSync, LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncReadOnlyError, LocalSyncSchemaError, type LocalSyncState, LocalSyncUnavailableError, LocalSyncUnsupportedOperationError, attachLocalSync, defaultJsonCommandLimits };
74
+ //# sourceMappingURL=local-sync.d.ts.map
@@ -0,0 +1,476 @@
1
+ import { i as isRecord, r as isPlainObject } from "./ownership-B-VAPcce.js";
2
+ import { i as installRuntimeWriteDriver } from "./driver-CbzfW5MR.js";
3
+ //#region core/src/local-sync/contract.ts
4
+ var LocalSyncUnavailableError = class extends Error {
5
+ constructor(capability) {
6
+ super(`${capability} is required by doxum/local-sync in this environment.`);
7
+ this.name = "LocalSyncUnavailableError";
8
+ }
9
+ };
10
+ var LocalSyncSchemaError = class extends Error {
11
+ constructor(documentId, expected, actual) {
12
+ super(`Local document '${documentId}' uses schema version ${actual}, but version ${expected} was requested.`);
13
+ this.name = "LocalSyncSchemaError";
14
+ }
15
+ };
16
+ var LocalSyncConsistencyError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "LocalSyncConsistencyError";
20
+ }
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
+ };
34
+ var LocalSyncDisposedError = class extends Error {
35
+ constructor() {
36
+ super("Local sync has been disposed.");
37
+ this.name = "LocalSyncDisposedError";
38
+ }
39
+ };
40
+ //#endregion
41
+ //#region core/src/local-sync/json.ts
42
+ const defaultJsonCommandLimits = Object.freeze({
43
+ maxOperations: 1e3,
44
+ maxBytes: 1e6,
45
+ maxDepth: 64,
46
+ maxStringLength: 256e3
47
+ });
48
+ var LocalSyncDataError = class extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "LocalSyncDataError";
52
+ }
53
+ };
54
+ const describePath = (path) => path.length === 0 ? "value" : path.join(".");
55
+ const limit = (value, fallback, label) => {
56
+ if (value === void 0) return fallback;
57
+ if (Number.isSafeInteger(value) && value > 0) return value;
58
+ throw new TypeError(`${label} must be a positive safe integer.`);
59
+ };
60
+ const resolveLimits = (input) => ({
61
+ maxOperations: limit(input?.maxOperations, defaultJsonCommandLimits.maxOperations, "maxOperations"),
62
+ maxBytes: limit(input?.maxBytes, defaultJsonCommandLimits.maxBytes, "maxBytes"),
63
+ maxDepth: limit(input?.maxDepth, defaultJsonCommandLimits.maxDepth, "maxDepth"),
64
+ maxStringLength: limit(input?.maxStringLength, defaultJsonCommandLimits.maxStringLength, "maxStringLength")
65
+ });
66
+ const validate = (value, path, limits, depth, ancestors) => {
67
+ if (limits && depth > limits.maxDepth) throw new LocalSyncDataError(`${describePath(path)} exceeds the maximum JSON depth.`);
68
+ if (value === null || typeof value === "boolean") return value;
69
+ if (typeof value === "string") {
70
+ if (!limits || value.length <= limits.maxStringLength) return value;
71
+ throw new LocalSyncDataError(`${describePath(path)} exceeds the maximum string length.`);
72
+ }
73
+ if (typeof value === "number") {
74
+ if (Number.isFinite(value)) return value;
75
+ throw new LocalSyncDataError(`${describePath(path)} contains a non-finite number.`);
76
+ }
77
+ if (Array.isArray(value)) {
78
+ if (ancestors.has(value)) throw new LocalSyncDataError(`${describePath(path)} contains a cycle.`);
79
+ ancestors.add(value);
80
+ try {
81
+ for (let index = 0; index < value.length; index += 1) validate(value[index], [...path, String(index)], limits, depth + 1, ancestors);
82
+ } finally {
83
+ ancestors.delete(value);
84
+ }
85
+ return value;
86
+ }
87
+ if (isPlainObject(value)) {
88
+ if (ancestors.has(value)) throw new LocalSyncDataError(`${describePath(path)} contains a cycle.`);
89
+ ancestors.add(value);
90
+ try {
91
+ for (const [key, entry] of Object.entries(value)) validate(entry, [...path, key], limits, depth + 1, ancestors);
92
+ } finally {
93
+ ancestors.delete(value);
94
+ }
95
+ return value;
96
+ }
97
+ throw new LocalSyncDataError(`${describePath(path)} must be JSON data.`);
98
+ };
99
+ const json = (value, label) => validate(value, [label], void 0, 0, /* @__PURE__ */ new WeakSet());
100
+ const jsonArray = (value, label, input) => {
101
+ const limits = resolveLimits(input);
102
+ const parsed = validate(value, [label], limits, 0, /* @__PURE__ */ new WeakSet());
103
+ if (!Array.isArray(parsed)) throw new LocalSyncDataError(`${label} must be a JSON array.`);
104
+ if (parsed.length > limits.maxOperations) throw new LocalSyncDataError(`${label} exceeds the maximum operation count.`);
105
+ const serialized = JSON.stringify(parsed);
106
+ if (new TextEncoder().encode(serialized).byteLength > limits.maxBytes) throw new LocalSyncDataError(`${label} exceeds the maximum command size.`);
107
+ return parsed;
108
+ };
109
+ //#endregion
110
+ //#region core/src/local-sync/timeline.ts
111
+ const DATABASE_VERSION = 2;
112
+ const DOCUMENTS = "documents";
113
+ const COMMITS = "commits";
114
+ const ACTORS = "actors";
115
+ const request = (value) => new Promise((resolve, reject) => {
116
+ value.onsuccess = () => resolve(value.result);
117
+ value.onerror = () => reject(value.error ?? /* @__PURE__ */ new Error("IndexedDB request failed."));
118
+ });
119
+ const complete = (transaction) => new Promise((resolve, reject) => {
120
+ transaction.oncomplete = () => resolve();
121
+ transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction was aborted."));
122
+ transaction.onerror = () => reject(transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
123
+ });
124
+ const factory = () => {
125
+ if (!globalThis.indexedDB) throw new LocalSyncUnavailableError("IndexedDB");
126
+ return globalThis.indexedDB;
127
+ };
128
+ const keyRange = () => {
129
+ if (!globalThis.IDBKeyRange) throw new LocalSyncUnavailableError("IndexedDB");
130
+ return globalThis.IDBKeyRange;
131
+ };
132
+ const string = (value, label) => {
133
+ if (typeof value === "string") return value;
134
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
135
+ };
136
+ const nonNegativeInteger = (value, label) => {
137
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
138
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
139
+ };
140
+ const positiveInteger$1 = (value, label) => {
141
+ const parsed = nonNegativeInteger(value, label);
142
+ if (parsed > 0) return parsed;
143
+ throw new LocalSyncConsistencyError(`${label} is malformed in IndexedDB.`);
144
+ };
145
+ const documentRecord = (value) => {
146
+ if (!isRecord(value)) throw new LocalSyncConsistencyError("Document record is malformed in IndexedDB.");
147
+ const checkpointSeq = nonNegativeInteger(value.checkpointSeq, "document.checkpointSeq");
148
+ const headSeq = nonNegativeInteger(value.headSeq, "document.headSeq");
149
+ if (checkpointSeq > headSeq) throw new LocalSyncConsistencyError("Document checkpoint exceeds its head sequence.");
150
+ return Object.freeze({
151
+ documentId: string(value.documentId, "document.documentId"),
152
+ schemaVersion: positiveInteger$1(value.schemaVersion, "document.schemaVersion"),
153
+ checkpointSeq,
154
+ headSeq,
155
+ checkpoint: json(value.checkpoint, "document.checkpoint")
156
+ });
157
+ };
158
+ const commitRecord = (value) => {
159
+ if (!isRecord(value)) throw new LocalSyncConsistencyError("Commit record is malformed in IndexedDB.");
160
+ return Object.freeze({
161
+ documentId: string(value.documentId, "commit.documentId"),
162
+ seq: positiveInteger$1(value.seq, "commit.seq"),
163
+ operations: jsonArray(value.operations, "commit.operations"),
164
+ createdAt: nonNegativeInteger(value.createdAt, "commit.createdAt")
165
+ });
166
+ };
167
+ const openIndexedDbTimeline = async (databaseName) => {
168
+ const database = await new Promise((resolve, reject) => {
169
+ const open = factory().open(databaseName, DATABASE_VERSION);
170
+ open.onerror = () => reject(open.error ?? /* @__PURE__ */ new Error("Unable to open IndexedDB."));
171
+ open.onblocked = () => reject(/* @__PURE__ */ new Error(`IndexedDB database '${databaseName}' is blocked.`));
172
+ open.onupgradeneeded = () => {
173
+ const db = open.result;
174
+ if (!db.objectStoreNames.contains(DOCUMENTS)) db.createObjectStore(DOCUMENTS, { keyPath: "documentId" });
175
+ if (!db.objectStoreNames.contains(COMMITS)) db.createObjectStore(COMMITS, { keyPath: ["documentId", "seq"] });
176
+ if (db.objectStoreNames.contains(ACTORS)) db.deleteObjectStore(ACTORS);
177
+ };
178
+ open.onsuccess = () => resolve(open.result);
179
+ });
180
+ const readDocument = async (transaction, documentId) => {
181
+ const value = await request(transaction.objectStore(DOCUMENTS).get(documentId));
182
+ if (value === void 0) throw new LocalSyncConsistencyError(`Local document '${documentId}' does not exist.`);
183
+ return documentRecord(value);
184
+ };
185
+ return {
186
+ initialize: async (documentId, schemaVersion, checkpoint) => {
187
+ const transaction = database.transaction(DOCUMENTS, "readwrite");
188
+ const store = transaction.objectStore(DOCUMENTS);
189
+ const existing = await request(store.get(documentId));
190
+ if (existing !== void 0) {
191
+ const record = documentRecord(existing);
192
+ if (record.schemaVersion !== schemaVersion) throw new LocalSyncSchemaError(documentId, schemaVersion, record.schemaVersion);
193
+ await complete(transaction);
194
+ return record;
195
+ }
196
+ const record = {
197
+ documentId,
198
+ schemaVersion,
199
+ checkpointSeq: 0,
200
+ headSeq: 0,
201
+ checkpoint
202
+ };
203
+ store.put(record);
204
+ await complete(transaction);
205
+ return Object.freeze(record);
206
+ },
207
+ read: async (documentId) => {
208
+ const transaction = database.transaction(DOCUMENTS, "readonly");
209
+ const record = await readDocument(transaction, documentId);
210
+ await complete(transaction);
211
+ return record;
212
+ },
213
+ tail: async (documentId, afterSeq) => {
214
+ const transaction = database.transaction(COMMITS, "readonly");
215
+ const range = keyRange().bound([documentId, afterSeq + 1], [documentId, Number.MAX_SAFE_INTEGER]);
216
+ const values = await request(transaction.objectStore(COMMITS).getAll(range));
217
+ await complete(transaction);
218
+ return Object.freeze(values.map(commitRecord).sort((left, right) => left.seq - right.seq));
219
+ },
220
+ append: async (input) => {
221
+ const transaction = database.transaction([DOCUMENTS, COMMITS], "readwrite");
222
+ const documents = transaction.objectStore(DOCUMENTS);
223
+ const current = await readDocument(transaction, input.documentId);
224
+ if (current.headSeq !== input.expectedHeadSeq) throw new LocalSyncConsistencyError("Local document advanced before the leader could append its command.");
225
+ const commit = {
226
+ documentId: input.documentId,
227
+ seq: current.headSeq + 1,
228
+ operations: input.operations,
229
+ createdAt: Date.now()
230
+ };
231
+ documents.put({
232
+ ...current,
233
+ headSeq: commit.seq
234
+ });
235
+ transaction.objectStore(COMMITS).put(commit);
236
+ await complete(transaction);
237
+ return Object.freeze(commit);
238
+ },
239
+ close: () => database.close()
240
+ };
241
+ };
242
+ //#endregion
243
+ //#region core/src/local-sync/session.ts
244
+ const locks = () => {
245
+ const value = globalThis.navigator?.locks;
246
+ if (typeof value !== "object" || value === null || !("request" in value) || typeof value.request !== "function") throw new LocalSyncUnavailableError("Web Locks");
247
+ return value;
248
+ };
249
+ const channelConstructor = () => {
250
+ const value = globalThis.BroadcastChannel;
251
+ if (typeof value !== "function") throw new LocalSyncUnavailableError("BroadcastChannel");
252
+ return value;
253
+ };
254
+ const requiredString = (value, label) => {
255
+ if (value.length > 0) return value;
256
+ throw new TypeError(`${label} must not be empty.`);
257
+ };
258
+ const positiveInteger = (value, label) => {
259
+ if (Number.isSafeInteger(value) && value > 0) return value;
260
+ throw new TypeError(`${label} must be a positive safe integer.`);
261
+ };
262
+ const notification = (value) => {
263
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
264
+ const record = value;
265
+ if (record.kind !== "commit" || typeof record.documentId !== "string" || typeof record.headSeq !== "number" || !Number.isSafeInteger(record.headSeq) || record.headSeq < 0) return void 0;
266
+ return record;
267
+ };
268
+ const attachLocalSync = async (input) => {
269
+ const runtime = input.runtime;
270
+ const databaseName = requiredString(input.database, "database");
271
+ const documentId = requiredString(input.documentId, "documentId");
272
+ const schemaVersion = positiveInteger(input.schemaVersion ?? 1, "schemaVersion");
273
+ const lockManager = locks();
274
+ const BroadcastChannel = channelConstructor();
275
+ const timeline = await openIndexedDbTimeline(databaseName);
276
+ let channel;
277
+ let unsubscribe;
278
+ let driver;
279
+ try {
280
+ const stored = await timeline.initialize(documentId, schemaVersion, json(runtime.snapshot(), "runtime snapshot"));
281
+ let headSeq = stored.checkpointSeq;
282
+ let checkpointSeq = stored.checkpointSeq;
283
+ let role = "follower";
284
+ let fault;
285
+ let closing = false;
286
+ let disposed = false;
287
+ let releaseLeadership;
288
+ let leadershipLease;
289
+ let queued = Promise.resolve();
290
+ const fail = (error) => {
291
+ if (fault || disposed) return;
292
+ fault = error;
293
+ try {
294
+ input.onError?.(error);
295
+ } catch {}
296
+ };
297
+ const assertOpen = () => {
298
+ if (closing || disposed) throw new LocalSyncDisposedError();
299
+ if (fault) throw fault;
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();
307
+ const applyStored = (commit) => {
308
+ if (commit.seq !== headSeq + 1) throw new LocalSyncConsistencyError("Local commit log contains a sequence gap.");
309
+ if (runRuntime(() => runtime.apply(commit.operations, {
310
+ source: "remote",
311
+ history: false
312
+ })).status !== "committed") throw new LocalSyncConsistencyError("A stored local command could not be applied.");
313
+ headSeq = commit.seq;
314
+ };
315
+ const restore = async (reset = false) => {
316
+ const current = await timeline.read(documentId);
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.");
321
+ headSeq = current.checkpointSeq;
322
+ }
323
+ const tail = await timeline.tail(documentId, headSeq);
324
+ for (const commit of tail) applyStored(commit);
325
+ if (headSeq !== current.headSeq) throw new LocalSyncConsistencyError("Local commit log does not reach its recorded head sequence.");
326
+ checkpointSeq = current.checkpointSeq;
327
+ };
328
+ const enqueue = (run) => {
329
+ const next = queued.then(run, run);
330
+ queued = next.then(() => void 0, (error) => {
331
+ fail(error);
332
+ });
333
+ return next;
334
+ };
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) => {
350
+ try {
351
+ const operations = jsonArray(commit.operations, "local command operations", input.commandLimits);
352
+ enqueue(() => persist(operations)).catch(() => void 0);
353
+ } catch (error) {
354
+ fail(error);
355
+ }
356
+ };
357
+ const holdLeadership = () => new Promise((resolve) => {
358
+ releaseLeadership = resolve;
359
+ });
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?.();
366
+ try {
367
+ await holdLeadership();
368
+ } finally {
369
+ releaseLeadership = void 0;
370
+ if (!closing && !disposed && !fault) role = "follower";
371
+ }
372
+ };
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);
382
+ });
383
+ };
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;
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);
406
+ });
407
+ if (!await claimed && !fault && !closing && !disposed) watchLeadership();
408
+ };
409
+ await restore(true);
410
+ runtime.history.clear();
411
+ driver = installRuntimeWriteDriver(runtime, { assertWritable: assertLeader });
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
+ });
416
+ channel.onmessage = (event) => {
417
+ const message = notification(event.data);
418
+ if (!message || message.documentId !== documentId || message.headSeq <= headSeq || closing || disposed || fault) return;
419
+ enqueue(restore).catch(() => void 0);
420
+ };
421
+ await claimInitialLeadership();
422
+ if (fault) throw fault;
423
+ return Object.freeze({
424
+ state: () => {
425
+ if (disposed) return { status: "disposed" };
426
+ if (fault) return {
427
+ status: "error",
428
+ headSeq,
429
+ checkpointSeq,
430
+ error: fault
431
+ };
432
+ return {
433
+ status: role,
434
+ headSeq,
435
+ checkpointSeq
436
+ };
437
+ },
438
+ flush: async () => {
439
+ assertOpen();
440
+ try {
441
+ await enqueue(restore);
442
+ } catch (error) {
443
+ fail(error);
444
+ throw error;
445
+ }
446
+ assertOpen();
447
+ },
448
+ dispose: async () => {
449
+ if (disposed) return;
450
+ if (closing) {
451
+ await queued;
452
+ return;
453
+ }
454
+ closing = true;
455
+ unsubscribe?.();
456
+ await queued;
457
+ releaseLeadership?.();
458
+ if (role === "leader") await leadershipLease;
459
+ disposed = true;
460
+ driver?.dispose();
461
+ channel?.close();
462
+ timeline.close();
463
+ }
464
+ });
465
+ } catch (error) {
466
+ unsubscribe?.();
467
+ driver?.dispose();
468
+ channel?.close();
469
+ timeline.close();
470
+ throw error;
471
+ }
472
+ };
473
+ //#endregion
474
+ export { LocalSyncConsistencyError, LocalSyncDataError, LocalSyncDisposedError, LocalSyncReadOnlyError, LocalSyncSchemaError, LocalSyncUnavailableError, LocalSyncUnsupportedOperationError, attachLocalSync, defaultJsonCommandLimits };
475
+
476
+ //# sourceMappingURL=local-sync.js.map