korajs 0.4.0 → 0.6.0

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