@korajs/store 1.0.0-beta.5 → 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 (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 +309 -128
  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.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 +304 -128
  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-FKW56RVZ.js} +306 -129
  18. package/dist/chunk-FKW56RVZ.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 +16 -0
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +12 -4
  25. package/dist/index.d.ts +12 -4
  26. package/dist/index.js +34 -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 +1 -11
  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
@@ -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
+ }
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
+ }
491
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,50 +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, {
526
- type: "module",
527
- name: `kora-sw-${this.dbName}`
528
- });
529
- this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
530
- } else if (this.workerUrl) {
531
- this.schema = schema;
532
- this.tabSession = await acquireTabStorageSession(this.dbName, {
533
- onPromote: () => {
534
- void this.promoteToLeader();
535
- }
536
- });
537
- const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
538
- if (this.tabSession.role === "leader") {
539
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
540
- this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
541
- this.bridge = workerBridge;
542
- } else {
543
- const followerBridge = new FollowerBroadcastBridge(
544
- this.tabSession.channelName,
545
- this.workerResponseTimeoutMs
546
- );
547
- this.bridge = followerBridge;
548
- this.emitter?.emit({
549
- type: "store:db-name-collision",
550
- dbName: this.dbName,
551
- 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.`
552
- });
553
- 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();
554
687
  }
555
- } else {
556
- throw new AdapterError(
557
- '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)
558
693
  );
694
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
695
+ this.bridge = workerBridge;
696
+ return;
559
697
  }
560
- 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) {
561
711
  const response = await this.sendRequest({
562
712
  id: 0,
563
713
  type: "open",
@@ -570,8 +720,7 @@ var SqliteWasmAdapter = class {
570
720
  dbName: this.dbName
571
721
  });
572
722
  }
573
- this.reportStorageMode(response.data);
574
- this.opened = true;
723
+ return response;
575
724
  }
576
725
  /**
577
726
  * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
@@ -580,12 +729,12 @@ var SqliteWasmAdapter = class {
580
729
  * bridge that does not report a mode, e.g. the Node mock) emits nothing.
581
730
  */
582
731
  reportStorageMode(data) {
583
- if (!this.emitter || typeof data !== "object" || data === null) {
732
+ this.storageOpenState = parseStorageOpenState(data);
733
+ if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
584
734
  return;
585
735
  }
586
- const mode = data;
587
- if (mode.persistent === false) {
588
- const reason = mode.fallbackReason ?? "unsupported";
736
+ if (this.storageOpenState.persistent === false) {
737
+ const reason = this.storageOpenState.fallbackReason ?? "unsupported";
589
738
  this.emitter.emit({
590
739
  type: "store:opfs-unavailable",
591
740
  dbName: this.dbName,
@@ -594,6 +743,18 @@ var SqliteWasmAdapter = class {
594
743
  });
595
744
  }
596
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
+ }
597
758
  async close() {
598
759
  if (!this.bridge) return;
599
760
  try {
@@ -605,7 +766,6 @@ var SqliteWasmAdapter = class {
605
766
  await this.tabSession.releaseLock();
606
767
  }
607
768
  this.tabSession = null;
608
- this.sharedWorker = null;
609
769
  this.bridge.terminate();
610
770
  this.bridge = null;
611
771
  this.opened = false;
@@ -624,7 +784,9 @@ var SqliteWasmAdapter = class {
624
784
  }
625
785
  this.promoting = true;
626
786
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
627
- const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
787
+ const workerBridge = new TransactionSerializingWorkerBridge(
788
+ new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
789
+ );
628
790
  const previousBridge = this.bridge;
629
791
  this.bridge = workerBridge;
630
792
  if (this.tabSession) {
@@ -755,6 +917,20 @@ var SqliteWasmAdapter = class {
755
917
  }
756
918
  }
757
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
+ }
758
934
 
759
935
  // src/adapters/sqlite-wasm.ts
760
936
  init_sqlite_wasm_channel();