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.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ ApplyPipeline,
3
+ MergeAwareSyncStore,
4
+ StoreQueueStorage,
5
+ StoreSyncStatePersistence
6
+ } from "./chunk-WALHLVVF.js";
7
+
1
8
  // src/create-app.ts
2
9
  import { buildScopeMap } from "@korajs/core";
3
10
  import { SimpleEventEmitter } from "@korajs/core/internal";
@@ -6,12 +13,18 @@ import { MergeEngine } from "@korajs/merge";
6
13
  import { Store } from "@korajs/store";
7
14
  import {
8
15
  ConnectionMonitor,
16
+ HttpLongPollingTransport,
9
17
  ReconnectionManager,
18
+ SyncEncryptor,
10
19
  SyncEngine,
11
20
  WebSocketTransport
12
21
  } from "@korajs/sync";
13
22
 
14
23
  // src/adapter-resolver.ts
24
+ function importOptionalPeer(specifier) {
25
+ const dynamicImport = new Function("specifier", "return import(specifier)");
26
+ return dynamicImport(specifier);
27
+ }
15
28
  function detectAdapterType() {
16
29
  if (typeof globalThis !== "undefined" && typeof globalThis.__TAURI_INTERNALS__ !== "undefined") {
17
30
  return "tauri-sqlite";
@@ -27,13 +40,10 @@ function detectAdapterType() {
27
40
  }
28
41
  return "better-sqlite3";
29
42
  }
30
- async function createAdapter(type, dbName, workerUrl) {
43
+ async function createAdapter(type, dbName, workerUrl, emitter, workerResponseTimeoutMs, sharedWorkerUrl) {
31
44
  switch (type) {
32
45
  case "tauri-sqlite": {
33
- const { TauriSqliteAdapter } = await import(
34
- /* @vite-ignore */
35
- "@korajs/tauri"
36
- );
46
+ const { TauriSqliteAdapter } = await importOptionalPeer("@korajs/tauri");
37
47
  return new TauriSqliteAdapter({ path: `${dbName}.db` });
38
48
  }
39
49
  case "better-sqlite3": {
@@ -45,11 +55,11 @@ async function createAdapter(type, dbName, workerUrl) {
45
55
  }
46
56
  case "sqlite-wasm": {
47
57
  const { SqliteWasmAdapter } = await import("@korajs/store/sqlite-wasm");
48
- return new SqliteWasmAdapter({ dbName, workerUrl });
58
+ return new SqliteWasmAdapter({ dbName, workerUrl, sharedWorkerUrl, workerResponseTimeoutMs });
49
59
  }
50
60
  case "indexeddb": {
51
61
  const { IndexedDbAdapter } = await import("@korajs/store/indexeddb");
52
- return new IndexedDbAdapter({ dbName, workerUrl });
62
+ return new IndexedDbAdapter({ dbName, workerUrl, emitter, workerResponseTimeoutMs });
53
63
  }
54
64
  default: {
55
65
  const _exhaustive = type;
@@ -58,133 +68,289 @@ async function createAdapter(type, dbName, workerUrl) {
58
68
  }
59
69
  }
60
70
 
61
- // src/merge-aware-sync-store.ts
62
- var MergeAwareSyncStore = class {
63
- constructor(store, mergeEngine, emitter) {
64
- this.store = store;
65
- this.mergeEngine = mergeEngine;
66
- this.emitter = emitter;
67
- }
68
- store;
69
- mergeEngine;
70
- emitter;
71
- getVersionVector() {
72
- 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";
73
79
  }
74
- getNodeId() {
75
- 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
+ }
76
117
  }
77
- async getOperationRange(nodeId, fromSeq, toSeq) {
78
- return this.store.getOperationRange(nodeId, fromSeq, toSeq);
118
+ if (Object.keys(where).length === 0) {
119
+ return null;
79
120
  }
80
- async applyRemoteOperation(op2) {
81
- if (op2.type !== "update" || !op2.data || !op2.previousData) {
82
- return this.store.applyRemoteOperation(op2);
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
+ };
83
132
  }
84
- const schema = this.store.getSchema();
85
- const collectionDef = schema.collections[op2.collection];
86
- if (!collectionDef) {
87
- return this.store.applyRemoteOperation(op2);
133
+ const engine = getSyncEngine();
134
+ if (!engine) {
135
+ return () => {
136
+ };
88
137
  }
89
- const accessor = this.store.collection(op2.collection);
90
- const currentRecord = await accessor.findById(op2.recordId);
91
- if (!currentRecord) {
92
- return this.store.applyRemoteOperation(op2);
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;
93
173
  }
94
- let hasConflict = false;
95
- for (const field of Object.keys(op2.data)) {
96
- const expectedBase = op2.previousData[field];
97
- const currentLocal = currentRecord[field];
98
- if (!deepEqual(expectedBase, currentLocal)) {
99
- hasConflict = true;
100
- break;
101
- }
174
+ currentStatus = next;
175
+ for (const listener of listeners) {
176
+ listener(currentStatus);
102
177
  }
103
- if (!hasConflict) {
104
- return this.store.applyRemoteOperation(op2);
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();
105
201
  }
106
- this.emitter?.emit({
107
- type: "merge:started",
108
- operationA: op2,
109
- operationB: op2
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: { ... } })"
110
213
  });
111
- const baseState = { ...op2.previousData };
112
- const localOp = {
113
- ...op2,
114
- // The "local" operation's data is the diff between base and current local state
115
- data: buildLocalDiff(op2.previousData, currentRecord, Object.keys(op2.data)),
116
- previousData: op2.previousData,
117
- nodeId: this.store.getNodeId()
118
- };
119
- const input = {
120
- local: localOp,
121
- remote: op2,
122
- baseState,
123
- collectionDef
124
- };
125
- const result = this.mergeEngine.mergeFields(input);
126
- for (const trace of result.traces) {
127
- this.emitter?.emit({ type: "merge:conflict", trace });
128
- }
129
- const firstTrace = result.traces[0];
130
- if (firstTrace) {
131
- this.emitter?.emit({ type: "merge:completed", trace: firstTrace });
132
- }
133
- const mergedOp = {
134
- ...op2,
135
- data: result.mergedData
136
- };
137
- return this.store.applyRemoteOperation(mergedOp);
138
214
  }
139
- };
140
- function buildLocalDiff(baseState, currentRecord, fields) {
141
- const diff = {};
142
- for (const field of fields) {
143
- 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
+ );
144
240
  }
145
- return diff;
146
241
  }
147
- function deepEqual(a, b) {
148
- if (a === b) return true;
149
- if (a === null || b === null) return false;
150
- if (a === void 0 || b === void 0) return false;
151
- if (typeof a !== typeof b) return false;
152
- if (Array.isArray(a) && Array.isArray(b)) {
153
- if (a.length !== b.length) return false;
154
- 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
+ });
155
247
  }
156
- if (typeof a === "object" && typeof b === "object") {
157
- const keysA = Object.keys(a);
158
- const keysB = Object.keys(b);
159
- if (keysA.length !== keysB.length) return false;
160
- return keysA.every(
161
- (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
+ }
162
266
  );
163
267
  }
164
- return false;
165
268
  }
166
269
 
167
270
  // src/create-app.ts
168
271
  function createApp(config) {
272
+ validateCreateAppConfig(config);
169
273
  const emitter = new SimpleEventEmitter();
170
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
+ }
171
296
  let store = null;
172
297
  let syncEngine = null;
173
298
  let unsubscribeSync = null;
299
+ let unsubscribeAudit = null;
174
300
  let reconnectionManager = null;
175
301
  let connectionMonitor = null;
176
302
  let instrumenter = null;
177
303
  let intentionalDisconnect = false;
178
304
  let qualityInterval = null;
305
+ let syncStatusBridge = null;
306
+ let destroyDevtoolsOverlay = null;
179
307
  if (config.devtools) {
180
308
  instrumenter = new Instrumenter(emitter, {
181
309
  bridgeEnabled: typeof globalThis !== "undefined" && "window" in globalThis
182
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
+ }
183
319
  }
184
320
  const ready = initializeAsync(config, emitter, mergeEngine).then((result) => {
185
321
  store = result.store;
186
322
  syncEngine = result.syncEngine;
187
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
+ }
188
354
  if (config.sync && syncEngine) {
189
355
  connectionMonitor = new ConnectionMonitor();
190
356
  reconnectionManager = new ReconnectionManager({
@@ -209,10 +375,24 @@ function createApp(config) {
209
375
  qualityInterval = null;
210
376
  }
211
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
+ });
212
392
  if (config.sync.autoReconnect !== false) {
213
393
  const engine = syncEngine;
214
394
  emitter.on("sync:disconnected", () => {
215
- if (intentionalDisconnect) return;
395
+ if (intentionalDisconnect || engine.isSchemaBlocked()) return;
216
396
  if (reconnectionManager?.isRunning()) return;
217
397
  engine.setReconnecting(true);
218
398
  reconnectionManager?.stop();
@@ -229,9 +409,32 @@ function createApp(config) {
229
409
  });
230
410
  });
231
411
  }
412
+ if (config.sync.autoConnect === true && syncEngine) {
413
+ void syncEngine.start().catch(() => {
414
+ });
415
+ }
232
416
  }
233
417
  });
418
+ const offlineSyncStatus = () => ({
419
+ status: "offline",
420
+ pendingOperations: 0,
421
+ lastSyncedAt: null,
422
+ lastSuccessfulPush: null,
423
+ lastSuccessfulPull: null,
424
+ conflicts: 0
425
+ });
234
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
+ },
235
438
  async connect() {
236
439
  await ready;
237
440
  if (syncEngine) {
@@ -239,6 +442,7 @@ function createApp(config) {
239
442
  reconnectionManager?.stop();
240
443
  reconnectionManager?.reset();
241
444
  await syncEngine.start();
445
+ syncStatusBridge?.refresh();
242
446
  }
243
447
  },
244
448
  async disconnect() {
@@ -247,20 +451,14 @@ function createApp(config) {
247
451
  intentionalDisconnect = true;
248
452
  reconnectionManager?.stop();
249
453
  await syncEngine.stop();
454
+ syncStatusBridge?.refresh();
250
455
  }
251
456
  },
252
457
  getStatus() {
253
458
  if (syncEngine) {
254
459
  return syncEngine.getStatus();
255
460
  }
256
- return {
257
- status: "offline",
258
- pendingOperations: 0,
259
- lastSyncedAt: null,
260
- lastSuccessfulPush: null,
261
- lastSuccessfulPull: null,
262
- conflicts: 0
263
- };
461
+ return offlineSyncStatus();
264
462
  },
265
463
  async retryNow() {
266
464
  await ready;
@@ -268,6 +466,9 @@ function createApp(config) {
268
466
  await syncEngine.retryNow();
269
467
  }
270
468
  },
469
+ clearSchemaBlock() {
470
+ syncEngine?.clearSchemaBlock();
471
+ },
271
472
  exportDiagnostics() {
272
473
  if (syncEngine) {
273
474
  return syncEngine.exportDiagnostics();
@@ -301,33 +502,23 @@ function createApp(config) {
301
502
  if (!store) {
302
503
  throw new Error("Store not initialized. Await app.ready before using transactions.");
303
504
  }
304
- const txContext = store.createTransaction();
305
- if (mutationName !== void 0) {
306
- txContext.setMutationName(mutationName);
307
- }
308
505
  const collectionNames = Object.keys(config.schema.collections);
309
- const proxy = {};
310
- for (const name of collectionNames) {
311
- Object.defineProperty(proxy, name, {
312
- get() {
313
- return txContext.collection(name);
314
- },
315
- enumerable: true,
316
- configurable: false
317
- });
318
- }
319
- try {
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
+ }
320
520
  await fn(proxy);
321
- const { operations } = await txContext.commit();
322
- for (const op2 of operations) {
323
- store.getSubscriptionManager().notify(op2.collection, op2);
324
- emitter.emit({ type: "operation:created", operation: op2 });
325
- }
326
- return operations;
327
- } catch (error) {
328
- txContext.rollback();
329
- throw error;
330
- }
521
+ });
331
522
  }
332
523
  const sequences = {
333
524
  async next(name, config2) {
@@ -374,6 +565,10 @@ function createApp(config) {
374
565
  qualityInterval = null;
375
566
  }
376
567
  reconnectionManager?.stop();
568
+ if (destroyDevtoolsOverlay) {
569
+ destroyDevtoolsOverlay();
570
+ destroyDevtoolsOverlay = null;
571
+ }
377
572
  if (instrumenter) {
378
573
  instrumenter.destroy();
379
574
  instrumenter = null;
@@ -382,15 +577,51 @@ function createApp(config) {
382
577
  unsubscribeSync();
383
578
  unsubscribeSync = null;
384
579
  }
580
+ if (unsubscribeAudit) {
581
+ unsubscribeAudit();
582
+ unsubscribeAudit = null;
583
+ }
385
584
  if (syncEngine) {
386
585
  await syncEngine.stop();
387
586
  syncEngine = null;
388
587
  }
588
+ if (syncStatusBridge) {
589
+ syncStatusBridge.destroy();
590
+ syncStatusBridge = null;
591
+ }
389
592
  if (store) {
390
593
  await store.close();
391
594
  store = null;
392
595
  }
393
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);
394
625
  }
395
626
  };
396
627
  for (const collectionName of Object.keys(config.schema.collections)) {
@@ -407,39 +638,87 @@ function createApp(config) {
407
638
  async function initializeAsync(config, emitter, mergeEngine) {
408
639
  const adapterType = config.store?.adapter ?? detectAdapterType();
409
640
  const dbName = config.store?.name ?? "kora-db";
410
- 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;
411
652
  const store = new Store({
412
653
  schema: config.schema,
413
654
  adapter,
414
- emitter
655
+ emitter,
656
+ dbName,
657
+ nodeId: authNodeId,
658
+ isolation: authNodeId ? "shared" : config.store?.isolation,
659
+ ...config.sync ? { onQuerySubscribed: createSyncQuerySubscriptionHook(() => syncEngine) } : {}
415
660
  });
416
661
  await store.open();
417
- 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);
418
671
  let unsubscribeSync = null;
419
672
  if (config.sync) {
420
- const transport = new WebSocketTransport();
421
- const mergeAwareStore = new MergeAwareSyncStore(store, mergeEngine, emitter);
422
- const scopeMap = config.sync.scope ? buildScopeMap(config.schema, config.sync.scope) : void 0;
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;
423
683
  syncEngine = new SyncEngine({
424
684
  transport,
425
685
  store: mergeAwareStore,
426
686
  config: {
427
687
  url: config.sync.url,
428
688
  transport: config.sync.transport,
429
- auth: config.sync.auth,
689
+ auth: syncAuth,
430
690
  batchSize: config.sync.batchSize,
431
691
  schemaVersion: config.sync.schemaVersion ?? config.schema.version,
432
- scopeMap
692
+ scopeMap,
693
+ encryption: config.sync.encryption,
694
+ strictHandshake: config.sync.strictHandshake,
695
+ operationTransforms: config.sync.operationTransforms
433
696
  },
434
- emitter
697
+ emitter,
698
+ queueStorage: new StoreQueueStorage(adapter),
699
+ syncState: new StoreSyncStatePersistence(store, scopeMap),
700
+ encryptor
435
701
  });
702
+ recordConflict = () => syncEngine?.recordConflict();
436
703
  unsubscribeSync = emitter.on("operation:created", (event) => {
437
704
  if (syncEngine) {
438
705
  syncEngine.pushOperation(event.operation);
439
706
  }
440
707
  });
441
708
  }
442
- 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();
443
722
  }
444
723
  function createCollectionAccessor(collectionName, getStore) {
445
724
  return {
@@ -504,15 +783,19 @@ function createPendingQueryBuilder(initialWhere) {
504
783
  return this;
505
784
  },
506
785
  async exec() {
507
- return [];
786
+ throw new AppNotReadyError(
787
+ "Cannot execute a query before app.ready. Await app.ready or use <KoraProvider app={app}>."
788
+ );
508
789
  },
509
790
  async count() {
510
- return 0;
791
+ throw new AppNotReadyError(
792
+ "Cannot count query results before app.ready. Await app.ready or use <KoraProvider app={app}>."
793
+ );
511
794
  },
512
- subscribe(callback) {
513
- void callback([]);
514
- return () => {
515
- };
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
+ );
516
799
  },
517
800
  getDescriptor() {
518
801
  return { ...descriptor, where: { ...descriptor.where }, orderBy: [...descriptor.orderBy] };
@@ -522,19 +805,30 @@ function createPendingQueryBuilder(initialWhere) {
522
805
  }
523
806
 
524
807
  // src/index.ts
808
+ import {
809
+ decodeAuditExport,
810
+ readAuditExportManifest,
811
+ verifyAuditExportChecksum
812
+ } from "@korajs/store";
525
813
  import { defineSchema, migrate, t } from "@korajs/core";
526
814
  import { HybridLogicalClock } from "@korajs/core";
527
815
  import { generateUUIDv7 } from "@korajs/core";
528
816
  import { createOperation } from "@korajs/core";
529
- import { KoraError } from "@korajs/core";
817
+ import { KoraError as KoraError3 } from "@korajs/core";
530
818
  import { op } from "@korajs/core";
531
819
  import { SequenceManager, Store as Store2 } from "@korajs/store";
532
820
  import { TransactionContext } from "@korajs/store";
821
+ import {
822
+ exportBackup,
823
+ readBackupManifest,
824
+ restoreBackup,
825
+ verifyBackupChecksum
826
+ } from "@korajs/store";
533
827
  import { MergeEngine as MergeEngine2 } from "@korajs/merge";
534
828
  import { SyncEngine as SyncEngine2, WebSocketTransport as WebSocketTransport2 } from "@korajs/sync";
535
829
  export {
536
830
  HybridLogicalClock,
537
- KoraError,
831
+ KoraError3 as KoraError,
538
832
  MergeEngine2 as MergeEngine,
539
833
  SequenceManager,
540
834
  Store2 as Store,
@@ -543,10 +837,17 @@ export {
543
837
  WebSocketTransport2 as WebSocketTransport,
544
838
  createApp,
545
839
  createOperation,
840
+ decodeAuditExport,
546
841
  defineSchema,
842
+ exportBackup,
547
843
  generateUUIDv7,
548
844
  migrate,
549
845
  op,
550
- t
846
+ readAuditExportManifest,
847
+ readBackupManifest,
848
+ restoreBackup,
849
+ t,
850
+ verifyAuditExportChecksum,
851
+ verifyBackupChecksum
551
852
  };
552
853
  //# sourceMappingURL=index.js.map