korajs 0.4.0 → 0.6.0

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/dist/index.cjs CHANGED
@@ -30,33 +30,90 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- HybridLogicalClock: () => import_core3.HybridLogicalClock,
34
- KoraError: () => import_core6.KoraError,
35
- MergeEngine: () => import_merge2.MergeEngine,
36
- SequenceManager: () => import_store2.SequenceManager,
37
- Store: () => import_store2.Store,
38
- SyncEngine: () => import_sync2.SyncEngine,
39
- TransactionContext: () => import_store3.TransactionContext,
40
- WebSocketTransport: () => import_sync2.WebSocketTransport,
33
+ AppNotReadyError: () => import_core10.AppNotReadyError,
34
+ HybridLogicalClock: () => import_core7.HybridLogicalClock,
35
+ KoraError: () => import_core10.KoraError,
36
+ MergeEngine: () => import_merge3.MergeEngine,
37
+ SequenceManager: () => import_store7.SequenceManager,
38
+ Store: () => import_store7.Store,
39
+ SyncEngine: () => import_sync7.SyncEngine,
40
+ TransactionContext: () => import_store8.TransactionContext,
41
+ WebSocketTransport: () => import_sync7.WebSocketTransport,
41
42
  createApp: () => createApp,
42
- createOperation: () => import_core5.createOperation,
43
- defineSchema: () => import_core2.defineSchema,
44
- generateUUIDv7: () => import_core4.generateUUIDv7,
45
- migrate: () => import_core2.migrate,
46
- op: () => import_core7.op,
47
- t: () => import_core2.t
43
+ createOperation: () => import_core9.createOperation,
44
+ decodeAuditExport: () => import_store6.decodeAuditExport,
45
+ defineSchema: () => import_core6.defineSchema,
46
+ exportBackup: () => import_store9.exportBackup,
47
+ generateUUIDv7: () => import_core8.generateUUIDv7,
48
+ migrate: () => import_core6.migrate,
49
+ op: () => import_core11.op,
50
+ readAuditExportManifest: () => import_store6.readAuditExportManifest,
51
+ readBackupManifest: () => import_store9.readBackupManifest,
52
+ restoreBackup: () => import_store9.restoreBackup,
53
+ t: () => import_core6.t,
54
+ verifyAuditExportChecksum: () => import_store6.verifyAuditExportChecksum,
55
+ verifyBackupChecksum: () => import_store9.verifyBackupChecksum
48
56
  });
49
57
  module.exports = __toCommonJS(index_exports);
50
58
 
51
59
  // src/create-app.ts
60
+ var import_internal5 = require("@korajs/core/internal");
61
+ var import_merge2 = require("@korajs/merge");
62
+ var import_store5 = require("@korajs/store");
63
+
64
+ // src/collection-accessor.ts
52
65
  var import_core = require("@korajs/core");
53
- var import_internal = require("@korajs/core/internal");
54
- var import_devtools = require("@korajs/devtools");
55
- var import_merge = require("@korajs/merge");
56
- var import_store = require("@korajs/store");
57
- var import_sync = require("@korajs/sync");
66
+ function createCollectionAccessor(collectionName, getStore) {
67
+ const notReady = (action) => new import_core.AppNotReadyError(
68
+ `Cannot ${action} on collection "${collectionName}" before app.ready. Await app.ready or use <KoraProvider app={app}>.`
69
+ );
70
+ return {
71
+ async insert(data) {
72
+ const currentStore = getStore();
73
+ if (!currentStore) {
74
+ throw notReady("insert into");
75
+ }
76
+ return currentStore.collection(collectionName).insert(data);
77
+ },
78
+ async findById(id) {
79
+ const currentStore = getStore();
80
+ if (!currentStore) return null;
81
+ return currentStore.collection(collectionName).findById(id);
82
+ },
83
+ async update(id, data) {
84
+ const currentStore = getStore();
85
+ if (!currentStore) {
86
+ throw notReady("update");
87
+ }
88
+ return currentStore.collection(collectionName).update(id, data);
89
+ },
90
+ async delete(id) {
91
+ const currentStore = getStore();
92
+ if (!currentStore) {
93
+ throw notReady("delete from");
94
+ }
95
+ return currentStore.collection(collectionName).delete(id);
96
+ },
97
+ where(conditions) {
98
+ const currentStore = getStore();
99
+ if (!currentStore) {
100
+ throw notReady("query");
101
+ }
102
+ return currentStore.collection(collectionName).where(conditions);
103
+ }
104
+ };
105
+ }
106
+
107
+ // src/initialize-app.ts
108
+ var import_core4 = require("@korajs/core");
109
+ var import_store4 = require("@korajs/store");
110
+ var import_sync3 = require("@korajs/sync");
58
111
 
59
112
  // src/adapter-resolver.ts
113
+ function importOptionalPeer(specifier) {
114
+ const dynamicImport = new Function("specifier", "return import(specifier)");
115
+ return dynamicImport(specifier);
116
+ }
60
117
  function detectAdapterType() {
61
118
  if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
62
119
  return "tauri-sqlite";
@@ -72,13 +129,10 @@ function detectAdapterType() {
72
129
  }
73
130
  return "better-sqlite3";
74
131
  }
75
- async function createAdapter(type, dbName, workerUrl) {
132
+ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
76
133
  switch (type) {
77
134
  case "tauri-sqlite": {
78
- const { TauriSqliteAdapter } = await import(
79
- /* @vite-ignore */
80
- "@korajs/tauri"
81
- );
135
+ const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
82
136
  return new TauriSqliteAdapter({ path: `${dbName}.db` });
83
137
  }
84
138
  case "better-sqlite3": {
@@ -90,11 +144,11 @@ async function createAdapter(type, dbName, workerUrl) {
90
144
  }
91
145
  case "sqlite-wasm": {
92
146
  const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
93
- return new SqliteWasmAdapter({ dbName, workerUrl });
147
+ return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
94
148
  }
95
149
  case "indexeddb": {
96
150
  const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
97
- return new IndexedDbAdapter({ dbName, workerUrl });
151
+ return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
98
152
  }
99
153
  default: {
100
154
  const _exhaustive = type;
@@ -103,85 +157,525 @@ async function createAdapter(type, dbName, workerUrl) {
103
157
  }
104
158
  }
105
159
 
106
- // src/merge-aware-sync-store.ts
107
- var MergeAwareSyncStore = class {
108
- constructor(store, mergeEngine, emitter) {
109
- this.store = store;
110
- this.mergeEngine = mergeEngine;
111
- this.emitter = emitter;
160
+ // src/apply-pipeline.ts
161
+ var import_core3 = require("@korajs/core");
162
+ var import_internal2 = require("@korajs/core/internal");
163
+ var import_merge = require("@korajs/merge");
164
+ var import_store = require("@korajs/store");
165
+ var import_internal3 = require("@korajs/store/internal");
166
+
167
+ // src/build-side-effect-entry.ts
168
+ var import_core2 = require("@korajs/core");
169
+ var import_internal = require("@korajs/store/internal");
170
+ async function buildSideEffectEntry(ctx, effect, parentOpId, transactionId, mutationName) {
171
+ const causalDeps = [parentOpId];
172
+ const baseInput = {
173
+ nodeId: ctx.nodeId,
174
+ collection: effect.collection,
175
+ recordId: effect.recordId,
176
+ sequenceNumber: await ctx.allocateSequenceNumber(),
177
+ causalDeps,
178
+ schemaVersion: ctx.schema.version,
179
+ ...transactionId !== void 0 ? { transactionId } : {},
180
+ ...mutationName !== void 0 ? { mutationName } : {}
181
+ };
182
+ if (effect.type === "delete") {
183
+ const operation2 = await (0, import_core2.createOperation)(
184
+ {
185
+ ...baseInput,
186
+ type: "delete",
187
+ data: null,
188
+ previousData: effect.previousData
189
+ },
190
+ ctx.clock
191
+ );
192
+ ctx.causalTracker?.afterOperation(effect.collection, operation2.id, ctx.inTransaction);
193
+ const version2 = (0, import_internal.serializeRowVersion)(operation2.timestamp);
194
+ const deleteQuery = (0, import_internal.buildSoftDeleteQuery)(
195
+ effect.collection,
196
+ effect.recordId,
197
+ operation2.timestamp.wallTime,
198
+ version2
199
+ );
200
+ const opInsert2 = (0, import_internal.buildInsertQuery)(
201
+ `_kora_ops_${effect.collection}`,
202
+ (0, import_internal.serializeOperation)(operation2)
203
+ );
204
+ return {
205
+ operation: operation2,
206
+ collection: effect.collection,
207
+ commands: [
208
+ { sql: deleteQuery.sql, params: deleteQuery.params },
209
+ { sql: opInsert2.sql, params: opInsert2.params },
210
+ {
211
+ sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
212
+ params: [ctx.nodeId, operation2.sequenceNumber]
213
+ }
214
+ ]
215
+ };
112
216
  }
113
- store;
114
- mergeEngine;
115
- emitter;
116
- getVersionVector() {
117
- return this.store.getVersionVector();
217
+ const operation = await (0, import_core2.createOperation)(
218
+ {
219
+ ...baseInput,
220
+ type: "update",
221
+ data: effect.data,
222
+ previousData: effect.previousData
223
+ },
224
+ ctx.clock
225
+ );
226
+ ctx.causalTracker?.afterOperation(effect.collection, operation.id, ctx.inTransaction);
227
+ const definition = ctx.schema.collections[effect.collection];
228
+ const serializedChanges = effect.data && definition ? (0, import_internal.serializeRecord)(effect.data, definition.fields) : effect.data ?? {};
229
+ const version = (0, import_internal.serializeRowVersion)(operation.timestamp);
230
+ const updateQuery = (0, import_internal.buildUpdateQuery)(effect.collection, effect.recordId, {
231
+ ...serializedChanges,
232
+ _updated_at: operation.timestamp.wallTime,
233
+ _version: version
234
+ });
235
+ const opInsert = (0, import_internal.buildInsertQuery)(
236
+ `_kora_ops_${effect.collection}`,
237
+ (0, import_internal.serializeOperation)(operation)
238
+ );
239
+ return {
240
+ operation,
241
+ collection: effect.collection,
242
+ commands: [
243
+ { sql: updateQuery.sql, params: updateQuery.params },
244
+ { sql: opInsert.sql, params: opInsert.params },
245
+ {
246
+ sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
247
+ params: [ctx.nodeId, operation.sequenceNumber]
248
+ }
249
+ ]
250
+ };
251
+ }
252
+
253
+ // src/apply-pipeline.ts
254
+ var ApplyPipeline = class {
255
+ constructor(deps) {
256
+ this.deps = deps;
257
+ this.relationLookupMap = (0, import_merge.buildMergeRelationLookup)(deps.store.getSchema());
118
258
  }
119
- getNodeId() {
120
- return this.store.getNodeId();
259
+ deps;
260
+ relationLookupMap;
261
+ /** Local insert — same persistence path as remote apply side effects. */
262
+ async insert(collection, data) {
263
+ return (0, import_internal3.executeInsert)(this.deps.store.createMutationContext(collection), data);
121
264
  }
122
- async getOperationRange(nodeId, fromSeq, toSeq) {
123
- return this.store.getOperationRange(nodeId, fromSeq, toSeq);
265
+ /** Local update. */
266
+ async update(collection, id, data) {
267
+ return (0, import_internal3.executeUpdate)(this.deps.store.createMutationContext(collection), id, data);
124
268
  }
125
- async applyRemoteOperation(op2) {
126
- if (op2.type !== "update" || !op2.data || !op2.previousData) {
127
- return this.store.applyRemoteOperation(op2);
269
+ /**
270
+ * Local delete with merge-package referential integrity (same rules as remote delete).
271
+ */
272
+ async delete(collection, id) {
273
+ const ctx = this.deps.store.createMutationContext(collection);
274
+ const causalDeps = (0, import_internal3.resolveCausalDeps)(ctx);
275
+ const operation = await (0, import_core3.createOperation)(
276
+ {
277
+ nodeId: ctx.nodeId,
278
+ type: "delete",
279
+ collection,
280
+ recordId: id,
281
+ data: null,
282
+ previousData: null,
283
+ sequenceNumber: await ctx.allocateSequenceNumber(),
284
+ causalDeps,
285
+ schemaVersion: ctx.schema.version
286
+ },
287
+ ctx.clock
288
+ );
289
+ ctx.causalTracker?.afterOperation(collection, operation.id, ctx.inTransaction);
290
+ const refCtx = createReferentialMergeContext(this.deps.store);
291
+ const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
292
+ operation,
293
+ this.deps.store.getSchema(),
294
+ refCtx,
295
+ this.relationLookupMap
296
+ );
297
+ for (const trace of check.traces) {
298
+ this.deps.emitter?.emit({ type: "merge:conflict", trace });
299
+ }
300
+ if (!check.allowed) {
301
+ throw new import_core3.KoraError(
302
+ `Cannot delete record "${id}" from "${collection}": referential restrict policy violated`,
303
+ "REFERENTIAL_INTEGRITY",
304
+ { collection, recordId: id }
305
+ );
306
+ }
307
+ await (0, import_internal3.executeDelete)(ctx, id, {
308
+ skipReferentialEnforcement: true,
309
+ operation
310
+ });
311
+ if (check.sideEffectOps.length > 0) {
312
+ await applySideEffectOps(this.deps.store, check.sideEffectOps, operation.id);
313
+ }
314
+ }
315
+ /**
316
+ * Commit a buffered transaction: merge-package delete enforcement, causal ordering, single DB txn.
317
+ */
318
+ async commitTransaction(batch) {
319
+ const store = this.deps.store;
320
+ const supplemental = [];
321
+ for (const entry of batch.entries) {
322
+ if (entry.operation.type !== "delete") {
323
+ continue;
324
+ }
325
+ const refCtx = createReferentialMergeContext(store);
326
+ const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
327
+ entry.operation,
328
+ store.getSchema(),
329
+ refCtx,
330
+ this.relationLookupMap
331
+ );
332
+ for (const trace of check.traces) {
333
+ this.deps.emitter?.emit({ type: "merge:conflict", trace });
334
+ }
335
+ if (!check.allowed) {
336
+ throw new import_core3.KoraError(
337
+ `Cannot delete record "${entry.operation.recordId}" from "${entry.collection}": referential restrict policy violated`,
338
+ "REFERENTIAL_INTEGRITY",
339
+ { collection: entry.collection, recordId: entry.operation.recordId }
340
+ );
341
+ }
342
+ for (const effect of check.sideEffectOps) {
343
+ const ctx2 = store.createMutationContext(effect.collection, { inTransaction: true });
344
+ supplemental.push(
345
+ await buildSideEffectEntry(
346
+ ctx2,
347
+ effect,
348
+ entry.operation.id,
349
+ batch.transactionId,
350
+ batch.mutationName
351
+ )
352
+ );
353
+ }
354
+ }
355
+ const allEntries = [...batch.entries, ...supplemental];
356
+ const sortedOps = (0, import_internal2.topologicalSort)(allEntries.map((e) => e.operation));
357
+ const commandsByOpId = new Map(allEntries.map((e) => [e.operation.id, e.commands]));
358
+ const ctx = store.createMutationContext(
359
+ batch.entries[0]?.collection ?? Object.keys(store.getSchema().collections)[0] ?? "todos",
360
+ { inTransaction: true }
361
+ );
362
+ await ctx.adapter.transaction(async (tx) => {
363
+ for (const op2 of sortedOps) {
364
+ const commands = commandsByOpId.get(op2.id);
365
+ if (!commands) {
366
+ continue;
367
+ }
368
+ for (const cmd of commands) {
369
+ await tx.execute(cmd.sql, cmd.params);
370
+ }
371
+ }
372
+ });
373
+ const affectedCollections = /* @__PURE__ */ new Set();
374
+ for (const entry of allEntries) {
375
+ affectedCollections.add(entry.collection);
376
+ }
377
+ return { operations: sortedOps, affectedCollections };
378
+ }
379
+ async applyRemote(op2) {
380
+ return this.apply(op2, { mode: "remote", schema: this.deps.store.getSchema() });
381
+ }
382
+ async apply(op2, context) {
383
+ if (context.mode === "local") {
384
+ return this.deps.store.applyRemoteOperation(op2);
385
+ }
386
+ if (op2.type === "delete") {
387
+ return this.applyRemoteDelete(op2);
388
+ }
389
+ if (op2.type === "insert" && op2.data) {
390
+ return this.applyRemoteInsert(op2);
391
+ }
392
+ if (op2.type === "update" && op2.data && op2.previousData) {
393
+ return this.applyRemoteUpdate(op2);
394
+ }
395
+ return this.deps.store.applyRemoteOperation(op2);
396
+ }
397
+ async applyRemoteDelete(op2) {
398
+ const blocked = await this.resolveRemoteDeleteVsLocalUpdate(op2);
399
+ if (blocked) {
400
+ return "skipped";
401
+ }
402
+ const refCtx = createReferentialMergeContext(this.deps.store);
403
+ const check = await (0, import_merge.checkReferentialIntegrityOnDelete)(
404
+ op2,
405
+ this.deps.store.getSchema(),
406
+ refCtx,
407
+ this.relationLookupMap
408
+ );
409
+ for (const trace of check.traces) {
410
+ this.deps.emitter?.emit({ type: "merge:conflict", trace });
411
+ }
412
+ if (!check.allowed) {
413
+ return "rejected";
414
+ }
415
+ const result = await this.deps.store.applyRemoteOperation(op2);
416
+ if (result !== "applied") {
417
+ return result;
418
+ }
419
+ if (check.sideEffectOps.length > 0) {
420
+ await applySideEffectOps(this.deps.store, check.sideEffectOps, op2.id);
421
+ }
422
+ return "applied";
423
+ }
424
+ /**
425
+ * When a local update is newer than a remote delete, keep the record alive.
426
+ */
427
+ async resolveRemoteDeleteVsLocalUpdate(op2) {
428
+ const collectionDef = this.deps.store.getSchema().collections[op2.collection];
429
+ if (!collectionDef) {
430
+ return false;
431
+ }
432
+ const localOp = await this.deps.store.getLatestLocalOperationForRecord(
433
+ op2.collection,
434
+ op2.recordId
435
+ );
436
+ if (!localOp || localOp.type !== "update") {
437
+ return false;
128
438
  }
129
- const schema = this.store.getSchema();
439
+ const baseState = localOp.previousData ?? {};
440
+ const mergeResult = await this.deps.mergeEngine.merge({
441
+ local: localOp,
442
+ remote: op2,
443
+ baseState,
444
+ collectionDef
445
+ });
446
+ this.emitMergeLifecycle(op2, localOp, mergeResult);
447
+ return mergeResult.appliedOperation === "local";
448
+ }
449
+ async applyRemoteUpdate(op2) {
450
+ const schema = this.deps.store.getSchema();
130
451
  const collectionDef = schema.collections[op2.collection];
131
452
  if (!collectionDef) {
132
- return this.store.applyRemoteOperation(op2);
453
+ return this.deps.store.applyRemoteOperation(op2);
133
454
  }
134
- const accessor = this.store.collection(op2.collection);
455
+ const accessor = this.deps.store.collection(op2.collection);
135
456
  const currentRecord = await accessor.findById(op2.recordId);
136
457
  if (!currentRecord) {
137
- return this.store.applyRemoteOperation(op2);
458
+ const tombstoneResult = await this.applyRemoteUpdateOnDeletedRow(op2, collectionDef);
459
+ if (tombstoneResult !== null) {
460
+ return tombstoneResult;
461
+ }
462
+ return this.deps.store.applyRemoteOperation(op2);
138
463
  }
139
464
  let hasConflict = false;
140
- for (const field of Object.keys(op2.data)) {
141
- const expectedBase = op2.previousData[field];
465
+ for (const field of Object.keys(op2.data ?? {})) {
466
+ const fieldDef = collectionDef.fields[field];
467
+ const expectedBase = op2.previousData?.[field];
142
468
  const currentLocal = currentRecord[field];
469
+ if (fieldDef?.kind === "richtext") {
470
+ if (!(0, import_store.richtextStatesEqual)(currentLocal, expectedBase)) {
471
+ hasConflict = true;
472
+ }
473
+ continue;
474
+ }
143
475
  if (!deepEqual(expectedBase, currentLocal)) {
144
476
  hasConflict = true;
145
477
  break;
146
478
  }
147
479
  }
148
480
  if (!hasConflict) {
149
- return this.store.applyRemoteOperation(op2);
481
+ return this.deps.store.applyRemoteOperation(op2);
150
482
  }
151
- this.emitter?.emit({
152
- type: "merge:started",
153
- operationA: op2,
154
- operationB: op2
483
+ return this.applyMergedUpdate(op2, collectionDef, currentRecord, op2.previousData ?? {});
484
+ }
485
+ /**
486
+ * Remote update vs local soft-delete: merge before materializing to avoid zombie rows.
487
+ */
488
+ async applyRemoteUpdateOnDeletedRow(op2, collectionDef) {
489
+ const snapshot = await this.deps.store.findMaterializedRow(op2.collection, op2.recordId);
490
+ if (!snapshot?.deleted) {
491
+ return null;
492
+ }
493
+ const localOp = await this.deps.store.getLatestLocalOperationForRecord(
494
+ op2.collection,
495
+ op2.recordId
496
+ );
497
+ if (!localOp || localOp.type !== "delete") {
498
+ return null;
499
+ }
500
+ const mergeResult = await this.deps.mergeEngine.merge({
501
+ local: localOp,
502
+ remote: op2,
503
+ baseState: op2.previousData ?? {},
504
+ collectionDef
505
+ });
506
+ this.emitMergeLifecycle(op2, localOp, mergeResult);
507
+ if (mergeResult.appliedOperation === "local") {
508
+ return "skipped";
509
+ }
510
+ const mergedOp = {
511
+ ...op2,
512
+ data: { ...mergeResult.mergedData },
513
+ timestamp: maxTimestamp(op2.timestamp, localOp.timestamp)
514
+ };
515
+ const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, {
516
+ reactivateIfDeleted: true
155
517
  });
156
- const baseState = { ...op2.previousData };
518
+ if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
519
+ await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op2.id);
520
+ }
521
+ return applyResult;
522
+ }
523
+ async applyMergedUpdate(op2, collectionDef, currentRecord, baseState, applyOptions) {
524
+ const localTimestamp = await resolveLocalTimestamp(
525
+ this.deps.store,
526
+ op2.collection,
527
+ op2.recordId,
528
+ currentRecord,
529
+ this.deps.store.getNodeId()
530
+ );
157
531
  const localOp = {
158
532
  ...op2,
159
- // The "local" operation's data is the diff between base and current local state
160
- data: buildLocalDiff(op2.previousData, currentRecord, Object.keys(op2.data)),
533
+ data: buildLocalDiff(baseState, currentRecord, Object.keys(op2.data ?? {})),
161
534
  previousData: op2.previousData,
162
- nodeId: this.store.getNodeId()
535
+ nodeId: this.deps.store.getNodeId(),
536
+ timestamp: localTimestamp
537
+ };
538
+ const constraintContext = createConstraintContext(this.deps.store);
539
+ const mergeResult = await this.deps.mergeEngine.merge(
540
+ {
541
+ local: localOp,
542
+ remote: op2,
543
+ baseState,
544
+ collectionDef
545
+ },
546
+ constraintContext
547
+ );
548
+ this.emitMergeLifecycle(op2, localOp, mergeResult);
549
+ const mergedOp = {
550
+ ...op2,
551
+ data: mergeResult.mergedData,
552
+ timestamp: maxTimestamp(op2.timestamp, localTimestamp)
163
553
  };
164
- const input = {
554
+ const applyResult = await this.deps.store.applyRemoteOperation(mergedOp, applyOptions);
555
+ if (applyResult === "applied" && mergeResult.sideEffects.length > 0) {
556
+ await applySideEffectOps(this.deps.store, mergeResult.sideEffects, op2.id);
557
+ }
558
+ return applyResult;
559
+ }
560
+ async applyRemoteInsert(op2) {
561
+ const collectionDef = this.deps.store.getSchema().collections[op2.collection];
562
+ if (!collectionDef || !op2.data) {
563
+ return this.deps.store.applyRemoteOperation(op2);
564
+ }
565
+ const snapshot = await this.deps.store.findMaterializedRow(op2.collection, op2.recordId);
566
+ if (!snapshot || snapshot.deleted) {
567
+ return this.deps.store.applyRemoteOperation(op2);
568
+ }
569
+ const localOp = await resolveLocalOpForRecord(
570
+ this.deps.store,
571
+ op2.collection,
572
+ op2.recordId,
573
+ snapshot.record
574
+ );
575
+ const mergeResult = await this.deps.mergeEngine.merge({
165
576
  local: localOp,
166
577
  remote: op2,
167
- baseState,
578
+ baseState: {},
168
579
  collectionDef
580
+ });
581
+ this.emitMergeLifecycle(op2, localOp, mergeResult);
582
+ const mergedOp = {
583
+ ...op2,
584
+ data: mergeResult.mergedData,
585
+ timestamp: maxTimestamp(op2.timestamp, localOp.timestamp)
169
586
  };
170
- const result = this.mergeEngine.mergeFields(input);
171
- for (const trace of result.traces) {
172
- this.emitter?.emit({ type: "merge:conflict", trace });
587
+ return this.deps.store.applyRemoteOperation(mergedOp);
588
+ }
589
+ emitMergeLifecycle(remote, local, mergeResult) {
590
+ this.deps.emitter?.emit({
591
+ type: "merge:started",
592
+ operationA: remote,
593
+ operationB: local
594
+ });
595
+ const hadMergeConflict = mergeResult.traces.some(
596
+ (t2) => t2.strategy !== "no-conflict-local" && t2.strategy !== "no-conflict-remote" && t2.strategy !== "no-conflict-unchanged"
597
+ );
598
+ if (hadMergeConflict) {
599
+ this.deps.onMergeConflict?.();
600
+ }
601
+ for (const trace of mergeResult.traces) {
602
+ if (trace.strategy !== "no-conflict-local" && trace.strategy !== "no-conflict-remote" && trace.strategy !== "no-conflict-unchanged") {
603
+ this.deps.emitter?.emit({ type: "merge:conflict", trace });
604
+ }
173
605
  }
174
- const firstTrace = result.traces[0];
606
+ const firstTrace = mergeResult.traces[0];
175
607
  if (firstTrace) {
176
- this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
608
+ this.deps.emitter?.emit({ type: "merge:completed", trace: firstTrace });
177
609
  }
178
- const mergedOp = {
179
- ...op2,
180
- data: result.mergedData
181
- };
182
- return this.store.applyRemoteOperation(mergedOp);
183
610
  }
184
611
  };
612
+ async function resolveLocalOpForRecord(store, collection, recordId, record) {
613
+ const localLatest = await store.getLatestLocalOperationForRecord(collection, recordId);
614
+ if (localLatest) {
615
+ return localLatest;
616
+ }
617
+ const anyLatest = await store.getLatestOperationForRecord(collection, recordId);
618
+ if (anyLatest) {
619
+ return anyLatest;
620
+ }
621
+ return syntheticInsertFromSnapshot(
622
+ record,
623
+ collection,
624
+ store.getNodeId(),
625
+ store.getSchema().version
626
+ );
627
+ }
628
+ function syntheticInsertFromSnapshot(record, collection, nodeId, schemaVersion) {
629
+ const { id, createdAt, updatedAt, ...fields } = record;
630
+ return {
631
+ id: `synthetic-local-${id}`,
632
+ nodeId,
633
+ type: "insert",
634
+ collection,
635
+ recordId: id,
636
+ data: fields,
637
+ previousData: null,
638
+ timestamp: { wallTime: updatedAt, logical: 0, nodeId },
639
+ sequenceNumber: 0,
640
+ causalDeps: [],
641
+ schemaVersion
642
+ };
643
+ }
644
+ function createConstraintContext(store) {
645
+ return {
646
+ async queryRecords(collection, where) {
647
+ const rows = await store.collection(collection).where(where).exec();
648
+ return rows;
649
+ },
650
+ async countRecords(collection, where) {
651
+ return store.collection(collection).where(where).count();
652
+ }
653
+ };
654
+ }
655
+ function createReferentialMergeContext(store) {
656
+ return {
657
+ async queryRecords(collection, where) {
658
+ const rows = await store.collection(collection).where(where).exec();
659
+ return rows;
660
+ },
661
+ async recordExists(collection, recordId) {
662
+ const row = await store.collection(collection).findById(recordId);
663
+ return row !== null;
664
+ }
665
+ };
666
+ }
667
+ async function applySideEffectOps(store, sideEffects, parentOpId) {
668
+ for (const effect of sideEffects) {
669
+ const ctx = store.createMutationContext(effect.collection, {
670
+ extraCausalDeps: [parentOpId]
671
+ });
672
+ if (effect.type === "delete") {
673
+ await (0, import_internal3.executeDelete)(ctx, effect.recordId, { skipReferentialEnforcement: true });
674
+ } else if (effect.type === "update" && effect.data) {
675
+ await (0, import_internal3.executeUpdate)(ctx, effect.recordId, effect.data);
676
+ }
677
+ }
678
+ }
185
679
  function buildLocalDiff(baseState, currentRecord, fields) {
186
680
  const diff = {};
187
681
  for (const field of fields) {
@@ -189,6 +683,20 @@ function buildLocalDiff(baseState, currentRecord, fields) {
189
683
  }
190
684
  return diff;
191
685
  }
686
+ async function resolveLocalTimestamp(store, collection, recordId, currentRecord, nodeId) {
687
+ const latestLocal = await store.getLatestLocalOperationForRecord(collection, recordId);
688
+ if (latestLocal) {
689
+ return latestLocal.timestamp;
690
+ }
691
+ const updatedAt = currentRecord.updatedAt;
692
+ if (typeof updatedAt === "number") {
693
+ return { wallTime: updatedAt, logical: 0, nodeId };
694
+ }
695
+ return { wallTime: Date.now(), logical: 0, nodeId };
696
+ }
697
+ function maxTimestamp(a, b) {
698
+ return import_core3.HybridLogicalClock.compare(a, b) >= 0 ? a : b;
699
+ }
192
700
  function deepEqual(a, b) {
193
701
  if (a === b) return true;
194
702
  if (a === null || b === null) return false;
@@ -209,113 +717,371 @@ function deepEqual(a, b) {
209
717
  return false;
210
718
  }
211
719
 
212
- // src/create-app.ts
213
- function createApp(config) {
214
- const emitter = new import_internal.SimpleEventEmitter();
215
- const mergeEngine = new import_merge.MergeEngine();
216
- let store = null;
217
- let syncEngine = null;
218
- let unsubscribeSync = null;
219
- let reconnectionManager = null;
220
- let connectionMonitor = null;
221
- let instrumenter = null;
222
- let intentionalDisconnect = false;
223
- let qualityInterval = null;
224
- if (config.devtools) {
225
- instrumenter = new import_devtools.Instrumenter(emitter, {
226
- bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
720
+ // src/audit-bridge.ts
721
+ var import_store2 = require("@korajs/store");
722
+ var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
723
+ function isAuditEvent(event) {
724
+ return AUDIT_EVENT_TYPES.includes(event.type);
725
+ }
726
+ function wireAuditPersistence(store, emitter) {
727
+ const unsubscribers = AUDIT_EVENT_TYPES.map(
728
+ (eventType) => emitter.on(eventType, (event) => {
729
+ if (!isAuditEvent(event)) {
730
+ return;
731
+ }
732
+ const trace = (0, import_store2.persistedAuditTraceFromEvent)(event);
733
+ void store.appendAuditTrace(trace).catch(() => {
734
+ });
735
+ })
736
+ );
737
+ return () => {
738
+ for (const unsub of unsubscribers) {
739
+ unsub();
740
+ }
741
+ };
742
+ }
743
+
744
+ // src/create-sync-transport.ts
745
+ var import_sync = require("@korajs/sync");
746
+ function createSyncTransport(sync) {
747
+ if (sync.transport === "http") {
748
+ return new import_sync.HttpLongPollingTransport();
749
+ }
750
+ return new import_sync.WebSocketTransport();
751
+ }
752
+
753
+ // src/merge-aware-sync-store.ts
754
+ var MergeAwareSyncStore = class {
755
+ constructor(store, mergeEngine, emitter, options) {
756
+ this.store = store;
757
+ this.pipeline = new ApplyPipeline({
758
+ store,
759
+ mergeEngine,
760
+ emitter,
761
+ onMergeConflict: options?.onMergeConflict
227
762
  });
228
763
  }
229
- const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
230
- store = result.store;
231
- syncEngine = result.syncEngine;
232
- unsubscribeSync = result.unsubscribeSync;
233
- if (config.sync && syncEngine) {
234
- connectionMonitor = new import_sync.ConnectionMonitor();
235
- reconnectionManager = new import_sync.ReconnectionManager({
236
- initialDelay: config.sync.reconnectInterval,
237
- maxDelay: config.sync.maxReconnectInterval
238
- });
239
- emitter.on("sync:sent", () => connectionMonitor?.recordActivity());
240
- emitter.on("sync:received", () => connectionMonitor?.recordActivity());
241
- emitter.on("sync:acknowledged", () => connectionMonitor?.recordActivity());
242
- emitter.on("sync:connected", () => {
243
- if (qualityInterval !== null) clearInterval(qualityInterval);
244
- qualityInterval = setInterval(() => {
245
- if (connectionMonitor) {
246
- emitter.emit({ type: "connection:quality", quality: connectionMonitor.getQuality() });
247
- }
248
- }, 5e3);
249
- });
250
- emitter.on("sync:disconnected", () => {
251
- connectionMonitor?.reset();
252
- if (qualityInterval !== null) {
253
- clearInterval(qualityInterval);
254
- qualityInterval = null;
255
- }
256
- });
257
- if (config.sync.autoReconnect !== false) {
258
- const engine = syncEngine;
259
- emitter.on("sync:disconnected", () => {
260
- if (intentionalDisconnect) return;
261
- if (reconnectionManager?.isRunning()) return;
262
- engine.setReconnecting(true);
263
- reconnectionManager?.stop();
264
- reconnectionManager?.start(async () => {
265
- try {
266
- await engine.start();
267
- engine.setReconnecting(false);
268
- return true;
269
- } catch {
270
- return false;
271
- }
272
- }).then(() => {
273
- engine.setReconnecting(false);
274
- });
275
- });
276
- }
764
+ store;
765
+ pipeline;
766
+ getVersionVector() {
767
+ return this.store.getVersionVector();
768
+ }
769
+ getNodeId() {
770
+ return this.store.getNodeId();
771
+ }
772
+ async getOperationRange(nodeId, fromSeq, toSeq) {
773
+ return this.store.getOperationRange(nodeId, fromSeq, toSeq);
774
+ }
775
+ async applyRemoteOperation(op2) {
776
+ return this.pipeline.applyRemote(op2);
777
+ }
778
+ };
779
+
780
+ // src/store-queue-storage.ts
781
+ var import_internal4 = require("@korajs/store/internal");
782
+ var StoreQueueStorage = class {
783
+ constructor(adapter) {
784
+ this.adapter = adapter;
785
+ }
786
+ adapter;
787
+ async load() {
788
+ const rows = await this.adapter.query(
789
+ "SELECT id, payload FROM _kora_sync_queue ORDER BY rowid ASC"
790
+ );
791
+ return rows.map((row) => operationFromQueuePayload(row.payload));
792
+ }
793
+ async enqueue(op2) {
794
+ const row = (0, import_internal4.serializeOperation)(op2);
795
+ const payload = { ...row, _collection: op2.collection };
796
+ await this.adapter.execute(
797
+ "INSERT OR REPLACE INTO _kora_sync_queue (id, payload) VALUES (?, ?)",
798
+ [op2.id, JSON.stringify(payload)]
799
+ );
800
+ }
801
+ async dequeue(ids) {
802
+ if (ids.length === 0) return;
803
+ const placeholders = ids.map(() => "?").join(", ");
804
+ await this.adapter.execute(`DELETE FROM _kora_sync_queue WHERE id IN (${placeholders})`, ids);
805
+ }
806
+ async count() {
807
+ const rows = await this.adapter.query(
808
+ "SELECT COUNT(*) as cnt FROM _kora_sync_queue"
809
+ );
810
+ return rows[0]?.cnt ?? 0;
811
+ }
812
+ };
813
+ function operationFromQueuePayload(payload) {
814
+ const parsed = JSON.parse(payload);
815
+ const op2 = (0, import_internal4.deserializeOperation)(parsed);
816
+ return {
817
+ ...op2,
818
+ collection: parsed._collection
819
+ };
820
+ }
821
+
822
+ // src/store-sync-state.ts
823
+ var import_store3 = require("@korajs/store");
824
+ var import_sync2 = require("@korajs/sync");
825
+ var StoreSyncStatePersistence = class {
826
+ constructor(store, scope) {
827
+ this.store = store;
828
+ this.scope = scope;
829
+ }
830
+ store;
831
+ scope;
832
+ loadLastAckedServerVector() {
833
+ return this.store.loadLastAckedServerVector();
834
+ }
835
+ saveLastAckedServerVector(vector) {
836
+ return this.store.saveLastAckedServerVector(vector);
837
+ }
838
+ mergeServerVectors(a, b) {
839
+ return this.store.mergeServerVectors(a, b);
840
+ }
841
+ async countUnsyncedOperations(serverVector) {
842
+ const ops = await this.getUnsyncedOperations(serverVector);
843
+ return ops.length;
844
+ }
845
+ async getUnsyncedOperations(serverVector) {
846
+ const ops = await this.store.getUnsyncedOperations(serverVector);
847
+ return ops.filter((op2) => (0, import_sync2.operationMatchesScope)(op2, this.scope));
848
+ }
849
+ loadDeltaCursor() {
850
+ return this.store.loadDeltaCursor().then((encoded) => (0, import_sync2.decodeDeltaCursor)(encoded));
851
+ }
852
+ async saveDeltaCursor(cursor) {
853
+ await this.store.saveDeltaCursor(cursor ? (0, import_sync2.encodeDeltaCursor)(cursor) : null);
854
+ }
855
+ };
856
+
857
+ // src/sync-query-bridge.ts
858
+ function queryDescriptorToSyncSubset(descriptor) {
859
+ const where = {};
860
+ const skippedFields = [];
861
+ for (const [field, value] of Object.entries(descriptor.where)) {
862
+ if (value === null || value === void 0) {
863
+ where[field] = value;
864
+ continue;
277
865
  }
278
- });
279
- const syncControl = config.sync ? {
280
- async connect() {
281
- await ready;
282
- if (syncEngine) {
283
- intentionalDisconnect = false;
284
- reconnectionManager?.stop();
285
- reconnectionManager?.reset();
286
- await syncEngine.start();
287
- }
866
+ if (typeof value !== "object" || Array.isArray(value)) {
867
+ where[field] = value;
868
+ continue;
869
+ }
870
+ skippedFields.push(field);
871
+ }
872
+ if (skippedFields.length > 0 && typeof console !== "undefined") {
873
+ console.warn(
874
+ `[Kora] Sync query subset omitted non-equality filters on ${descriptor.collection}: ${skippedFields.join(", ")}. Only plain equality WHERE clauses are registered for incremental sync.`
875
+ );
876
+ }
877
+ if (Object.keys(where).length === 0) {
878
+ return null;
879
+ }
880
+ return {
881
+ collection: descriptor.collection,
882
+ where
883
+ };
884
+ }
885
+ function createSyncQuerySubscriptionHook(getSyncEngine) {
886
+ return (descriptor) => {
887
+ const subset = queryDescriptorToSyncSubset(descriptor);
888
+ if (!subset) {
889
+ return () => {
890
+ };
891
+ }
892
+ const engine = getSyncEngine();
893
+ if (!engine) {
894
+ return () => {
895
+ };
896
+ }
897
+ return engine.registerQuerySubset(subset);
898
+ };
899
+ }
900
+
901
+ // src/initialize-app.ts
902
+ async function initializeApp(config, emitter, mergeEngine) {
903
+ const adapterType = config.store?.adapter ?? detectAdapterType();
904
+ const dbName = config.store?.name ?? "kora-db";
905
+ const adapter = await createAdapter(
906
+ adapterType,
907
+ dbName,
908
+ config.store?.workerUrl,
909
+ emitter,
910
+ config.store?.workerResponseTimeoutMs,
911
+ config.store?.sharedWorkerUrl
912
+ );
913
+ const authBinding = config.sync?.authClient ?? null;
914
+ const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
915
+ let syncEngine = null;
916
+ const store = new import_store4.Store({
917
+ schema: config.schema,
918
+ adapter,
919
+ emitter,
920
+ dbName,
921
+ nodeId: authNodeId,
922
+ isolation: authNodeId ? "shared" : config.store?.isolation,
923
+ ...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
924
+ });
925
+ await store.open();
926
+ let recordConflict;
927
+ const applyPipeline = new ApplyPipeline({
928
+ store,
929
+ mergeEngine,
930
+ emitter,
931
+ onMergeConflict: () => recordConflict?.()
932
+ });
933
+ store.setLocalMutationHandler(applyPipeline);
934
+ const unsubscribeAudit = wireAuditPersistence(store, emitter);
935
+ let unsubscribeSync = null;
936
+ if (config.sync) {
937
+ const transport = createSyncTransport(config.sync);
938
+ const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
939
+ onMergeConflict: () => recordConflict?.()
940
+ });
941
+ let scopeMap = config.sync.scope ? (0, import_core4.buildScopeMap)(config.schema, config.sync.scope) : void 0;
942
+ if (authBinding?.resolveScopeMap) {
943
+ scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
944
+ }
945
+ const syncAuth = authBinding?.auth ?? config.sync.auth;
946
+ const encryptor = config.sync.encryption?.enabled === true ? await import_sync3.SyncEncryptor.create(config.sync.encryption) : void 0;
947
+ syncEngine = new import_sync3.SyncEngine({
948
+ transport,
949
+ store: mergeAwareStore,
950
+ config: {
951
+ url: config.sync.url,
952
+ transport: config.sync.transport,
953
+ auth: syncAuth,
954
+ batchSize: config.sync.batchSize,
955
+ schemaVersion: config.sync.schemaVersion ?? config.schema.version,
956
+ scopeMap,
957
+ encryption: config.sync.encryption,
958
+ strictHandshake: config.sync.strictHandshake,
959
+ operationTransforms: config.sync.operationTransforms
960
+ },
961
+ emitter,
962
+ queueStorage: new StoreQueueStorage(adapter),
963
+ syncState: new StoreSyncStatePersistence(store, scopeMap),
964
+ encryptor
965
+ });
966
+ recordConflict = () => syncEngine?.recordConflict();
967
+ unsubscribeSync = emitter.on("operation:created", (event) => {
968
+ if (syncEngine) {
969
+ syncEngine.pushOperation(event.operation);
970
+ }
971
+ });
972
+ }
973
+ return {
974
+ store,
975
+ syncEngine,
976
+ unsubscribeSync,
977
+ unsubscribeAudit,
978
+ authBinding
979
+ };
980
+ }
981
+
982
+ // src/sequences-accessor.ts
983
+ function createSequencesAccessor(ready, getStore) {
984
+ const requireStore = async () => {
985
+ await ready;
986
+ const store = getStore();
987
+ if (!store) {
988
+ throw new Error("Store not initialized. Await app.ready before using sequences.");
989
+ }
990
+ return store;
991
+ };
992
+ return {
993
+ async next(name, config) {
994
+ const store = await requireStore();
995
+ return store.getSequenceManager().next(name, config);
996
+ },
997
+ async current(name, config) {
998
+ const store = await requireStore();
999
+ return store.getSequenceManager().current(name, config);
1000
+ },
1001
+ async reset(name, config) {
1002
+ const store = await requireStore();
1003
+ return store.getSequenceManager().reset(name, config);
1004
+ }
1005
+ };
1006
+ }
1007
+
1008
+ // src/setup-devtools.ts
1009
+ var import_devtools = require("@korajs/devtools");
1010
+ function setupDevtools(config, emitter) {
1011
+ if (!config.devtools) {
1012
+ return { instrumenter: null, destroyOverlay: null };
1013
+ }
1014
+ const instrumenter = new import_devtools.Instrumenter(emitter, {
1015
+ bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
1016
+ });
1017
+ let destroyOverlay = null;
1018
+ if (typeof globalThis !== "undefined" && "window" in globalThis) {
1019
+ void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
1020
+ destroyOverlay = mountKoraDevtoolsOverlay(instrumenter);
1021
+ }).catch(() => {
1022
+ });
1023
+ }
1024
+ return { instrumenter, destroyOverlay: () => destroyOverlay?.() };
1025
+ }
1026
+
1027
+ // src/sync-control.ts
1028
+ var import_sync4 = require("@korajs/sync");
1029
+ function createSyncControl(options) {
1030
+ const { config, ready, state } = options;
1031
+ if (!config.sync) {
1032
+ return null;
1033
+ }
1034
+ const offlineSyncStatus = () => import_sync4.OFFLINE_SYNC_STATUS;
1035
+ const bridgeStatus = () => state.syncStatusBridge?.status ?? offlineSyncStatus();
1036
+ return {
1037
+ get status() {
1038
+ return bridgeStatus();
1039
+ },
1040
+ subscribeStatus(listener) {
1041
+ if (state.syncStatusBridge) {
1042
+ return state.syncStatusBridge.subscribe(listener);
1043
+ }
1044
+ listener(offlineSyncStatus());
1045
+ return () => {
1046
+ };
1047
+ },
1048
+ async connect() {
1049
+ await ready;
1050
+ if (state.syncEngine) {
1051
+ state.intentionalDisconnect = false;
1052
+ state.reconnectionManager?.stop();
1053
+ state.reconnectionManager?.reset();
1054
+ await state.syncEngine.start();
1055
+ state.syncStatusBridge?.refresh();
1056
+ }
288
1057
  },
289
1058
  async disconnect() {
290
1059
  await ready;
291
- if (syncEngine) {
292
- intentionalDisconnect = true;
293
- reconnectionManager?.stop();
294
- await syncEngine.stop();
1060
+ if (state.syncEngine) {
1061
+ state.intentionalDisconnect = true;
1062
+ state.reconnectionManager?.stop();
1063
+ await state.syncEngine.stop();
1064
+ state.syncStatusBridge?.refresh();
295
1065
  }
296
1066
  },
297
1067
  getStatus() {
298
- if (syncEngine) {
299
- return syncEngine.getStatus();
1068
+ if (state.syncEngine) {
1069
+ return state.syncEngine.getStatus();
300
1070
  }
301
- return {
302
- status: "offline",
303
- pendingOperations: 0,
304
- lastSyncedAt: null,
305
- lastSuccessfulPush: null,
306
- lastSuccessfulPull: null,
307
- conflicts: 0
308
- };
1071
+ return offlineSyncStatus();
309
1072
  },
310
1073
  async retryNow() {
311
1074
  await ready;
312
- if (syncEngine) {
313
- await syncEngine.retryNow();
1075
+ if (state.syncEngine) {
1076
+ await state.syncEngine.retryNow();
314
1077
  }
315
1078
  },
1079
+ clearSchemaBlock() {
1080
+ state.syncEngine?.clearSchemaBlock();
1081
+ },
316
1082
  exportDiagnostics() {
317
- if (syncEngine) {
318
- return syncEngine.exportDiagnostics();
1083
+ if (state.syncEngine) {
1084
+ return state.syncEngine.exportDiagnostics();
319
1085
  }
320
1086
  return {
321
1087
  state: "disconnected",
@@ -340,62 +1106,341 @@ function createApp(config) {
340
1106
  timestamp: Date.now()
341
1107
  };
342
1108
  }
343
- } : null;
344
- async function executeTransaction(fn, mutationName) {
1109
+ };
1110
+ }
1111
+
1112
+ // src/auth-sync-coordinator.ts
1113
+ var AuthSyncCoordinator = class {
1114
+ constructor(getEngine, authBinding) {
1115
+ this.getEngine = getEngine;
1116
+ this.authBinding = authBinding;
1117
+ }
1118
+ getEngine;
1119
+ authBinding;
1120
+ inFlight = null;
1121
+ pending = false;
1122
+ disposed = false;
1123
+ scheduleReconnect() {
1124
+ if (this.disposed) {
1125
+ return;
1126
+ }
1127
+ if (this.inFlight) {
1128
+ this.pending = true;
1129
+ return;
1130
+ }
1131
+ this.inFlight = this.run().finally(() => {
1132
+ this.inFlight = null;
1133
+ if (this.pending && !this.disposed) {
1134
+ this.pending = false;
1135
+ this.scheduleReconnect();
1136
+ }
1137
+ });
1138
+ }
1139
+ destroy() {
1140
+ this.disposed = true;
1141
+ this.pending = false;
1142
+ }
1143
+ async run() {
1144
+ const engine = this.getEngine();
1145
+ if (!engine) {
1146
+ return;
1147
+ }
1148
+ const headers = await this.authBinding.auth();
1149
+ if (!headers.token) {
1150
+ await engine.stop();
1151
+ return;
1152
+ }
1153
+ if (this.authBinding.resolveScopeMap) {
1154
+ const nextScope = await this.authBinding.resolveScopeMap();
1155
+ engine.updateScope(nextScope);
1156
+ }
1157
+ const status = engine.getStatus().status;
1158
+ if (status !== "offline") {
1159
+ await engine.stop();
1160
+ }
1161
+ await engine.start();
1162
+ }
1163
+ };
1164
+
1165
+ // src/sync-status-bridge.ts
1166
+ var import_sync5 = require("@korajs/sync");
1167
+ function createSyncStatusBridge(emitter, getSyncEngine) {
1168
+ const controller = (0, import_sync5.createSyncStatusController)({
1169
+ getSyncEngine,
1170
+ subscribeSyncStatus: null,
1171
+ events: emitter
1172
+ });
1173
+ return {
1174
+ get status() {
1175
+ return controller.getSnapshot();
1176
+ },
1177
+ subscribe(listener) {
1178
+ return controller.subscribe(() => {
1179
+ listener(controller.getSnapshot());
1180
+ });
1181
+ },
1182
+ refresh: () => controller.refresh(),
1183
+ destroy: () => controller.destroy()
1184
+ };
1185
+ }
1186
+
1187
+ // src/sync-lifecycle.ts
1188
+ var import_sync6 = require("@korajs/sync");
1189
+ function wireSyncLifecycleAfterReady(config, emitter, state, init) {
1190
+ state.syncEngine = init.syncEngine;
1191
+ if (!config.sync) {
1192
+ return;
1193
+ }
1194
+ state.syncStatusBridge = createSyncStatusBridge(emitter, () => state.syncEngine);
1195
+ state.syncStatusBridge.refresh();
1196
+ if (state.syncEngine && init.authBinding?.subscribe) {
1197
+ state.authSyncCoordinator = new AuthSyncCoordinator(() => state.syncEngine, init.authBinding);
1198
+ init.authBinding.subscribe(() => {
1199
+ state.authSyncCoordinator?.scheduleReconnect();
1200
+ });
1201
+ }
1202
+ if (!state.syncEngine) {
1203
+ return;
1204
+ }
1205
+ const syncEngine = state.syncEngine;
1206
+ state.connectionMonitor = new import_sync6.ConnectionMonitor();
1207
+ state.reconnectionManager = new import_sync6.ReconnectionManager({
1208
+ initialDelay: config.sync.reconnectInterval,
1209
+ maxDelay: config.sync.maxReconnectInterval
1210
+ });
1211
+ emitter.on("sync:sent", () => state.connectionMonitor?.recordActivity());
1212
+ emitter.on("sync:received", () => state.connectionMonitor?.recordActivity());
1213
+ emitter.on("sync:acknowledged", () => state.connectionMonitor?.recordActivity());
1214
+ emitter.on("sync:connected", () => {
1215
+ if (state.qualityInterval !== null) {
1216
+ clearInterval(state.qualityInterval);
1217
+ }
1218
+ state.qualityInterval = setInterval(() => {
1219
+ if (state.connectionMonitor) {
1220
+ emitter.emit({
1221
+ type: "connection:quality",
1222
+ quality: state.connectionMonitor.getQuality()
1223
+ });
1224
+ }
1225
+ }, 5e3);
1226
+ });
1227
+ emitter.on("sync:disconnected", () => {
1228
+ state.connectionMonitor?.reset();
1229
+ if (state.qualityInterval !== null) {
1230
+ clearInterval(state.qualityInterval);
1231
+ state.qualityInterval = null;
1232
+ }
1233
+ });
1234
+ const browserGlobal = globalThis;
1235
+ if (typeof browserGlobal.addEventListener === "function") {
1236
+ const onOnline = () => {
1237
+ if (state.intentionalDisconnect || config.sync?.autoReconnect === false) {
1238
+ return;
1239
+ }
1240
+ state.reconnectionManager?.wake();
1241
+ state.reconnectionManager?.reset();
1242
+ void syncEngine.retryNow();
1243
+ };
1244
+ browserGlobal.addEventListener("online", onOnline);
1245
+ state.removeOnlineListener = () => {
1246
+ browserGlobal.removeEventListener?.("online", onOnline);
1247
+ };
1248
+ }
1249
+ emitter.on("sync:schema-mismatch", () => {
1250
+ state.reconnectionManager?.stop();
1251
+ state.intentionalDisconnect = true;
1252
+ });
1253
+ if (config.sync.autoReconnect !== false) {
1254
+ emitter.on("sync:disconnected", () => {
1255
+ if (state.intentionalDisconnect || syncEngine.isSchemaBlocked()) {
1256
+ return;
1257
+ }
1258
+ if (state.reconnectionManager?.isRunning()) {
1259
+ return;
1260
+ }
1261
+ syncEngine.setReconnecting(true);
1262
+ state.reconnectionManager?.stop();
1263
+ state.reconnectionManager?.start(async () => {
1264
+ try {
1265
+ await syncEngine.start();
1266
+ syncEngine.setReconnecting(false);
1267
+ return true;
1268
+ } catch {
1269
+ return false;
1270
+ }
1271
+ }).then(() => {
1272
+ syncEngine.setReconnecting(false);
1273
+ });
1274
+ });
1275
+ }
1276
+ if (config.sync.autoConnect === true) {
1277
+ void syncEngine.start().catch(() => {
1278
+ });
1279
+ }
1280
+ }
1281
+ function teardownSyncLifecycle(state) {
1282
+ if (state.qualityInterval !== null) {
1283
+ clearInterval(state.qualityInterval);
1284
+ state.qualityInterval = null;
1285
+ }
1286
+ state.reconnectionManager?.stop();
1287
+ state.syncStatusBridge?.destroy();
1288
+ state.syncStatusBridge = null;
1289
+ state.authSyncCoordinator?.destroy();
1290
+ state.authSyncCoordinator = null;
1291
+ state.removeOnlineListener?.();
1292
+ state.removeOnlineListener = null;
1293
+ }
1294
+
1295
+ // src/transaction-executor.ts
1296
+ function createTransactionExecutor(config, ready, getStore) {
1297
+ return async (fn, mutationName) => {
345
1298
  await ready;
1299
+ const store = getStore();
346
1300
  if (!store) {
347
1301
  throw new Error("Store not initialized. Await app.ready before using transactions.");
348
1302
  }
349
- const txContext = store.createTransaction();
350
- if (mutationName !== void 0) {
351
- txContext.setMutationName(mutationName);
352
- }
353
1303
  const collectionNames = Object.keys(config.schema.collections);
354
- const proxy = {};
355
- for (const name of collectionNames) {
356
- Object.defineProperty(proxy, name, {
357
- get() {
358
- return txContext.collection(name);
359
- },
360
- enumerable: true,
361
- configurable: false
362
- });
363
- }
364
- try {
1304
+ return store.transaction(async (tx) => {
1305
+ if (mutationName !== void 0) {
1306
+ tx.setMutationName(mutationName);
1307
+ }
1308
+ const proxy = {};
1309
+ for (const name of collectionNames) {
1310
+ Object.defineProperty(proxy, name, {
1311
+ get() {
1312
+ return tx.collection(name);
1313
+ },
1314
+ enumerable: true,
1315
+ configurable: false
1316
+ });
1317
+ }
365
1318
  await fn(proxy);
366
- const { operations } = await txContext.commit();
367
- for (const op2 of operations) {
368
- store.getSubscriptionManager().notify(op2.collection, op2);
369
- emitter.emit({ type: "operation:created", operation: op2 });
1319
+ });
1320
+ };
1321
+ }
1322
+
1323
+ // src/validate-config.ts
1324
+ var import_core5 = require("@korajs/core");
1325
+ function validateCreateAppConfig(config) {
1326
+ if (!config.schema) {
1327
+ throw new import_core5.SchemaValidationError("createApp requires a schema.", {
1328
+ fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
1329
+ });
1330
+ }
1331
+ if (config.schema.version < 1) {
1332
+ throw new import_core5.SchemaValidationError("Schema version must be at least 1.", {
1333
+ version: config.schema.version
1334
+ });
1335
+ }
1336
+ const collectionNames = Object.keys(config.schema.collections);
1337
+ if (collectionNames.length === 0) {
1338
+ throw new import_core5.SchemaValidationError("Schema must define at least one collection.", {
1339
+ fix: "Add entries under collections in defineSchema()."
1340
+ });
1341
+ }
1342
+ if (config.sync) {
1343
+ validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
1344
+ }
1345
+ const adapter = config.store?.adapter ?? detectAdapterType();
1346
+ const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
1347
+ if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
1348
+ throw new import_core5.KoraError(
1349
+ "Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
1350
+ "MISSING_WORKER_URL",
1351
+ {
1352
+ adapter,
1353
+ fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
370
1354
  }
371
- return operations;
372
- } catch (error) {
373
- txContext.rollback();
374
- throw error;
375
- }
1355
+ );
376
1356
  }
377
- const sequences = {
378
- async next(name, config2) {
379
- await ready;
380
- if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
381
- return store.getSequenceManager().next(name, config2);
382
- },
383
- async current(name, config2) {
384
- await ready;
385
- if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
386
- return store.getSequenceManager().current(name, config2);
387
- },
388
- async reset(name, config2) {
389
- await ready;
390
- if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
391
- return store.getSequenceManager().reset(name, config2);
1357
+ }
1358
+ function validateSyncUrl(url, transport) {
1359
+ if (!url || url.trim().length === 0) {
1360
+ throw new import_core5.KoraError("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
1361
+ fix: 'Pass sync: { url: "wss://your-server/kora" }.'
1362
+ });
1363
+ }
1364
+ try {
1365
+ const parsed = new URL(url);
1366
+ if (transport === "http") {
1367
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1368
+ throw new Error("bad protocol");
1369
+ }
1370
+ } else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
1371
+ throw new Error("bad protocol");
392
1372
  }
1373
+ } catch {
1374
+ throw new import_core5.KoraError(
1375
+ `Invalid sync URL "${url}" for transport "${transport}".`,
1376
+ "INVALID_SYNC_URL",
1377
+ {
1378
+ url,
1379
+ transport,
1380
+ fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
1381
+ }
1382
+ );
1383
+ }
1384
+ }
1385
+
1386
+ // src/wire-sync-event-forwarding.ts
1387
+ var SYNC_EVENT_TYPES = [
1388
+ "sync:connected",
1389
+ "sync:disconnected",
1390
+ "sync:schema-mismatch",
1391
+ "sync:auth-failed",
1392
+ "sync:sent",
1393
+ "sync:received",
1394
+ "sync:acknowledged",
1395
+ "sync:apply-failed",
1396
+ "sync:diagnostics",
1397
+ "sync:bandwidth",
1398
+ "sync:initial-sync-progress"
1399
+ ];
1400
+ function wireSyncEventForwarding(emitter, handler) {
1401
+ for (const type of SYNC_EVENT_TYPES) {
1402
+ emitter.on(type, (event) => {
1403
+ handler(event);
1404
+ });
1405
+ }
1406
+ }
1407
+
1408
+ // src/create-app.ts
1409
+ function createApp(config) {
1410
+ validateCreateAppConfig(config);
1411
+ const emitter = new import_internal5.SimpleEventEmitter();
1412
+ const mergeEngine = new import_merge2.MergeEngine();
1413
+ if (config.onSyncEvent) {
1414
+ wireSyncEventForwarding(emitter, config.onSyncEvent);
1415
+ }
1416
+ let store = null;
1417
+ let unsubscribeSync = null;
1418
+ let unsubscribeAudit = null;
1419
+ const syncState = {
1420
+ syncEngine: null,
1421
+ syncStatusBridge: null,
1422
+ authSyncCoordinator: null,
1423
+ reconnectionManager: null,
1424
+ connectionMonitor: null,
1425
+ qualityInterval: null,
1426
+ intentionalDisconnect: false,
1427
+ removeOnlineListener: null
393
1428
  };
1429
+ const devtools = setupDevtools(config, emitter);
1430
+ const queryStoreCache = new import_store5.QueryStoreCache(config.store?.name ?? "kora-db");
1431
+ const ready = initializeApp(config, emitter, mergeEngine).then((init) => {
1432
+ store = init.store;
1433
+ unsubscribeSync = init.unsubscribeSync;
1434
+ unsubscribeAudit = init.unsubscribeAudit;
1435
+ wireSyncLifecycleAfterReady(config, emitter, syncState, init);
1436
+ });
1437
+ const getStore = () => store;
1438
+ const executeTransaction = createTransactionExecutor(config, ready, getStore);
394
1439
  const app = {
395
1440
  ready,
396
1441
  events: emitter,
397
- sync: syncControl,
398
- sequences,
1442
+ sync: createSyncControl({ config, ready, state: syncState }),
1443
+ sequences: createSequencesAccessor(ready, getStore),
399
1444
  getStore() {
400
1445
  if (!store) {
401
1446
  throw new Error("Store not initialized. Await app.ready before accessing the store.");
@@ -403,45 +1448,75 @@ function createApp(config) {
403
1448
  return store;
404
1449
  },
405
1450
  getSyncEngine() {
406
- return syncEngine;
1451
+ return syncState.syncEngine;
1452
+ },
1453
+ getQueryStoreCache() {
1454
+ return queryStoreCache;
407
1455
  },
408
- async transaction(fn) {
1456
+ transaction(fn) {
409
1457
  return executeTransaction(fn);
410
1458
  },
411
- async mutation(name, fn) {
1459
+ mutation(name, fn) {
412
1460
  return executeTransaction(fn, name);
413
1461
  },
414
1462
  async close() {
415
1463
  await ready;
416
- intentionalDisconnect = true;
417
- if (qualityInterval !== null) {
418
- clearInterval(qualityInterval);
419
- qualityInterval = null;
420
- }
421
- reconnectionManager?.stop();
422
- if (instrumenter) {
423
- instrumenter.destroy();
424
- instrumenter = null;
425
- }
1464
+ syncState.intentionalDisconnect = true;
1465
+ teardownSyncLifecycle(syncState);
1466
+ devtools.destroyOverlay?.();
1467
+ devtools.instrumenter?.destroy();
426
1468
  if (unsubscribeSync) {
427
1469
  unsubscribeSync();
428
1470
  unsubscribeSync = null;
429
1471
  }
430
- if (syncEngine) {
431
- await syncEngine.stop();
432
- syncEngine = null;
1472
+ if (unsubscribeAudit) {
1473
+ unsubscribeAudit();
1474
+ unsubscribeAudit = null;
1475
+ }
1476
+ if (syncState.syncEngine) {
1477
+ await syncState.syncEngine.stop();
1478
+ syncState.syncEngine = null;
433
1479
  }
1480
+ queryStoreCache.clear();
434
1481
  if (store) {
435
1482
  await store.close();
436
1483
  store = null;
437
1484
  }
438
1485
  emitter.clear();
1486
+ },
1487
+ async exportBackup(options) {
1488
+ await ready;
1489
+ if (!store) {
1490
+ throw new Error("Store not initialized. Await app.ready before exporting backup.");
1491
+ }
1492
+ return store.exportBackup(options);
1493
+ },
1494
+ async importBackup(data, options) {
1495
+ await ready;
1496
+ if (!store) {
1497
+ throw new Error("Store not initialized. Await app.ready before importing backup.");
1498
+ }
1499
+ return store.importBackup(data, options);
1500
+ },
1501
+ async replayTo(operationId) {
1502
+ await ready;
1503
+ if (!store) {
1504
+ throw new Error("Store not initialized. Await app.ready before replaying operations.");
1505
+ }
1506
+ return store.replayTo(operationId);
1507
+ },
1508
+ async exportAudit(options) {
1509
+ await ready;
1510
+ if (!store) {
1511
+ throw new Error("Store not initialized. Await app.ready before exporting audit data.");
1512
+ }
1513
+ return store.exportAudit(options);
439
1514
  }
440
1515
  };
441
1516
  for (const collectionName of Object.keys(config.schema.collections)) {
442
1517
  Object.defineProperty(app, collectionName, {
443
1518
  get() {
444
- return createCollectionAccessor(collectionName, () => store);
1519
+ return createCollectionAccessor(collectionName, getStore);
445
1520
  },
446
1521
  enumerable: true,
447
1522
  configurable: false
@@ -449,136 +1524,23 @@ function createApp(config) {
449
1524
  }
450
1525
  return app;
451
1526
  }
452
- async function initializeAsync(config, emitter, mergeEngine) {
453
- const adapterType = config.store?.adapter ?? detectAdapterType();
454
- const dbName = config.store?.name ?? "kora-db";
455
- const adapter = await createAdapter(adapterType, dbName, config.store?.workerUrl);
456
- const store = new import_store.Store({
457
- schema: config.schema,
458
- adapter,
459
- emitter
460
- });
461
- await store.open();
462
- let syncEngine = null;
463
- let unsubscribeSync = null;
464
- if (config.sync) {
465
- const transport = new import_sync.WebSocketTransport();
466
- const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter);
467
- const scopeMap = config.sync.scope ? (0, import_core.buildScopeMap)(config.schema, config.sync.scope) : void 0;
468
- syncEngine = new import_sync.SyncEngine({
469
- transport,
470
- store: mergeAwareStore,
471
- config: {
472
- url: config.sync.url,
473
- transport: config.sync.transport,
474
- auth: config.sync.auth,
475
- batchSize: config.sync.batchSize,
476
- schemaVersion: config.sync.schemaVersion ?? config.schema.version,
477
- scopeMap
478
- },
479
- emitter
480
- });
481
- unsubscribeSync = emitter.on("operation:created", (event) => {
482
- if (syncEngine) {
483
- syncEngine.pushOperation(event.operation);
484
- }
485
- });
486
- }
487
- return { store, syncEngine, unsubscribeSync };
488
- }
489
- function createCollectionAccessor(collectionName, getStore) {
490
- return {
491
- async insert(data) {
492
- const currentStore = getStore();
493
- if (!currentStore) {
494
- throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
495
- }
496
- return currentStore.collection(collectionName).insert(data);
497
- },
498
- async findById(id) {
499
- const currentStore = getStore();
500
- if (!currentStore) return null;
501
- return currentStore.collection(collectionName).findById(id);
502
- },
503
- async update(id, data) {
504
- const currentStore = getStore();
505
- if (!currentStore) {
506
- throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
507
- }
508
- return currentStore.collection(collectionName).update(id, data);
509
- },
510
- async delete(id) {
511
- const currentStore = getStore();
512
- if (!currentStore) {
513
- throw new Error(`Cannot mutate collection "${collectionName}" before app.ready resolves.`);
514
- }
515
- return currentStore.collection(collectionName).delete(id);
516
- },
517
- where(conditions) {
518
- const currentStore = getStore();
519
- if (!currentStore) {
520
- return createPendingQueryBuilder(conditions);
521
- }
522
- return currentStore.collection(collectionName).where(conditions);
523
- }
524
- };
525
- }
526
- function createPendingQueryBuilder(initialWhere) {
527
- const descriptor = {
528
- collection: "__pending__",
529
- where: { ...initialWhere },
530
- orderBy: [],
531
- limit: void 0,
532
- offset: void 0
533
- };
534
- const builder = {
535
- where(conditions) {
536
- descriptor.where = { ...descriptor.where, ...conditions };
537
- return this;
538
- },
539
- orderBy(field, direction = "asc") {
540
- descriptor.orderBy.push({ field, direction });
541
- return this;
542
- },
543
- limit(n) {
544
- descriptor.limit = n;
545
- return this;
546
- },
547
- offset(n) {
548
- descriptor.offset = n;
549
- return this;
550
- },
551
- async exec() {
552
- return [];
553
- },
554
- async count() {
555
- return 0;
556
- },
557
- subscribe(callback) {
558
- void callback([]);
559
- return () => {
560
- };
561
- },
562
- getDescriptor() {
563
- return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
564
- }
565
- };
566
- return builder;
567
- }
568
1527
 
569
1528
  // src/index.ts
570
- var import_core2 = require("@korajs/core");
571
- var import_core3 = require("@korajs/core");
572
- var import_core4 = require("@korajs/core");
573
- var import_core5 = require("@korajs/core");
1529
+ var import_store6 = require("@korajs/store");
574
1530
  var import_core6 = require("@korajs/core");
575
1531
  var import_core7 = require("@korajs/core");
576
- var import_store2 = require("@korajs/store");
577
- var import_store3 = require("@korajs/store");
578
- var import_merge2 = require("@korajs/merge");
579
- var import_sync2 = require("@korajs/sync");
1532
+ var import_core8 = require("@korajs/core");
1533
+ var import_core9 = require("@korajs/core");
1534
+ var import_core10 = require("@korajs/core");
1535
+ var import_core11 = require("@korajs/core");
1536
+ var import_store7 = require("@korajs/store");
1537
+ var import_store8 = require("@korajs/store");
1538
+ var import_store9 = require("@korajs/store");
1539
+ var import_merge3 = require("@korajs/merge");
1540
+ var import_sync7 = require("@korajs/sync");
580
1541
  // Annotate the CommonJS export names for ESM import in node:
581
1542
  0 && (module.exports = {
1543
+ AppNotReadyError,
582
1544
  HybridLogicalClock,
583
1545
  KoraError,
584
1546
  MergeEngine,
@@ -589,10 +1551,17 @@ var import_sync2 = require("@korajs/sync");
589
1551
  WebSocketTransport,
590
1552
  createApp,
591
1553
  createOperation,
1554
+ decodeAuditExport,
592
1555
  defineSchema,
1556
+ exportBackup,
593
1557
  generateUUIDv7,
594
1558
  migrate,
595
1559
  op,
596
- t
1560
+ readAuditExportManifest,
1561
+ readBackupManifest,
1562
+ restoreBackup,
1563
+ t,
1564
+ verifyAuditExportChecksum,
1565
+ verifyBackupChecksum
597
1566
  });
598
1567
  //# sourceMappingURL=index.cjs.map