korajs 0.3.5 → 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.js CHANGED
@@ -1,11 +1,30 @@
1
+ import {
2
+ ApplyPipeline,
3
+ MergeAwareSyncStore,
4
+ StoreQueueStorage,
5
+ StoreSyncStatePersistence
6
+ } from "./chunk-WALHLVVF.js";
7
+
1
8
  // src/create-app.ts
9
+ import { buildScopeMap } from "@korajs/core";
2
10
  import { SimpleEventEmitter } from "@korajs/core/internal";
3
11
  import { Instrumenter } from "@korajs/devtools";
4
12
  import { MergeEngine } from "@korajs/merge";
5
13
  import { Store } from "@korajs/store";
6
- import { ConnectionMonitor, ReconnectionManager, SyncEngine, WebSocketTransport } from "@korajs/sync";
14
+ import {
15
+ ConnectionMonitor,
16
+ HttpLongPollingTransport,
17
+ ReconnectionManager,
18
+ SyncEncryptor,
19
+ SyncEngine,
20
+ WebSocketTransport
21
+ } from "@korajs/sync";
7
22
 
8
23
  // src/adapter-resolver.ts
24
+ function importOptionalPeer(specifier) {
25
+ const dynamicImport = new Function("specifier", "return import(specifier)");
26
+ return dynamicImport(specifier);
27
+ }
9
28
  function detectAdapterType() {
10
29
  if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
11
30
  return "tauri-sqlite";
@@ -21,13 +40,10 @@ function detectAdapterType() {
21
40
  }
22
41
  return "better-sqlite3";
23
42
  }
24
- async function createAdapter(type, dbName, workerUrl) {
43
+ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
25
44
  switch (type) {
26
45
  case "tauri-sqlite": {
27
- const { TauriSqliteAdapter } = await import(
28
- /* @vite-ignore */
29
- "@korajs/tauri"
30
- );
46
+ const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
31
47
  return new TauriSqliteAdapter({ path: `${dbName}.db` });
32
48
  }
33
49
  case "better-sqlite3": {
@@ -39,11 +55,11 @@ async function createAdapter(type, dbName, workerUrl) {
39
55
  }
40
56
  case "sqlite-wasm": {
41
57
  const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
42
- return new SqliteWasmAdapter({ dbName, workerUrl });
58
+ return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
43
59
  }
44
60
  case "indexeddb": {
45
61
  const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
46
- return new IndexedDbAdapter({ dbName, workerUrl });
62
+ return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
47
63
  }
48
64
  default: {
49
65
  const _exhaustive = type;
@@ -52,133 +68,289 @@ async function createAdapter(type, dbName, workerUrl) {
52
68
  }
53
69
  }
54
70
 
55
- // src/merge-aware-sync-store.ts
56
- var MergeAwareSyncStore = class {
57
- constructor(store, mergeEngine, emitter) {
58
- this.store = store;
59
- this.mergeEngine = mergeEngine;
60
- this.emitter = emitter;
61
- }
62
- store;
63
- mergeEngine;
64
- emitter;
65
- getVersionVector() {
66
- return this.store.getVersionVector();
71
+ // src/app-not-ready-error.ts
72
+ import { KoraError } from "@korajs/core";
73
+ var AppNotReadyError = class extends KoraError {
74
+ constructor(detail) {
75
+ super(detail, "APP_NOT_READY", {
76
+ fix: "Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery()."
77
+ });
78
+ this.name = "AppNotReadyError";
67
79
  }
68
- getNodeId() {
69
- return this.store.getNodeId();
80
+ };
81
+
82
+ // src/audit-bridge.ts
83
+ import { persistedAuditTraceFromEvent } from "@korajs/store";
84
+ var AUDIT_EVENT_TYPES = ["merge:completed", "merge:conflict", "constraint:violated"];
85
+ function isAuditEvent(event) {
86
+ return AUDIT_EVENT_TYPES.includes(event.type);
87
+ }
88
+ function wireAuditPersistence(store, emitter) {
89
+ const unsubscribers = AUDIT_EVENT_TYPES.map(
90
+ (eventType) => emitter.on(eventType, (event) => {
91
+ if (!isAuditEvent(event)) {
92
+ return;
93
+ }
94
+ const trace = persistedAuditTraceFromEvent(event);
95
+ void store.appendAuditTrace(trace).catch(() => {
96
+ });
97
+ })
98
+ );
99
+ return () => {
100
+ for (const unsub of unsubscribers) {
101
+ unsub();
102
+ }
103
+ };
104
+ }
105
+
106
+ // src/sync-query-bridge.ts
107
+ function queryDescriptorToSyncSubset(descriptor) {
108
+ const where = {};
109
+ for (const [field, value] of Object.entries(descriptor.where)) {
110
+ if (value === null || value === void 0) {
111
+ where[field] = value;
112
+ continue;
113
+ }
114
+ if (typeof value !== "object" || Array.isArray(value)) {
115
+ where[field] = value;
116
+ }
70
117
  }
71
- async getOperationRange(nodeId, fromSeq, toSeq) {
72
- return this.store.getOperationRange(nodeId, fromSeq, toSeq);
118
+ if (Object.keys(where).length === 0) {
119
+ return null;
73
120
  }
74
- async applyRemoteOperation(op) {
75
- if (op.type !== "update" || !op.data || !op.previousData) {
76
- return this.store.applyRemoteOperation(op);
121
+ return {
122
+ collection: descriptor.collection,
123
+ where
124
+ };
125
+ }
126
+ function createSyncQuerySubscriptionHook(getSyncEngine) {
127
+ return (descriptor) => {
128
+ const subset = queryDescriptorToSyncSubset(descriptor);
129
+ if (!subset) {
130
+ return () => {
131
+ };
77
132
  }
78
- const schema = this.store.getSchema();
79
- const collectionDef = schema.collections[op.collection];
80
- if (!collectionDef) {
81
- return this.store.applyRemoteOperation(op);
133
+ const engine = getSyncEngine();
134
+ if (!engine) {
135
+ return () => {
136
+ };
82
137
  }
83
- const accessor = this.store.collection(op.collection);
84
- const currentRecord = await accessor.findById(op.recordId);
85
- if (!currentRecord) {
86
- return this.store.applyRemoteOperation(op);
138
+ return engine.registerQuerySubset(subset);
139
+ };
140
+ }
141
+
142
+ // src/sync-status-bridge.ts
143
+ var OFFLINE_STATUS = Object.freeze({
144
+ status: "offline",
145
+ pendingOperations: 0,
146
+ lastSyncedAt: null,
147
+ lastSuccessfulPush: null,
148
+ lastSuccessfulPull: null,
149
+ conflicts: 0
150
+ });
151
+ var SYNC_STATUS_EVENT_TYPES = [
152
+ "sync:connected",
153
+ "sync:disconnected",
154
+ "sync:schema-mismatch",
155
+ "sync:auth-failed",
156
+ "sync:sent",
157
+ "sync:received",
158
+ "sync:acknowledged",
159
+ "sync:apply-failed",
160
+ "sync:diagnostics",
161
+ "sync:initial-sync-progress"
162
+ ];
163
+ function createSyncStatusBridge(emitter, getSyncEngine) {
164
+ let currentStatus = OFFLINE_STATUS;
165
+ const listeners = /* @__PURE__ */ new Set();
166
+ const refresh = () => {
167
+ const engine = getSyncEngine();
168
+ const next = engine ? engine.getStatus() : OFFLINE_STATUS;
169
+ const prevSerialized = JSON.stringify(currentStatus);
170
+ const nextSerialized = JSON.stringify(next);
171
+ if (prevSerialized === nextSerialized) {
172
+ return;
87
173
  }
88
- let hasConflict = false;
89
- for (const field of Object.keys(op.data)) {
90
- const expectedBase = op.previousData[field];
91
- const currentLocal = currentRecord[field];
92
- if (!deepEqual(expectedBase, currentLocal)) {
93
- hasConflict = true;
94
- break;
95
- }
174
+ currentStatus = next;
175
+ for (const listener of listeners) {
176
+ listener(currentStatus);
96
177
  }
97
- if (!hasConflict) {
98
- return this.store.applyRemoteOperation(op);
178
+ };
179
+ const unsubs = [];
180
+ for (const type of SYNC_STATUS_EVENT_TYPES) {
181
+ unsubs.push(emitter.on(type, refresh));
182
+ }
183
+ const bridge = {
184
+ get status() {
185
+ return currentStatus;
186
+ },
187
+ subscribe(listener) {
188
+ listeners.add(listener);
189
+ listener(currentStatus);
190
+ return () => {
191
+ listeners.delete(listener);
192
+ };
193
+ },
194
+ refresh,
195
+ destroy() {
196
+ for (const unsub of unsubs) {
197
+ unsub();
198
+ }
199
+ unsubs.length = 0;
200
+ listeners.clear();
99
201
  }
100
- this.emitter?.emit({
101
- type: "merge:started",
102
- operationA: op,
103
- operationB: op
202
+ };
203
+ refresh();
204
+ return bridge;
205
+ }
206
+
207
+ // src/validate-config.ts
208
+ import { KoraError as KoraError2, SchemaValidationError } from "@korajs/core";
209
+ function validateCreateAppConfig(config) {
210
+ if (!config.schema) {
211
+ throw new SchemaValidationError("createApp requires a schema.", {
212
+ fix: "Pass schema: defineSchema({ version: 1, collections: { ... } })"
104
213
  });
105
- const baseState = { ...op.previousData };
106
- const localOp = {
107
- ...op,
108
- // The "local" operation's data is the diff between base and current local state
109
- data: buildLocalDiff(op.previousData, currentRecord, Object.keys(op.data)),
110
- previousData: op.previousData,
111
- nodeId: this.store.getNodeId()
112
- };
113
- const input = {
114
- local: localOp,
115
- remote: op,
116
- baseState,
117
- collectionDef
118
- };
119
- const result = this.mergeEngine.mergeFields(input);
120
- for (const trace of result.traces) {
121
- this.emitter?.emit({ type: "merge:conflict", trace });
122
- }
123
- const firstTrace = result.traces[0];
124
- if (firstTrace) {
125
- this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
126
- }
127
- const mergedOp = {
128
- ...op,
129
- data: result.mergedData
130
- };
131
- return this.store.applyRemoteOperation(mergedOp);
132
214
  }
133
- };
134
- function buildLocalDiff(baseState, currentRecord, fields) {
135
- const diff = {};
136
- for (const field of fields) {
137
- diff[field] = currentRecord[field];
215
+ if (config.schema.version < 1) {
216
+ throw new SchemaValidationError("Schema version must be at least 1.", {
217
+ version: config.schema.version
218
+ });
219
+ }
220
+ const collectionNames = Object.keys(config.schema.collections);
221
+ if (collectionNames.length === 0) {
222
+ throw new SchemaValidationError("Schema must define at least one collection.", {
223
+ fix: "Add entries under collections in defineSchema()."
224
+ });
225
+ }
226
+ if (config.sync) {
227
+ validateSyncUrl(config.sync.url, config.sync.transport ?? "websocket");
228
+ }
229
+ const adapter = config.store?.adapter ?? detectAdapterType();
230
+ const isBrowser = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
231
+ if (isBrowser && (adapter === "sqlite-wasm" || adapter === "indexeddb") && !config.store?.workerUrl) {
232
+ throw new KoraError2(
233
+ "Browser storage requires store.workerUrl pointing to the SQLite WASM worker script.",
234
+ "MISSING_WORKER_URL",
235
+ {
236
+ adapter,
237
+ fix: 'Add store: { workerUrl: "/sqlite-wasm-worker.js" } (see create-kora-app templates).'
238
+ }
239
+ );
138
240
  }
139
- return diff;
140
241
  }
141
- function deepEqual(a, b) {
142
- if (a === b) return true;
143
- if (a === null || b === null) return false;
144
- if (a === void 0 || b === void 0) return false;
145
- if (typeof a !== typeof b) return false;
146
- if (Array.isArray(a) && Array.isArray(b)) {
147
- if (a.length !== b.length) return false;
148
- return a.every((val, i) => deepEqual(val, b[i]));
242
+ function validateSyncUrl(url, transport) {
243
+ if (!url || url.trim().length === 0) {
244
+ throw new KoraError2("sync.url is required when sync is configured.", "INVALID_SYNC_URL", {
245
+ fix: 'Pass sync: { url: "wss://your-server/kora" }.'
246
+ });
149
247
  }
150
- if (typeof a === "object" && typeof b === "object") {
151
- const keysA = Object.keys(a);
152
- const keysB = Object.keys(b);
153
- if (keysA.length !== keysB.length) return false;
154
- return keysA.every(
155
- (key) => deepEqual(a[key], b[key])
248
+ try {
249
+ const parsed = new URL(url);
250
+ if (transport === "http") {
251
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
252
+ throw new Error("bad protocol");
253
+ }
254
+ } else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
255
+ throw new Error("bad protocol");
256
+ }
257
+ } catch {
258
+ throw new KoraError2(
259
+ `Invalid sync URL "${url}" for transport "${transport}".`,
260
+ "INVALID_SYNC_URL",
261
+ {
262
+ url,
263
+ transport,
264
+ fix: transport === "http" ? "Use an absolute http:// or https:// URL." : "Use an absolute ws:// or wss:// URL."
265
+ }
156
266
  );
157
267
  }
158
- return false;
159
268
  }
160
269
 
161
270
  // src/create-app.ts
162
271
  function createApp(config) {
272
+ validateCreateAppConfig(config);
163
273
  const emitter = new SimpleEventEmitter();
164
274
  const mergeEngine = new MergeEngine();
275
+ if (config.onSyncEvent) {
276
+ const handler = config.onSyncEvent;
277
+ const syncTypes = [
278
+ "sync:connected",
279
+ "sync:disconnected",
280
+ "sync:schema-mismatch",
281
+ "sync:auth-failed",
282
+ "sync:sent",
283
+ "sync:received",
284
+ "sync:acknowledged",
285
+ "sync:apply-failed",
286
+ "sync:diagnostics",
287
+ "sync:bandwidth",
288
+ "sync:initial-sync-progress"
289
+ ];
290
+ for (const type of syncTypes) {
291
+ emitter.on(type, (event) => {
292
+ handler(event);
293
+ });
294
+ }
295
+ }
165
296
  let store = null;
166
297
  let syncEngine = null;
167
298
  let unsubscribeSync = null;
299
+ let unsubscribeAudit = null;
168
300
  let reconnectionManager = null;
169
301
  let connectionMonitor = null;
170
302
  let instrumenter = null;
171
303
  let intentionalDisconnect = false;
172
304
  let qualityInterval = null;
305
+ let syncStatusBridge = null;
306
+ let destroyDevtoolsOverlay = null;
173
307
  if (config.devtools) {
174
308
  instrumenter = new Instrumenter(emitter, {
175
309
  bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
176
310
  });
311
+ if (typeof globalThis !== "undefined" && "window" in globalThis) {
312
+ void import("@korajs/devtools/overlay").then(({ mountKoraDevtoolsOverlay }) => {
313
+ if (instrumenter) {
314
+ destroyDevtoolsOverlay = mountKoraDevtoolsOverlay(instrumenter);
315
+ }
316
+ }).catch(() => {
317
+ });
318
+ }
177
319
  }
178
320
  const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
179
321
  store = result.store;
180
322
  syncEngine = result.syncEngine;
181
323
  unsubscribeSync = result.unsubscribeSync;
324
+ unsubscribeAudit = result.unsubscribeAudit;
325
+ const authBinding = result.authBinding;
326
+ if (config.sync) {
327
+ syncStatusBridge = createSyncStatusBridge(emitter, () => syncEngine);
328
+ syncStatusBridge.refresh();
329
+ }
330
+ if (config.sync && syncEngine && authBinding?.subscribe) {
331
+ authBinding.subscribe(() => {
332
+ const engine = syncEngine;
333
+ if (!engine) {
334
+ return;
335
+ }
336
+ void (async () => {
337
+ const headers = await authBinding.auth();
338
+ if (!headers.token) {
339
+ await engine.stop();
340
+ return;
341
+ }
342
+ if (authBinding.resolveScopeMap) {
343
+ const nextScope = await authBinding.resolveScopeMap();
344
+ engine.updateScope(nextScope);
345
+ }
346
+ const status = engine.getStatus().status;
347
+ if (status !== "offline") {
348
+ await engine.stop();
349
+ }
350
+ await engine.start();
351
+ })();
352
+ });
353
+ }
182
354
  if (config.sync && syncEngine) {
183
355
  connectionMonitor = new ConnectionMonitor();
184
356
  reconnectionManager = new ReconnectionManager({
@@ -203,10 +375,24 @@ function createApp(config) {
203
375
  qualityInterval = null;
204
376
  }
205
377
  });
378
+ const browserGlobal = globalThis;
379
+ if (typeof browserGlobal.addEventListener === "function") {
380
+ const engine = syncEngine;
381
+ browserGlobal.addEventListener("online", () => {
382
+ if (intentionalDisconnect || config.sync?.autoReconnect === false) return;
383
+ reconnectionManager?.wake();
384
+ reconnectionManager?.reset();
385
+ void engine.retryNow();
386
+ });
387
+ }
388
+ emitter.on("sync:schema-mismatch", () => {
389
+ reconnectionManager?.stop();
390
+ intentionalDisconnect = true;
391
+ });
206
392
  if (config.sync.autoReconnect !== false) {
207
393
  const engine = syncEngine;
208
394
  emitter.on("sync:disconnected", () => {
209
- if (intentionalDisconnect) return;
395
+ if (intentionalDisconnect || engine.isSchemaBlocked()) return;
210
396
  if (reconnectionManager?.isRunning()) return;
211
397
  engine.setReconnecting(true);
212
398
  reconnectionManager?.stop();
@@ -223,9 +409,32 @@ function createApp(config) {
223
409
  });
224
410
  });
225
411
  }
412
+ if (config.sync.autoConnect === true && syncEngine) {
413
+ void syncEngine.start().catch(() => {
414
+ });
415
+ }
226
416
  }
227
417
  });
418
+ const offlineSyncStatus = () => ({
419
+ status: "offline",
420
+ pendingOperations: 0,
421
+ lastSyncedAt: null,
422
+ lastSuccessfulPush: null,
423
+ lastSuccessfulPull: null,
424
+ conflicts: 0
425
+ });
228
426
  const syncControl = config.sync ? {
427
+ get status() {
428
+ return syncStatusBridge?.status ?? offlineSyncStatus();
429
+ },
430
+ subscribeStatus(listener) {
431
+ if (syncStatusBridge) {
432
+ return syncStatusBridge.subscribe(listener);
433
+ }
434
+ listener(offlineSyncStatus());
435
+ return () => {
436
+ };
437
+ },
229
438
  async connect() {
230
439
  await ready;
231
440
  if (syncEngine) {
@@ -233,6 +442,7 @@ function createApp(config) {
233
442
  reconnectionManager?.stop();
234
443
  reconnectionManager?.reset();
235
444
  await syncEngine.start();
445
+ syncStatusBridge?.refresh();
236
446
  }
237
447
  },
238
448
  async disconnect() {
@@ -241,19 +451,97 @@ function createApp(config) {
241
451
  intentionalDisconnect = true;
242
452
  reconnectionManager?.stop();
243
453
  await syncEngine.stop();
454
+ syncStatusBridge?.refresh();
244
455
  }
245
456
  },
246
457
  getStatus() {
247
458
  if (syncEngine) {
248
459
  return syncEngine.getStatus();
249
460
  }
250
- return { status: "offline", pendingOperations: 0, lastSyncedAt: null };
461
+ return offlineSyncStatus();
462
+ },
463
+ async retryNow() {
464
+ await ready;
465
+ if (syncEngine) {
466
+ await syncEngine.retryNow();
467
+ }
468
+ },
469
+ clearSchemaBlock() {
470
+ syncEngine?.clearSchemaBlock();
471
+ },
472
+ exportDiagnostics() {
473
+ if (syncEngine) {
474
+ return syncEngine.exportDiagnostics();
475
+ }
476
+ return {
477
+ state: "disconnected",
478
+ status: {
479
+ status: "offline",
480
+ pendingOperations: 0,
481
+ lastSyncedAt: null,
482
+ lastSuccessfulPush: null,
483
+ lastSuccessfulPull: null,
484
+ conflicts: 0
485
+ },
486
+ nodeId: "",
487
+ url: config.sync?.url ?? "",
488
+ schemaVersion: config.schema.version,
489
+ lastSyncedAt: null,
490
+ lastSuccessfulPush: null,
491
+ lastSuccessfulPull: null,
492
+ conflicts: 0,
493
+ pendingOperations: 0,
494
+ hasInFlightBatch: false,
495
+ reconnecting: false,
496
+ timestamp: Date.now()
497
+ };
251
498
  }
252
499
  } : null;
500
+ async function executeTransaction(fn, mutationName) {
501
+ await ready;
502
+ if (!store) {
503
+ throw new Error("Store not initialized. Await app.ready before using transactions.");
504
+ }
505
+ const collectionNames = Object.keys(config.schema.collections);
506
+ return store.transaction(async (tx) => {
507
+ if (mutationName !== void 0) {
508
+ tx.setMutationName(mutationName);
509
+ }
510
+ const proxy = {};
511
+ for (const name of collectionNames) {
512
+ Object.defineProperty(proxy, name, {
513
+ get() {
514
+ return tx.collection(name);
515
+ },
516
+ enumerable: true,
517
+ configurable: false
518
+ });
519
+ }
520
+ await fn(proxy);
521
+ });
522
+ }
523
+ const sequences = {
524
+ async next(name, config2) {
525
+ await ready;
526
+ if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
527
+ return store.getSequenceManager().next(name, config2);
528
+ },
529
+ async current(name, config2) {
530
+ await ready;
531
+ if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
532
+ return store.getSequenceManager().current(name, config2);
533
+ },
534
+ async reset(name, config2) {
535
+ await ready;
536
+ if (!store) throw new Error("Store not initialized. Await app.ready before using sequences.");
537
+ return store.getSequenceManager().reset(name, config2);
538
+ }
539
+ };
253
540
  const app = {
254
541
  ready,
255
542
  events: emitter,
256
543
  sync: syncControl,
544
+ sequences,
257
545
  getStore() {
258
546
  if (!store) {
259
547
  throw new Error("Store not initialized. Await app.ready before accessing the store.");
@@ -263,6 +551,12 @@ function createApp(config) {
263
551
  getSyncEngine() {
264
552
  return syncEngine;
265
553
  },
554
+ async transaction(fn) {
555
+ return executeTransaction(fn);
556
+ },
557
+ async mutation(name, fn) {
558
+ return executeTransaction(fn, name);
559
+ },
266
560
  async close() {
267
561
  await ready;
268
562
  intentionalDisconnect = true;
@@ -271,6 +565,10 @@ function createApp(config) {
271
565
  qualityInterval = null;
272
566
  }
273
567
  reconnectionManager?.stop();
568
+ if (destroyDevtoolsOverlay) {
569
+ destroyDevtoolsOverlay();
570
+ destroyDevtoolsOverlay = null;
571
+ }
274
572
  if (instrumenter) {
275
573
  instrumenter.destroy();
276
574
  instrumenter = null;
@@ -279,15 +577,51 @@ function createApp(config) {
279
577
  unsubscribeSync();
280
578
  unsubscribeSync = null;
281
579
  }
580
+ if (unsubscribeAudit) {
581
+ unsubscribeAudit();
582
+ unsubscribeAudit = null;
583
+ }
282
584
  if (syncEngine) {
283
585
  await syncEngine.stop();
284
586
  syncEngine = null;
285
587
  }
588
+ if (syncStatusBridge) {
589
+ syncStatusBridge.destroy();
590
+ syncStatusBridge = null;
591
+ }
286
592
  if (store) {
287
593
  await store.close();
288
594
  store = null;
289
595
  }
290
596
  emitter.clear();
597
+ },
598
+ async exportBackup(options) {
599
+ await ready;
600
+ if (!store) {
601
+ throw new Error("Store not initialized. Await app.ready before exporting backup.");
602
+ }
603
+ return store.exportBackup(options);
604
+ },
605
+ async importBackup(data, options) {
606
+ await ready;
607
+ if (!store) {
608
+ throw new Error("Store not initialized. Await app.ready before importing backup.");
609
+ }
610
+ return store.importBackup(data, options);
611
+ },
612
+ async replayTo(operationId) {
613
+ await ready;
614
+ if (!store) {
615
+ throw new Error("Store not initialized. Await app.ready before replaying operations.");
616
+ }
617
+ return store.replayTo(operationId);
618
+ },
619
+ async exportAudit(options) {
620
+ await ready;
621
+ if (!store) {
622
+ throw new Error("Store not initialized. Await app.ready before exporting audit data.");
623
+ }
624
+ return store.exportAudit(options);
291
625
  }
292
626
  };
293
627
  for (const collectionName of Object.keys(config.schema.collections)) {
@@ -304,37 +638,87 @@ function createApp(config) {
304
638
  async function initializeAsync(config, emitter, mergeEngine) {
305
639
  const adapterType = config.store?.adapter ?? detectAdapterType();
306
640
  const dbName = config.store?.name ?? "kora-db";
307
- const adapter = await createAdapter(adapterType, dbName, config.store?.workerUrl);
641
+ const adapter = await createAdapter(
642
+ adapterType,
643
+ dbName,
644
+ config.store?.workerUrl,
645
+ emitter,
646
+ config.store?.workerResponseTimeoutMs,
647
+ config.store?.sharedWorkerUrl
648
+ );
649
+ const authBinding = config.sync?.authClient;
650
+ const authNodeId = authBinding?.resolveNodeId ? await authBinding.resolveNodeId() : void 0;
651
+ let syncEngine = null;
308
652
  const store = new Store({
309
653
  schema: config.schema,
310
654
  adapter,
311
- emitter
655
+ emitter,
656
+ dbName,
657
+ nodeId: authNodeId,
658
+ isolation: authNodeId ? "shared" : config.store?.isolation,
659
+ ...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
312
660
  });
313
661
  await store.open();
314
- let syncEngine = null;
662
+ let recordConflict;
663
+ const applyPipeline = new ApplyPipeline({
664
+ store,
665
+ mergeEngine,
666
+ emitter,
667
+ onMergeConflict: () => recordConflict?.()
668
+ });
669
+ store.setLocalMutationHandler(applyPipeline);
670
+ const unsubscribeAudit = wireAuditPersistence(store, emitter);
315
671
  let unsubscribeSync = null;
316
672
  if (config.sync) {
317
- const transport = new WebSocketTransport();
318
- const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter);
673
+ const transport = createSyncTransport(config.sync);
674
+ const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter, {
675
+ onMergeConflict: () => recordConflict?.()
676
+ });
677
+ let scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
678
+ if (authBinding?.resolveScopeMap) {
679
+ scopeMap = await authBinding.resolveScopeMap() ?? scopeMap;
680
+ }
681
+ const syncAuth = authBinding?.auth ?? config.sync.auth;
682
+ const encryptor = config.sync.encryption?.enabled === true ? await SyncEncryptor.create(config.sync.encryption) : void 0;
319
683
  syncEngine = new SyncEngine({
320
684
  transport,
321
685
  store: mergeAwareStore,
322
686
  config: {
323
687
  url: config.sync.url,
324
688
  transport: config.sync.transport,
325
- auth: config.sync.auth,
689
+ auth: syncAuth,
326
690
  batchSize: config.sync.batchSize,
327
- schemaVersion: config.sync.schemaVersion ?? config.schema.version
691
+ schemaVersion: config.sync.schemaVersion ?? config.schema.version,
692
+ scopeMap,
693
+ encryption: config.sync.encryption,
694
+ strictHandshake: config.sync.strictHandshake,
695
+ operationTransforms: config.sync.operationTransforms
328
696
  },
329
- emitter
697
+ emitter,
698
+ queueStorage: new StoreQueueStorage(adapter),
699
+ syncState: new StoreSyncStatePersistence(store, scopeMap),
700
+ encryptor
330
701
  });
702
+ recordConflict = () => syncEngine?.recordConflict();
331
703
  unsubscribeSync = emitter.on("operation:created", (event) => {
332
704
  if (syncEngine) {
333
705
  syncEngine.pushOperation(event.operation);
334
706
  }
335
707
  });
336
708
  }
337
- return { store, syncEngine, unsubscribeSync };
709
+ return {
710
+ store,
711
+ syncEngine,
712
+ unsubscribeSync,
713
+ unsubscribeAudit,
714
+ authBinding: config.sync?.authClient ?? null
715
+ };
716
+ }
717
+ function createSyncTransport(sync) {
718
+ if (sync.transport === "http") {
719
+ return new HttpLongPollingTransport();
720
+ }
721
+ return new WebSocketTransport();
338
722
  }
339
723
  function createCollectionAccessor(collectionName, getStore) {
340
724
  return {
@@ -399,15 +783,19 @@ function createPendingQueryBuilder(initialWhere) {
399
783
  return this;
400
784
  },
401
785
  async exec() {
402
- return [];
786
+ throw new AppNotReadyError(
787
+ "Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
788
+ );
403
789
  },
404
790
  async count() {
405
- return 0;
791
+ throw new AppNotReadyError(
792
+ "Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
793
+ );
406
794
  },
407
- subscribe(callback) {
408
- void callback([]);
409
- return () => {
410
- };
795
+ subscribe(_callback) {
796
+ throw new AppNotReadyError(
797
+ "Cannot subscribe to a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
798
+ );
411
799
  },
412
800
  getDescriptor() {
413
801
  return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
@@ -417,25 +805,49 @@ function createPendingQueryBuilder(initialWhere) {
417
805
  }
418
806
 
419
807
  // src/index.ts
420
- import { defineSchema, t } from "@korajs/core";
808
+ import {
809
+ decodeAuditExport,
810
+ readAuditExportManifest,
811
+ verifyAuditExportChecksum
812
+ } from "@korajs/store";
813
+ import { defineSchema, migrate, t } from "@korajs/core";
421
814
  import { HybridLogicalClock } from "@korajs/core";
422
815
  import { generateUUIDv7 } from "@korajs/core";
423
816
  import { createOperation } from "@korajs/core";
424
- import { KoraError } from "@korajs/core";
425
- import { Store as Store2 } from "@korajs/store";
817
+ import { KoraError as KoraError3 } from "@korajs/core";
818
+ import { op } from "@korajs/core";
819
+ import { SequenceManager, Store as Store2 } from "@korajs/store";
820
+ import { TransactionContext } from "@korajs/store";
821
+ import {
822
+ exportBackup,
823
+ readBackupManifest,
824
+ restoreBackup,
825
+ verifyBackupChecksum
826
+ } from "@korajs/store";
426
827
  import { MergeEngine as MergeEngine2 } from "@korajs/merge";
427
828
  import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
428
829
  export {
429
830
  HybridLogicalClock,
430
- KoraError,
831
+ KoraError3 as KoraError,
431
832
  MergeEngine2 as MergeEngine,
833
+ SequenceManager,
432
834
  Store2 as Store,
433
835
  SyncEngine2 as SyncEngine,
836
+ TransactionContext,
434
837
  WebSocketTransport2 as WebSocketTransport,
435
838
  createApp,
436
839
  createOperation,
840
+ decodeAuditExport,
437
841
  defineSchema,
842
+ exportBackup,
438
843
  generateUUIDv7,
439
- t
844
+ migrate,
845
+ op,
846
+ readAuditExportManifest,
847
+ readBackupManifest,
848
+ restoreBackup,
849
+ t,
850
+ verifyAuditExportChecksum,
851
+ verifyBackupChecksum
440
852
  };
441
853
  //# sourceMappingURL=index.js.map