@korajs/store 1.0.0-beta.4 → 1.0.0-beta.6

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 (62) 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 +309 -125
  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 +7 -2
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-worker.cjs +206 -208
  10. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  11. package/dist/adapters/sqlite-wasm-worker.js +206 -208
  12. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  13. package/dist/adapters/sqlite-wasm.cjs +304 -125
  14. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  15. package/dist/adapters/sqlite-wasm.d.cts +18 -7
  16. package/dist/adapters/sqlite-wasm.d.ts +18 -7
  17. package/dist/adapters/sqlite-wasm.js +2 -2
  18. package/dist/{backup-IQE4QC7Q.js → backup-5S7K2Z35.js} +3 -3
  19. package/dist/{chunk-RCTG3IJG.js → chunk-CYO3YKFL.js} +2 -2
  20. package/dist/{chunk-2ZRQGFEF.js → chunk-FKW56RVZ.js} +306 -126
  21. package/dist/chunk-FKW56RVZ.js.map +1 -0
  22. package/dist/{chunk-6NKLDBTY.js → chunk-GSJ7EEUC.js} +2 -1
  23. package/dist/chunk-GSJ7EEUC.js.map +1 -0
  24. package/dist/{chunk-OCOGQRHW.js → chunk-HS6DBMST.js} +2 -2
  25. package/dist/{chunk-COJSQBXW.js → chunk-VDBJQ75K.js} +1 -1
  26. package/dist/chunk-VDBJQ75K.js.map +1 -0
  27. package/dist/{chunk-UXRWVXBP.js → chunk-YT7MRFCP.js} +6 -6
  28. package/dist/{export-audit-MT2LTMIN.js → export-audit-ODGQTQIK.js} +3 -3
  29. package/dist/index.cjs +136 -3
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +35 -4
  32. package/dist/index.d.ts +35 -4
  33. package/dist/index.js +160 -27
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal.cjs +1 -0
  36. package/dist/internal.cjs.map +1 -1
  37. package/dist/internal.d.cts +3 -3
  38. package/dist/internal.d.ts +3 -3
  39. package/dist/internal.js +3 -3
  40. package/dist/{sqlite-wasm-channel-NGSBJR44.js → sqlite-wasm-channel-GGHA2BXU.js} +2 -2
  41. package/dist/{sqlite-wasm-channel-CNHRf-m2.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +2 -2
  42. package/dist/{sqlite-wasm-channel-CNHRf-m2.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +2 -2
  43. package/dist/{types-5h4Hqcwc.d.ts → types-BOO9EPt1.d.ts} +1 -1
  44. package/dist/{types-k6AZgiH4.d.ts → types-BslDOTk0.d.cts} +26 -1
  45. package/dist/{types-k6AZgiH4.d.cts → types-BslDOTk0.d.ts} +26 -1
  46. package/dist/{types-DfPiPwKr.d.cts → types-C_GDavG-.d.cts} +1 -1
  47. package/package.json +2 -12
  48. package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -95
  49. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
  50. package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -64
  51. package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -64
  52. package/dist/adapters/sqlite-wasm-shared-host.js +0 -70
  53. package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
  54. package/dist/chunk-2ZRQGFEF.js.map +0 -1
  55. package/dist/chunk-6NKLDBTY.js.map +0 -1
  56. package/dist/chunk-COJSQBXW.js.map +0 -1
  57. /package/dist/{backup-IQE4QC7Q.js.map → backup-5S7K2Z35.js.map} +0 -0
  58. /package/dist/{chunk-RCTG3IJG.js.map → chunk-CYO3YKFL.js.map} +0 -0
  59. /package/dist/{chunk-OCOGQRHW.js.map → chunk-HS6DBMST.js.map} +0 -0
  60. /package/dist/{chunk-UXRWVXBP.js.map → chunk-YT7MRFCP.js.map} +0 -0
  61. /package/dist/{export-audit-MT2LTMIN.js.map → export-audit-ODGQTQIK.js.map} +0 -0
  62. /package/dist/{sqlite-wasm-channel-NGSBJR44.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
@@ -199,93 +199,15 @@ module.exports = __toCommonJS(sqlite_wasm_exports);
199
199
  var import_core2 = require("@korajs/core");
200
200
  init_errors();
201
201
 
202
- // src/multi-tab/shared-worker-bridge.ts
203
- init_errors();
204
- var SW_REQUEST = "kora-sw-request";
205
- var SW_RESPONSE = "kora-sw-response";
206
- var SharedWorkerClientBridge = class {
207
- constructor(sharedWorker, dbName, workerUrl, timeoutMs = 3e4) {
208
- this.dbName = dbName;
209
- this.workerUrl = workerUrl;
210
- this.timeoutMs = timeoutMs;
211
- this.port = sharedWorker.port;
212
- this.port.start();
213
- this.port.addEventListener("message", (event) => {
214
- const data = event.data;
215
- if (data?.type !== SW_RESPONSE) {
216
- return;
217
- }
218
- const entry = this.pending.get(data.requestId);
219
- if (entry) {
220
- this.pending.delete(data.requestId);
221
- entry.resolve(data.response);
222
- }
223
- });
224
- }
225
- dbName;
226
- workerUrl;
227
- port;
228
- pending = /* @__PURE__ */ new Map();
229
- timeoutMs;
230
- terminated = false;
231
- async send(request) {
232
- if (this.terminated) {
233
- return {
234
- id: request.id,
235
- type: "error",
236
- message: "SharedWorker bridge terminated",
237
- code: "BRIDGE_TERMINATED"
238
- };
239
- }
240
- const requestId = crypto.randomUUID();
241
- const msg = {
242
- type: SW_REQUEST,
243
- requestId,
244
- dbName: this.dbName,
245
- workerUrl: this.workerUrl,
246
- request
247
- };
248
- return new Promise((resolve, reject) => {
249
- const timer = setTimeout(() => {
250
- this.pending.delete(requestId);
251
- reject(new WorkerTimeoutError(`shared-worker:${request.type}`, this.timeoutMs));
252
- }, this.timeoutMs);
253
- this.pending.set(requestId, {
254
- resolve: (response) => {
255
- clearTimeout(timer);
256
- resolve(response);
257
- },
258
- reject: (error) => {
259
- clearTimeout(timer);
260
- reject(error);
261
- }
262
- });
263
- this.port.postMessage(msg);
264
- });
265
- }
266
- terminate() {
267
- if (this.terminated) {
268
- return;
269
- }
270
- this.terminated = true;
271
- this.port.close();
272
- for (const [, entry] of this.pending) {
273
- entry.reject(new Error("SharedWorker bridge terminated"));
274
- }
275
- this.pending.clear();
276
- }
277
- };
278
-
279
202
  // src/multi-tab/tab-storage.ts
280
203
  init_errors();
281
204
  var RPC_REQUEST = "kora-worker-request";
282
205
  var RPC_RESPONSE = "kora-worker-response";
206
+ var CLIENT_LEAVE = "kora-client-leave";
283
207
  var LEADER_PING = "kora-leader-ping";
284
208
  var LEADER_PONG = "kora-leader-pong";
285
209
  var DEFAULT_LIVENESS_PROBE_MS = 2e3;
286
- function isSharedWorkerStorageSupported() {
287
- return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
288
- }
210
+ var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
289
211
  function isMultiTabStorageSupported() {
290
212
  return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
291
213
  }
@@ -347,10 +269,16 @@ function startLeaderRpcRelay(channelName, bridge) {
347
269
  channel.postMessage({ type: LEADER_PONG });
348
270
  return;
349
271
  }
272
+ if (data?.type === CLIENT_LEAVE) {
273
+ if (hasReclaimClient(bridge)) {
274
+ bridge.reclaimClient(data.clientId, "client-left");
275
+ }
276
+ return;
277
+ }
350
278
  if (data?.type !== RPC_REQUEST) {
351
279
  return;
352
280
  }
353
- void bridge.send(data.request).then((response) => {
281
+ void bridge.send(data.request, data.clientId).then((response) => {
354
282
  const msg = {
355
283
  type: RPC_RESPONSE,
356
284
  requestId: data.requestId,
@@ -380,6 +308,10 @@ function startLeaderRpcRelay(channelName, bridge) {
380
308
  }
381
309
  var FollowerBroadcastBridge = class {
382
310
  channel;
311
+ clientId = createClientId();
312
+ onPageHide = () => {
313
+ this.leaveLeader();
314
+ };
383
315
  pending = /* @__PURE__ */ new Map();
384
316
  timeoutMs;
385
317
  livenessProbeMs;
@@ -388,6 +320,9 @@ var FollowerBroadcastBridge = class {
388
320
  this.timeoutMs = timeoutMs;
389
321
  this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
390
322
  this.channel = new BroadcastChannel(channelName);
323
+ if (typeof addEventListener === "function") {
324
+ addEventListener("pagehide", this.onPageHide);
325
+ }
391
326
  this.channel.addEventListener("message", (event) => {
392
327
  const data = event.data;
393
328
  if (data?.type !== RPC_RESPONSE) {
@@ -451,8 +386,13 @@ var FollowerBroadcastBridge = class {
451
386
  code: "BRIDGE_TERMINATED"
452
387
  };
453
388
  }
454
- const requestId = crypto.randomUUID();
455
- const msg = { type: RPC_REQUEST, requestId, request };
389
+ const requestId = createClientId();
390
+ const msg = {
391
+ type: RPC_REQUEST,
392
+ requestId,
393
+ clientId: this.clientId,
394
+ request
395
+ };
456
396
  return new Promise((resolve, reject) => {
457
397
  const settle = (fn) => {
458
398
  clearTimeout(timer);
@@ -482,16 +422,213 @@ var FollowerBroadcastBridge = class {
482
422
  return;
483
423
  }
484
424
  this.terminated = true;
425
+ this.leaveLeader();
426
+ if (typeof removeEventListener === "function") {
427
+ removeEventListener("pagehide", this.onPageHide);
428
+ }
485
429
  this.channel.close();
486
430
  for (const [, entry] of this.pending) {
487
431
  entry.reject(new Error("Follower bridge terminated"));
488
432
  }
489
433
  this.pending.clear();
490
434
  }
435
+ leaveLeader() {
436
+ try {
437
+ const message = {
438
+ type: CLIENT_LEAVE,
439
+ clientId: this.clientId
440
+ };
441
+ this.channel.postMessage(message);
442
+ } catch {
443
+ }
444
+ }
491
445
  };
446
+ var TransactionSerializingWorkerBridge = class {
447
+ inner;
448
+ leaderClientId = createClientId();
449
+ transactionIdleTimeoutMs;
450
+ queue = [];
451
+ activeTransactionClient = null;
452
+ abortedClients = /* @__PURE__ */ new Set();
453
+ transactionIdleTimer = null;
454
+ reclaiming = false;
455
+ processing = false;
456
+ terminated = false;
457
+ constructor(inner, transactionIdleTimeoutMs = DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS) {
458
+ this.inner = inner;
459
+ this.transactionIdleTimeoutMs = transactionIdleTimeoutMs;
460
+ }
461
+ send(request, clientId = this.leaderClientId) {
462
+ if (this.terminated) {
463
+ return Promise.resolve({
464
+ id: request.id,
465
+ type: "error",
466
+ message: "Worker has been terminated",
467
+ code: "WORKER_TERMINATED"
468
+ });
469
+ }
470
+ if (this.abortedClients.has(clientId)) {
471
+ if (request.type === "begin") {
472
+ this.abortedClients.delete(clientId);
473
+ } else {
474
+ return Promise.resolve({
475
+ id: request.id,
476
+ type: "error",
477
+ message: "Previous transaction was aborted because the client stopped sending requests.",
478
+ code: "TRANSACTION_ABORTED"
479
+ });
480
+ }
481
+ }
482
+ return new Promise((resolve, reject) => {
483
+ this.queue.push({ clientId, request, resolve, reject });
484
+ void this.processQueue();
485
+ });
486
+ }
487
+ terminate() {
488
+ if (this.terminated) {
489
+ return;
490
+ }
491
+ this.terminated = true;
492
+ this.clearTransactionIdleTimer();
493
+ this.inner.terminate();
494
+ const pending = this.queue.splice(0);
495
+ for (const entry of pending) {
496
+ entry.reject(new Error("Worker terminated"));
497
+ }
498
+ }
499
+ reclaimClient(clientId, reason) {
500
+ if (this.terminated) {
501
+ return;
502
+ }
503
+ void this.reclaimClientNow(clientId, reason);
504
+ }
505
+ async processQueue() {
506
+ if (this.processing || this.reclaiming) {
507
+ return;
508
+ }
509
+ this.processing = true;
510
+ try {
511
+ while (!this.terminated) {
512
+ const index = this.nextRunnableIndex();
513
+ if (index === -1) {
514
+ return;
515
+ }
516
+ const [entry] = this.queue.splice(index, 1);
517
+ if (!entry) {
518
+ return;
519
+ }
520
+ try {
521
+ const response = await this.inner.send(entry.request);
522
+ this.recordTransactionState(entry, response);
523
+ entry.resolve(response);
524
+ } catch (error) {
525
+ this.resetFailedTransaction(entry);
526
+ entry.reject(error instanceof Error ? error : new Error(String(error)));
527
+ }
528
+ }
529
+ } finally {
530
+ this.processing = false;
531
+ if (!this.terminated && !this.reclaiming && this.nextRunnableIndex() !== -1) {
532
+ void this.processQueue();
533
+ }
534
+ }
535
+ }
536
+ nextRunnableIndex() {
537
+ if (this.queue.length === 0) {
538
+ return -1;
539
+ }
540
+ if (this.activeTransactionClient === null) {
541
+ return 0;
542
+ }
543
+ return this.queue.findIndex((entry) => entry.clientId === this.activeTransactionClient);
544
+ }
545
+ recordTransactionState(entry, response) {
546
+ if (response.type === "error") {
547
+ this.resetFailedTransaction(entry);
548
+ return;
549
+ }
550
+ if (entry.request.type === "begin") {
551
+ this.activeTransactionClient = entry.clientId;
552
+ this.armTransactionIdleTimer(entry.clientId);
553
+ return;
554
+ }
555
+ if (this.activeTransactionClient === entry.clientId) {
556
+ this.armTransactionIdleTimer(entry.clientId);
557
+ }
558
+ if (this.activeTransactionClient === entry.clientId && (entry.request.type === "commit" || entry.request.type === "rollback")) {
559
+ this.activeTransactionClient = null;
560
+ this.clearTransactionIdleTimer();
561
+ }
562
+ }
563
+ resetFailedTransaction(entry) {
564
+ if (this.activeTransactionClient === entry.clientId) {
565
+ this.activeTransactionClient = null;
566
+ this.clearTransactionIdleTimer();
567
+ }
568
+ }
569
+ armTransactionIdleTimer(clientId) {
570
+ this.clearTransactionIdleTimer();
571
+ this.transactionIdleTimer = setTimeout(() => {
572
+ void this.reclaimClientNow(clientId, "transaction-idle-timeout");
573
+ }, this.transactionIdleTimeoutMs);
574
+ }
575
+ clearTransactionIdleTimer() {
576
+ if (this.transactionIdleTimer) {
577
+ clearTimeout(this.transactionIdleTimer);
578
+ this.transactionIdleTimer = null;
579
+ }
580
+ }
581
+ async reclaimClientNow(clientId, reason) {
582
+ if (this.reclaiming || this.activeTransactionClient !== clientId) {
583
+ return;
584
+ }
585
+ this.reclaiming = true;
586
+ this.clearTransactionIdleTimer();
587
+ this.activeTransactionClient = null;
588
+ this.abortedClients.add(clientId);
589
+ this.rejectQueuedClientRequests(clientId, reason);
590
+ try {
591
+ const response = await this.inner.send({ id: 0, type: "rollback" });
592
+ if (response.type === "error") {
593
+ }
594
+ } catch {
595
+ } finally {
596
+ this.reclaiming = false;
597
+ if (!this.terminated && this.nextRunnableIndex() !== -1) {
598
+ void this.processQueue();
599
+ }
600
+ }
601
+ }
602
+ rejectQueuedClientRequests(clientId, reason) {
603
+ const keep = [];
604
+ for (const entry of this.queue) {
605
+ if (entry.clientId === clientId) {
606
+ entry.resolve({
607
+ id: entry.request.id,
608
+ type: "error",
609
+ message: `Transaction client was reclaimed: ${reason}`,
610
+ code: "TRANSACTION_ABORTED"
611
+ });
612
+ } else {
613
+ keep.push(entry);
614
+ }
615
+ }
616
+ this.queue = keep;
617
+ }
618
+ };
619
+ function hasReclaimClient(bridge) {
620
+ return typeof bridge.reclaimClient === "function";
621
+ }
622
+ function createClientId() {
623
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
624
+ return crypto.randomUUID();
625
+ }
626
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
627
+ }
492
628
 
493
629
  // src/adapters/sqlite-wasm-adapter.ts
494
630
  init_sqlite_wasm_channel();
631
+ var warnedSharedWorkerDeprecated = false;
495
632
  var SqliteWasmAdapter = class {
496
633
  bridge = null;
497
634
  opened = false;
@@ -503,7 +640,8 @@ var SqliteWasmAdapter = class {
503
640
  dbName;
504
641
  emitter;
505
642
  tabSession = null;
506
- sharedWorker = null;
643
+ emitNonPersistentDiagnostic;
644
+ storageOpenState = null;
507
645
  /** Retained so a follower promoted to leader can re-open its own worker. */
508
646
  schema = null;
509
647
  promoting = false;
@@ -514,47 +652,62 @@ var SqliteWasmAdapter = class {
514
652
  this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
515
653
  this.dbName = options.dbName ?? "kora-db";
516
654
  this.emitter = options.emitter;
655
+ this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
517
656
  }
518
657
  async open(schema) {
519
658
  if (this.opened) return;
659
+ const ddlStatements = (0, import_core2.generateFullDDL)(schema);
520
660
  if (this.injectedBridge) {
521
661
  this.bridge = this.injectedBridge;
522
- } else if (this.sharedWorkerUrl && this.workerUrl && isSharedWorkerStorageSupported()) {
523
- const workerUrlString = typeof this.workerUrl === "string" ? this.workerUrl : this.workerUrl.href;
524
- const sharedUrl = typeof this.sharedWorkerUrl === "string" ? this.sharedWorkerUrl : this.sharedWorkerUrl.href;
525
- this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
526
- this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
527
- } else if (this.workerUrl) {
528
- this.schema = schema;
529
- this.tabSession = await acquireTabStorageSession(this.dbName, {
530
- onPromote: () => {
531
- void this.promoteToLeader();
532
- }
533
- });
534
- const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
535
- if (this.tabSession.role === "leader") {
536
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
537
- this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
538
- this.bridge = workerBridge;
539
- } else {
540
- const followerBridge = new FollowerBroadcastBridge(
541
- this.tabSession.channelName,
542
- this.workerResponseTimeoutMs
543
- );
544
- this.bridge = followerBridge;
545
- this.emitter?.emit({
546
- type: "store:db-name-collision",
547
- dbName: this.dbName,
548
- 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.`
549
- });
550
- await followerBridge.waitForLeader();
662
+ const response = await this.openCurrentBridge(ddlStatements);
663
+ this.reportStorageMode(response.data);
664
+ this.opened = true;
665
+ return;
666
+ }
667
+ if (this.workerUrl) {
668
+ this.warnSharedWorkerDeprecated();
669
+ await this.openDurableWorkerBridge(schema);
670
+ const response = await this.openCurrentBridge(ddlStatements);
671
+ this.reportStorageMode(response.data);
672
+ this.opened = true;
673
+ return;
674
+ }
675
+ throw new AdapterError(
676
+ 'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
677
+ );
678
+ }
679
+ async openDurableWorkerBridge(schema) {
680
+ if (!this.workerUrl) {
681
+ throw new AdapterError("Durable SQLite WASM storage requires workerUrl.");
682
+ }
683
+ this.schema = schema;
684
+ this.tabSession = await acquireTabStorageSession(this.dbName, {
685
+ onPromote: () => {
686
+ void this.promoteToLeader();
551
687
  }
552
- } else {
553
- throw new AdapterError(
554
- 'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
688
+ });
689
+ const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
690
+ if (this.tabSession.role === "leader") {
691
+ const workerBridge = new TransactionSerializingWorkerBridge(
692
+ new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
555
693
  );
694
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
695
+ this.bridge = workerBridge;
696
+ return;
556
697
  }
557
- const ddlStatements = (0, import_core2.generateFullDDL)(schema);
698
+ const followerBridge = new FollowerBroadcastBridge(
699
+ this.tabSession.channelName,
700
+ this.workerResponseTimeoutMs
701
+ );
702
+ this.bridge = followerBridge;
703
+ this.emitter?.emit({
704
+ type: "store:db-name-collision",
705
+ dbName: this.dbName,
706
+ 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.`
707
+ });
708
+ await followerBridge.waitForLeader();
709
+ }
710
+ async openCurrentBridge(ddlStatements) {
558
711
  const response = await this.sendRequest({
559
712
  id: 0,
560
713
  type: "open",
@@ -567,8 +720,7 @@ var SqliteWasmAdapter = class {
567
720
  dbName: this.dbName
568
721
  });
569
722
  }
570
- this.reportStorageMode(response.data);
571
- this.opened = true;
723
+ return response;
572
724
  }
573
725
  /**
574
726
  * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
@@ -577,12 +729,12 @@ var SqliteWasmAdapter = class {
577
729
  * bridge that does not report a mode, e.g. the Node mock) emits nothing.
578
730
  */
579
731
  reportStorageMode(data) {
580
- if (!this.emitter || typeof data !== "object" || data === null) {
732
+ this.storageOpenState = parseStorageOpenState(data);
733
+ if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
581
734
  return;
582
735
  }
583
- const mode = data;
584
- if (mode.persistent === false) {
585
- const reason = mode.fallbackReason ?? "unsupported";
736
+ if (this.storageOpenState.persistent === false) {
737
+ const reason = this.storageOpenState.fallbackReason ?? "unsupported";
586
738
  this.emitter.emit({
587
739
  type: "store:opfs-unavailable",
588
740
  dbName: this.dbName,
@@ -591,6 +743,18 @@ var SqliteWasmAdapter = class {
591
743
  });
592
744
  }
593
745
  }
746
+ getStorageOpenState() {
747
+ return this.storageOpenState;
748
+ }
749
+ warnSharedWorkerDeprecated() {
750
+ if (!this.sharedWorkerUrl || warnedSharedWorkerDeprecated) {
751
+ return;
752
+ }
753
+ warnedSharedWorkerDeprecated = true;
754
+ console.warn(
755
+ "[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."
756
+ );
757
+ }
594
758
  async close() {
595
759
  if (!this.bridge) return;
596
760
  try {
@@ -602,7 +766,6 @@ var SqliteWasmAdapter = class {
602
766
  await this.tabSession.releaseLock();
603
767
  }
604
768
  this.tabSession = null;
605
- this.sharedWorker = null;
606
769
  this.bridge.terminate();
607
770
  this.bridge = null;
608
771
  this.opened = false;
@@ -621,7 +784,9 @@ var SqliteWasmAdapter = class {
621
784
  }
622
785
  this.promoting = true;
623
786
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
624
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
787
+ const workerBridge = new TransactionSerializingWorkerBridge(
788
+ new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
789
+ );
625
790
  const previousBridge = this.bridge;
626
791
  this.bridge = workerBridge;
627
792
  if (this.tabSession) {
@@ -752,6 +917,20 @@ var SqliteWasmAdapter = class {
752
917
  }
753
918
  }
754
919
  };
920
+ function parseStorageOpenState(data) {
921
+ if (typeof data !== "object" || data === null) {
922
+ return null;
923
+ }
924
+ const mode = data;
925
+ if (typeof mode.persistent !== "boolean") {
926
+ return null;
927
+ }
928
+ return {
929
+ persistent: mode.persistent,
930
+ mode: mode.persistent ? "opfs" : "memory",
931
+ ...mode.fallbackReason ? { fallbackReason: mode.fallbackReason } : {}
932
+ };
933
+ }
755
934
 
756
935
  // src/adapters/sqlite-wasm.ts
757
936
  init_sqlite_wasm_channel();