@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.
- package/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/indexeddb.cjs +347 -132
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +4 -2
- package/dist/adapters/indexeddb.d.ts +4 -2
- package/dist/adapters/indexeddb.js +42 -5
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +250 -3
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +308 -129
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +18 -7
- package/dist/adapters/sqlite-wasm.d.ts +18 -7
- package/dist/adapters/sqlite-wasm.js +2 -2
- package/dist/{chunk-Q57CHIIN.js → chunk-RJIEKEN4.js} +310 -130
- package/dist/chunk-RJIEKEN4.js.map +1 -0
- package/dist/{chunk-COJSQBXW.js → chunk-VDBJQ75K.js} +1 -1
- package/dist/chunk-VDBJQ75K.js.map +1 -0
- package/dist/{chunk-RL43UYQZ.js → chunk-YT7MRFCP.js} +5 -5
- package/dist/index.cjs +26 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +44 -18
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +2 -2
- package/dist/{sqlite-wasm-channel-NGSBJR44.js → sqlite-wasm-channel-GGHA2BXU.js} +2 -2
- package/dist/{sqlite-wasm-channel-Bx57s8Cq.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +2 -2
- package/dist/{sqlite-wasm-channel-Bx57s8Cq.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +2 -2
- package/dist/{types-C1kigrWz.d.ts → types-BOO9EPt1.d.ts} +1 -1
- package/dist/{types-D1MzKVmk.d.ts → types-BslDOTk0.d.cts} +16 -1
- package/dist/{types-D1MzKVmk.d.cts → types-BslDOTk0.d.ts} +16 -1
- package/dist/{types-DAo9oduO.d.cts → types-C_GDavG-.d.cts} +1 -1
- package/package.json +2 -12
- package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -345
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -82
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -82
- package/dist/adapters/sqlite-wasm-shared-host.js +0 -61
- package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
- package/dist/chunk-3Y2IERBF.js +0 -255
- package/dist/chunk-3Y2IERBF.js.map +0 -1
- package/dist/chunk-COJSQBXW.js.map +0 -1
- package/dist/chunk-Q57CHIIN.js.map +0 -1
- /package/dist/{chunk-RL43UYQZ.js.map → chunk-YT7MRFCP.js.map} +0 -0
- /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-
|
|
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-
|
|
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
|
-
|
|
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 =
|
|
538
|
-
const msg = {
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.
|
|
613
|
-
|
|
614
|
-
this.
|
|
615
|
-
this.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
815
|
+
this.storageOpenState = parseStorageOpenState(data);
|
|
816
|
+
if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
|
|
667
817
|
return;
|
|
668
818
|
}
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
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)
|
|
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
|