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