@korajs/store 1.0.0-beta.5 → 1.0.0-beta.7

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.
Files changed (50) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +347 -132
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +4 -2
  6. package/dist/adapters/indexeddb.d.ts +4 -2
  7. package/dist/adapters/indexeddb.js +42 -5
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  10. package/dist/adapters/sqlite-wasm-worker.js +250 -3
  11. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  12. package/dist/adapters/sqlite-wasm.cjs +308 -129
  13. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  14. package/dist/adapters/sqlite-wasm.d.cts +18 -7
  15. package/dist/adapters/sqlite-wasm.d.ts +18 -7
  16. package/dist/adapters/sqlite-wasm.js +2 -2
  17. package/dist/{chunk-Q57CHIIN.js → chunk-RJIEKEN4.js} +310 -130
  18. package/dist/chunk-RJIEKEN4.js.map +1 -0
  19. package/dist/{chunk-COJSQBXW.js → chunk-VDBJQ75K.js} +1 -1
  20. package/dist/chunk-VDBJQ75K.js.map +1 -0
  21. package/dist/{chunk-RL43UYQZ.js → chunk-YT7MRFCP.js} +5 -5
  22. package/dist/index.cjs +26 -0
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +18 -4
  25. package/dist/index.d.ts +18 -4
  26. package/dist/index.js +44 -18
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal.d.cts +3 -3
  29. package/dist/internal.d.ts +3 -3
  30. package/dist/internal.js +2 -2
  31. package/dist/{sqlite-wasm-channel-NGSBJR44.js → sqlite-wasm-channel-GGHA2BXU.js} +2 -2
  32. package/dist/{sqlite-wasm-channel-Bx57s8Cq.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +2 -2
  33. package/dist/{sqlite-wasm-channel-Bx57s8Cq.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +2 -2
  34. package/dist/{types-C1kigrWz.d.ts → types-BOO9EPt1.d.ts} +1 -1
  35. package/dist/{types-D1MzKVmk.d.ts → types-BslDOTk0.d.cts} +16 -1
  36. package/dist/{types-D1MzKVmk.d.cts → types-BslDOTk0.d.ts} +16 -1
  37. package/dist/{types-DAo9oduO.d.cts → types-C_GDavG-.d.cts} +1 -1
  38. package/package.json +2 -12
  39. package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -345
  40. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
  41. package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -82
  42. package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -82
  43. package/dist/adapters/sqlite-wasm-shared-host.js +0 -61
  44. package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
  45. package/dist/chunk-3Y2IERBF.js +0 -255
  46. package/dist/chunk-3Y2IERBF.js.map +0 -1
  47. package/dist/chunk-COJSQBXW.js.map +0 -1
  48. package/dist/chunk-Q57CHIIN.js.map +0 -1
  49. /package/dist/{chunk-RL43UYQZ.js.map → chunk-YT7MRFCP.js.map} +0 -0
  50. /package/dist/{sqlite-wasm-channel-NGSBJR44.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-D1MzKVmk.cjs';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.cjs';
3
3
 
4
4
  /**
5
5
  * Storage adapter backed by better-sqlite3 for Node.js environments.
@@ -1,5 +1,5 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-D1MzKVmk.js';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-BslDOTk0.js';
3
3
 
4
4
  /**
5
5
  * Storage adapter backed by better-sqlite3 for Node.js environments.
@@ -282,93 +282,15 @@ var IndexedDbPersistenceScheduler = class {
282
282
  var import_core2 = require("@korajs/core");
283
283
  init_errors();
284
284
 
285
- // src/multi-tab/shared-worker-bridge.ts
286
- init_errors();
287
- var SW_REQUEST = "kora-sw-request";
288
- var SW_RESPONSE = "kora-sw-response";
289
- var SharedWorkerClientBridge = class {
290
- constructor(sharedWorker, dbName, workerUrl, timeoutMs = 3e4) {
291
- this.dbName = dbName;
292
- this.workerUrl = workerUrl;
293
- this.timeoutMs = timeoutMs;
294
- this.port = sharedWorker.port;
295
- this.port.start();
296
- this.port.addEventListener("message", (event) => {
297
- const data = event.data;
298
- if (data?.type !== SW_RESPONSE) {
299
- return;
300
- }
301
- const entry = this.pending.get(data.requestId);
302
- if (entry) {
303
- this.pending.delete(data.requestId);
304
- entry.resolve(data.response);
305
- }
306
- });
307
- }
308
- dbName;
309
- workerUrl;
310
- port;
311
- pending = /* @__PURE__ */ new Map();
312
- timeoutMs;
313
- terminated = false;
314
- async send(request) {
315
- if (this.terminated) {
316
- return {
317
- id: request.id,
318
- type: "error",
319
- message: "SharedWorker bridge terminated",
320
- code: "BRIDGE_TERMINATED"
321
- };
322
- }
323
- const requestId = crypto.randomUUID();
324
- const msg = {
325
- type: SW_REQUEST,
326
- requestId,
327
- dbName: this.dbName,
328
- workerUrl: this.workerUrl,
329
- request
330
- };
331
- return new Promise((resolve, reject) => {
332
- const timer = setTimeout(() => {
333
- this.pending.delete(requestId);
334
- reject(new WorkerTimeoutError(`shared-worker:${request.type}`, this.timeoutMs));
335
- }, this.timeoutMs);
336
- this.pending.set(requestId, {
337
- resolve: (response) => {
338
- clearTimeout(timer);
339
- resolve(response);
340
- },
341
- reject: (error) => {
342
- clearTimeout(timer);
343
- reject(error);
344
- }
345
- });
346
- this.port.postMessage(msg);
347
- });
348
- }
349
- terminate() {
350
- if (this.terminated) {
351
- return;
352
- }
353
- this.terminated = true;
354
- this.port.close();
355
- for (const [, entry] of this.pending) {
356
- entry.reject(new Error("SharedWorker bridge terminated"));
357
- }
358
- this.pending.clear();
359
- }
360
- };
361
-
362
285
  // src/multi-tab/tab-storage.ts
363
286
  init_errors();
364
287
  var RPC_REQUEST = "kora-worker-request";
365
288
  var RPC_RESPONSE = "kora-worker-response";
289
+ var CLIENT_LEAVE = "kora-client-leave";
366
290
  var LEADER_PING = "kora-leader-ping";
367
291
  var LEADER_PONG = "kora-leader-pong";
368
292
  var DEFAULT_LIVENESS_PROBE_MS = 2e3;
369
- function isSharedWorkerStorageSupported() {
370
- return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
371
- }
293
+ var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
372
294
  function isMultiTabStorageSupported() {
373
295
  return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
374
296
  }
@@ -430,10 +352,16 @@ function startLeaderRpcRelay(channelName, bridge) {
430
352
  channel.postMessage({ type: LEADER_PONG });
431
353
  return;
432
354
  }
355
+ if (data?.type === CLIENT_LEAVE) {
356
+ if (hasReclaimClient(bridge)) {
357
+ bridge.reclaimClient(data.clientId, "client-left");
358
+ }
359
+ return;
360
+ }
433
361
  if (data?.type !== RPC_REQUEST) {
434
362
  return;
435
363
  }
436
- void bridge.send(data.request).then((response) => {
364
+ void bridge.send(data.request, data.clientId).then((response) => {
437
365
  const msg = {
438
366
  type: RPC_RESPONSE,
439
367
  requestId: data.requestId,
@@ -463,6 +391,10 @@ function startLeaderRpcRelay(channelName, bridge) {
463
391
  }
464
392
  var FollowerBroadcastBridge = class {
465
393
  channel;
394
+ clientId = createClientId();
395
+ onPageHide = () => {
396
+ this.leaveLeader();
397
+ };
466
398
  pending = /* @__PURE__ */ new Map();
467
399
  timeoutMs;
468
400
  livenessProbeMs;
@@ -471,6 +403,9 @@ var FollowerBroadcastBridge = class {
471
403
  this.timeoutMs = timeoutMs;
472
404
  this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
473
405
  this.channel = new BroadcastChannel(channelName);
406
+ if (typeof addEventListener === "function") {
407
+ addEventListener("pagehide", this.onPageHide);
408
+ }
474
409
  this.channel.addEventListener("message", (event) => {
475
410
  const data = event.data;
476
411
  if (data?.type !== RPC_RESPONSE) {
@@ -534,8 +469,13 @@ var FollowerBroadcastBridge = class {
534
469
  code: "BRIDGE_TERMINATED"
535
470
  };
536
471
  }
537
- const requestId = crypto.randomUUID();
538
- const msg = { type: RPC_REQUEST, requestId, request };
472
+ const requestId = createClientId();
473
+ const msg = {
474
+ type: RPC_REQUEST,
475
+ requestId,
476
+ clientId: this.clientId,
477
+ request
478
+ };
539
479
  return new Promise((resolve, reject) => {
540
480
  const settle = (fn) => {
541
481
  clearTimeout(timer);
@@ -565,16 +505,213 @@ var FollowerBroadcastBridge = class {
565
505
  return;
566
506
  }
567
507
  this.terminated = true;
508
+ this.leaveLeader();
509
+ if (typeof removeEventListener === "function") {
510
+ removeEventListener("pagehide", this.onPageHide);
511
+ }
568
512
  this.channel.close();
569
513
  for (const [, entry] of this.pending) {
570
514
  entry.reject(new Error("Follower bridge terminated"));
571
515
  }
572
516
  this.pending.clear();
573
517
  }
518
+ leaveLeader() {
519
+ try {
520
+ const message = {
521
+ type: CLIENT_LEAVE,
522
+ clientId: this.clientId
523
+ };
524
+ this.channel.postMessage(message);
525
+ } catch {
526
+ }
527
+ }
574
528
  };
529
+ var TransactionSerializingWorkerBridge = class {
530
+ inner;
531
+ leaderClientId = createClientId();
532
+ transactionIdleTimeoutMs;
533
+ queue = [];
534
+ activeTransactionClient = null;
535
+ abortedClients = /* @__PURE__ */ new Set();
536
+ transactionIdleTimer = null;
537
+ reclaiming = false;
538
+ processing = false;
539
+ terminated = false;
540
+ constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
541
+ this.inner = inner;
542
+ this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
543
+ }
544
+ send(request, clientId = this.leaderClientId) {
545
+ if (this.terminated) {
546
+ return Promise.resolve({
547
+ id: request.id,
548
+ type: "error",
549
+ message: "Worker has been terminated",
550
+ code: "WORKER_TERMINATED"
551
+ });
552
+ }
553
+ if (this.abortedClients.has(clientId)) {
554
+ if (request.type === "begin") {
555
+ this.abortedClients.delete(clientId);
556
+ } else {
557
+ return Promise.resolve({
558
+ id: request.id,
559
+ type: "error",
560
+ message: "Previous transaction was aborted because the client stopped sending requests.",
561
+ code: "TRANSACTION_ABORTED"
562
+ });
563
+ }
564
+ }
565
+ return new Promise((resolve, reject) => {
566
+ this.queue.push({ clientId, request, resolve, reject });
567
+ void this.processQueue();
568
+ });
569
+ }
570
+ terminate() {
571
+ if (this.terminated) {
572
+ return;
573
+ }
574
+ this.terminated = true;
575
+ this.clearTransactionIdleTimer();
576
+ this.inner.terminate();
577
+ const pending = this.queue.splice(0);
578
+ for (const entry of pending) {
579
+ entry.reject(new Error("Worker terminated"));
580
+ }
581
+ }
582
+ reclaimClient(clientId, reason) {
583
+ if (this.terminated) {
584
+ return;
585
+ }
586
+ void this.reclaimClientNow(clientId, reason);
587
+ }
588
+ async processQueue() {
589
+ if (this.processing || this.reclaiming) {
590
+ return;
591
+ }
592
+ this.processing = true;
593
+ try {
594
+ while (!this.terminated) {
595
+ const index = this.nextRunnableIndex();
596
+ if (index === -1) {
597
+ return;
598
+ }
599
+ const [entry] = this.queue.splice(index, 1);
600
+ if (!entry) {
601
+ return;
602
+ }
603
+ try {
604
+ const response = await this.inner.send(entry.request);
605
+ this.recordTransactionState(entry, response);
606
+ entry.resolve(response);
607
+ } catch (error) {
608
+ this.resetFailedTransaction(entry);
609
+ entry.reject(error instanceof Error ? error : new Error(String(error)));
610
+ }
611
+ }
612
+ } finally {
613
+ this.processing = false;
614
+ if (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {
615
+ void this.processQueue();
616
+ }
617
+ }
618
+ }
619
+ nextRunnableIndex() {
620
+ if (this.queue.length === 0) {
621
+ return -1;
622
+ }
623
+ if (this.activeTransactionClient === null) {
624
+ return 0;
625
+ }
626
+ return this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient);
627
+ }
628
+ recordTransactionState(entry, response) {
629
+ if (response.type === "error") {
630
+ this.resetFailedTransaction(entry);
631
+ return;
632
+ }
633
+ if (entry.request.type === "begin") {
634
+ this.activeTransactionClient = entry.clientId;
635
+ this.armTransactionIdleTimer(entry.clientId);
636
+ return;
637
+ }
638
+ if (this.activeTransactionClient === entry.clientId) {
639
+ this.armTransactionIdleTimer(entry.clientId);
640
+ }
641
+ if (this.activeTransactionClient === entry.clientId && (entry.request.type === "commit" || entry.request.type === "rollback")) {
642
+ this.activeTransactionClient = null;
643
+ this.clearTransactionIdleTimer();
644
+ }
645
+ }
646
+ resetFailedTransaction(entry) {
647
+ if (this.activeTransactionClient === entry.clientId) {
648
+ this.activeTransactionClient = null;
649
+ this.clearTransactionIdleTimer();
650
+ }
651
+ }
652
+ armTransactionIdleTimer(clientId) {
653
+ this.clearTransactionIdleTimer();
654
+ this.transactionIdleTimer = setTimeout(() => {
655
+ void this.reclaimClientNow(clientId, "transaction-idle-timeout");
656
+ }, this.transactionIdleTimeoutMs);
657
+ }
658
+ clearTransactionIdleTimer() {
659
+ if (this.transactionIdleTimer) {
660
+ clearTimeout(this.transactionIdleTimer);
661
+ this.transactionIdleTimer = null;
662
+ }
663
+ }
664
+ async reclaimClientNow(clientId, reason) {
665
+ if (this.reclaiming || this.activeTransactionClient !== clientId) {
666
+ return;
667
+ }
668
+ this.reclaiming = true;
669
+ this.clearTransactionIdleTimer();
670
+ this.activeTransactionClient = null;
671
+ this.abortedClients.add(clientId);
672
+ this.rejectQueuedClientRequests(clientId, reason);
673
+ try {
674
+ const response = await this.inner.send({ id: 0, type: "rollback" });
675
+ if (response.type === "error") {
676
+ }
677
+ } catch {
678
+ } finally {
679
+ this.reclaiming = false;
680
+ if (!this.terminated && this.nextRunnableIndex() !== -1) {
681
+ void this.processQueue();
682
+ }
683
+ }
684
+ }
685
+ rejectQueuedClientRequests(clientId, reason) {
686
+ const keep = [];
687
+ for (const entry of this.queue) {
688
+ if (entry.clientId === clientId) {
689
+ entry.resolve({
690
+ id: entry.request.id,
691
+ type: "error",
692
+ message: `Transaction client was reclaimed: ${reason}`,
693
+ code: "TRANSACTION_ABORTED"
694
+ });
695
+ } else {
696
+ keep.push(entry);
697
+ }
698
+ }
699
+ this.queue = keep;
700
+ }
701
+ };
702
+ function hasReclaimClient(bridge) {
703
+ return typeof bridge.reclaimClient === "function";
704
+ }
705
+ function createClientId() {
706
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
707
+ return crypto.randomUUID();
708
+ }
709
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
710
+ }
575
711
 
576
712
  // src/adapters/sqlite-wasm-adapter.ts
577
713
  init_sqlite_wasm_channel();
714
+ var warnedSharedWorkerDeprecated = false;
578
715
  var SqliteWasmAdapter = class {
579
716
  bridge = null;
580
717
  opened = false;
@@ -586,7 +723,8 @@ var SqliteWasmAdapter = class {
586
723
  dbName;
587
724
  emitter;
588
725
  tabSession = null;
589
- sharedWorker = null;
726
+ emitNonPersistentDiagnostic;
727
+ storageOpenState = null;
590
728
  /** Retained so a follower promoted to leader can re-open its own worker. */
591
729
  schema = null;
592
730
  promoting = false;
@@ -597,50 +735,62 @@ var SqliteWasmAdapter = class {
597
735
  this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
598
736
  this.dbName = options.dbName ?? "kora-db";
599
737
  this.emitter = options.emitter;
738
+ this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
600
739
  }
601
740
  async open(schema) {
602
741
  if (this.opened) return;
742
+ const ddlStatements = (0, import_core2.generateFullDDL)(schema);
603
743
  if (this.injectedBridge) {
604
744
  this.bridge = this.injectedBridge;
605
- } else if (this.sharedWorkerUrl && this.workerUrl && isSharedWorkerStorageSupported()) {
606
- const workerUrlString = typeof this.workerUrl === "string" ? this.workerUrl : this.workerUrl.href;
607
- const sharedUrl = typeof this.sharedWorkerUrl === "string" ? this.sharedWorkerUrl : this.sharedWorkerUrl.href;
608
- this.sharedWorker = new SharedWorker(sharedUrl, {
609
- type: "module",
610
- name: `kora-sw-${this.dbName}`
611
- });
612
- this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
613
- } else if (this.workerUrl) {
614
- this.schema = schema;
615
- this.tabSession = await acquireTabStorageSession(this.dbName, {
616
- onPromote: () => {
617
- void this.promoteToLeader();
618
- }
619
- });
620
- const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
621
- if (this.tabSession.role === "leader") {
622
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
623
- this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
624
- this.bridge = workerBridge;
625
- } else {
626
- const followerBridge = new FollowerBroadcastBridge(
627
- this.tabSession.channelName,
628
- this.workerResponseTimeoutMs
629
- );
630
- this.bridge = followerBridge;
631
- this.emitter?.emit({
632
- type: "store:db-name-collision",
633
- dbName: this.dbName,
634
- message: `Another runtime on this origin is already using database "${this.dbName}"; this runtime is sharing it as a follower. If these are separate apps, give each a distinct store name.`
635
- });
636
- await followerBridge.waitForLeader();
745
+ const response = await this.openCurrentBridge(ddlStatements);
746
+ this.reportStorageMode(response.data);
747
+ this.opened = true;
748
+ return;
749
+ }
750
+ if (this.workerUrl) {
751
+ this.warnSharedWorkerDeprecated();
752
+ await this.openDurableWorkerBridge(schema);
753
+ const response = await this.openCurrentBridge(ddlStatements);
754
+ this.reportStorageMode(response.data);
755
+ this.opened = true;
756
+ return;
757
+ }
758
+ throw new AdapterError(
759
+ 'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
760
+ );
761
+ }
762
+ async openDurableWorkerBridge(schema) {
763
+ if (!this.workerUrl) {
764
+ throw new AdapterError("Durable SQLite WASM storage requires workerUrl.");
765
+ }
766
+ this.schema = schema;
767
+ this.tabSession = await acquireTabStorageSession(this.dbName, {
768
+ onPromote: () => {
769
+ void this.promoteToLeader();
637
770
  }
638
- } else {
639
- throw new AdapterError(
640
- 'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
771
+ });
772
+ const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
773
+ if (this.tabSession.role === "leader") {
774
+ const workerBridge = new TransactionSerializingWorkerBridge(
775
+ new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
641
776
  );
777
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
778
+ this.bridge = workerBridge;
779
+ return;
642
780
  }
643
- const ddlStatements = (0, import_core2.generateFullDDL)(schema);
781
+ const followerBridge = new FollowerBroadcastBridge(
782
+ this.tabSession.channelName,
783
+ this.workerResponseTimeoutMs
784
+ );
785
+ this.bridge = followerBridge;
786
+ this.emitter?.emit({
787
+ type: "store:db-name-collision",
788
+ dbName: this.dbName,
789
+ message: `Another runtime on this origin is already using database "${this.dbName}"; this runtime is sharing it as a follower. If these are separate apps, give each a distinct store name.`
790
+ });
791
+ await followerBridge.waitForLeader();
792
+ }
793
+ async openCurrentBridge(ddlStatements) {
644
794
  const response = await this.sendRequest({
645
795
  id: 0,
646
796
  type: "open",
@@ -653,8 +803,7 @@ var SqliteWasmAdapter = class {
653
803
  dbName: this.dbName
654
804
  });
655
805
  }
656
- this.reportStorageMode(response.data);
657
- this.opened = true;
806
+ return response;
658
807
  }
659
808
  /**
660
809
  * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
@@ -663,12 +812,12 @@ var SqliteWasmAdapter = class {
663
812
  * bridge that does not report a mode, e.g. the Node mock) emits nothing.
664
813
  */
665
814
  reportStorageMode(data) {
666
- if (!this.emitter || typeof data !== "object" || data === null) {
815
+ this.storageOpenState = parseStorageOpenState(data);
816
+ if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
667
817
  return;
668
818
  }
669
- const mode = data;
670
- if (mode.persistent === false) {
671
- const reason = mode.fallbackReason ?? "unsupported";
819
+ if (this.storageOpenState.persistent === false) {
820
+ const reason = this.storageOpenState.fallbackReason ?? "unsupported";
672
821
  this.emitter.emit({
673
822
  type: "store:opfs-unavailable",
674
823
  dbName: this.dbName,
@@ -677,6 +826,18 @@ var SqliteWasmAdapter = class {
677
826
  });
678
827
  }
679
828
  }
829
+ getStorageOpenState() {
830
+ return this.storageOpenState;
831
+ }
832
+ warnSharedWorkerDeprecated() {
833
+ if (!this.sharedWorkerUrl || warnedSharedWorkerDeprecated) {
834
+ return;
835
+ }
836
+ warnedSharedWorkerDeprecated = true;
837
+ console.warn(
838
+ "[kora] sharedWorkerUrl is deprecated and ignored: SharedWorker-hosted SQLite cannot use OPFS and is never durable. Kora uses the durable dedicated-worker leader/follower path for multi-tab storage."
839
+ );
840
+ }
680
841
  async close() {
681
842
  if (!this.bridge) return;
682
843
  try {
@@ -688,7 +849,6 @@ var SqliteWasmAdapter = class {
688
849
  await this.tabSession.releaseLock();
689
850
  }
690
851
  this.tabSession = null;
691
- this.sharedWorker = null;
692
852
  this.bridge.terminate();
693
853
  this.bridge = null;
694
854
  this.opened = false;
@@ -707,7 +867,9 @@ var SqliteWasmAdapter = class {
707
867
  }
708
868
  this.promoting = true;
709
869
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
710
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
870
+ const workerBridge = new TransactionSerializingWorkerBridge(
871
+ new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
872
+ );
711
873
  const previousBridge = this.bridge;
712
874
  this.bridge = workerBridge;
713
875
  if (this.tabSession) {
@@ -805,7 +967,10 @@ var SqliteWasmAdapter = class {
805
967
  this.guardOpen();
806
968
  const response = await this.sendRequest({ id: 0, type: "export" });
807
969
  if (response.type === "error") {
808
- throw new AdapterError(`Export failed: ${response.message}`);
970
+ throw new AdapterError(`Export failed: ${response.message}`, {
971
+ code: response.code,
972
+ context: response.context
973
+ });
809
974
  }
810
975
  return response.data;
811
976
  }
@@ -838,6 +1003,20 @@ var SqliteWasmAdapter = class {
838
1003
  }
839
1004
  }
840
1005
  };
1006
+ function parseStorageOpenState(data) {
1007
+ if (typeof data !== "object" || data === null) {
1008
+ return null;
1009
+ }
1010
+ const mode = data;
1011
+ if (typeof mode.persistent !== "boolean") {
1012
+ return null;
1013
+ }
1014
+ return {
1015
+ persistent: mode.persistent,
1016
+ mode: mode.persistent ? "opfs" : "memory",
1017
+ ...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {}
1018
+ };
1019
+ }
841
1020
 
842
1021
  // src/adapters/sqlite-wasm-persistence.ts
843
1022
  init_errors();
@@ -972,6 +1151,24 @@ async function loadDumpFromIndexedDB(dbName) {
972
1151
  idb.close();
973
1152
  }
974
1153
  }
1154
+ async function deleteSnapshotFromIndexedDB(dbName) {
1155
+ const idb = await openIdb();
1156
+ try {
1157
+ await new Promise((resolve, reject) => {
1158
+ const tx = idb.transaction(IDB_STORE_NAME, "readwrite");
1159
+ const store = tx.objectStore(IDB_STORE_NAME);
1160
+ store.delete(dbName);
1161
+ tx.oncomplete = () => resolve();
1162
+ tx.onerror = () => reject(
1163
+ new PersistenceError(`Failed to delete database snapshot "${dbName}" from IndexedDB`, {
1164
+ dbName
1165
+ })
1166
+ );
1167
+ });
1168
+ } finally {
1169
+ idb.close();
1170
+ }
1171
+ }
975
1172
 
976
1173
  // src/adapters/indexeddb-adapter.ts
977
1174
  var IndexedDbAdapter = class {
@@ -979,6 +1176,7 @@ var IndexedDbAdapter = class {
979
1176
  dbName;
980
1177
  emitter;
981
1178
  scheduler;
1179
+ storageOpenState = null;
982
1180
  constructor(options = {}) {
983
1181
  this.dbName = options.dbName ?? "kora-db";
984
1182
  this.emitter = options.emitter;
@@ -996,8 +1194,12 @@ var IndexedDbAdapter = class {
996
1194
  }
997
1195
  async open(schema) {
998
1196
  await this.inner.open(schema);
1197
+ this.storageOpenState = { persistent: true, mode: "indexeddb" };
999
1198
  const persisted = await loadFromIndexedDB(this.dbName);
1000
- if (!persisted) return;
1199
+ if (!persisted) {
1200
+ await this.restoreFromDumpFallback();
1201
+ return;
1202
+ }
1001
1203
  try {
1002
1204
  await this.inner.importDatabase(persisted);
1003
1205
  } catch {
@@ -1024,6 +1226,9 @@ var IndexedDbAdapter = class {
1024
1226
  await this.inner.migrate(from, to, migration);
1025
1227
  this.scheduler.schedule();
1026
1228
  }
1229
+ getStorageOpenState() {
1230
+ return this.storageOpenState;
1231
+ }
1027
1232
  /**
1028
1233
  * Force an immediate snapshot write to IndexedDB (skips debounce).
1029
1234
  * Useful before tab unload or in tests.
@@ -1032,10 +1237,17 @@ var IndexedDbAdapter = class {
1032
1237
  await this.scheduler.flushNow();
1033
1238
  }
1034
1239
  async writeSnapshot() {
1035
- const data = await this.inner.exportDatabase();
1036
- await saveToIndexedDB(this.dbName, data);
1037
1240
  const dump = await this.exportDump();
1038
1241
  await saveDumpToIndexedDB(this.dbName, dump);
1242
+ try {
1243
+ const data = await this.inner.exportDatabase();
1244
+ await saveToIndexedDB(this.dbName, data);
1245
+ } catch (error) {
1246
+ if (!isUnsupportedWorkerExport(error)) {
1247
+ throw error;
1248
+ }
1249
+ await deleteSnapshotFromIndexedDB(this.dbName);
1250
+ }
1039
1251
  }
1040
1252
  handlePersistenceError(error) {
1041
1253
  const message = error instanceof Error ? error.message : "IndexedDB persistence failed";
@@ -1109,6 +1321,9 @@ function ensureSafeIdentifier(identifier) {
1109
1321
  }
1110
1322
  return identifier;
1111
1323
  }
1324
+ function isUnsupportedWorkerExport(error) {
1325
+ return error instanceof AdapterError && error.context?.code === "EXPORT_NOT_SUPPORTED";
1326
+ }
1112
1327
  // Annotate the CommonJS export names for ESM import in node:
1113
1328
  0 && (module.exports = {
1114
1329
  IndexedDbAdapter