@korajs/store 1.0.0-beta.0 → 1.0.0-beta.10
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.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/indexeddb.cjs +523 -148
- 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 +55 -11
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +207 -187
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +208 -188
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +474 -140
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +45 -8
- package/dist/adapters/sqlite-wasm.d.ts +45 -8
- package/dist/adapters/sqlite-wasm.js +3 -3
- package/dist/{backup-5XZ7THNJ.js → backup-5S7K2Z35.js} +3 -3
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -0
- package/dist/{chunk-EIVXWF4B.js → chunk-CYO3YKFL.js} +4 -3
- package/dist/chunk-CYO3YKFL.js.map +1 -0
- package/dist/{chunk-ETDJEH2I.js → chunk-GSJ7EEUC.js} +12 -1
- package/dist/chunk-GSJ7EEUC.js.map +1 -0
- package/dist/{chunk-5R4QMY3Z.js → chunk-HS6DBMST.js} +13 -10
- package/dist/chunk-HS6DBMST.js.map +1 -0
- package/dist/chunk-RJIEKEN4.js +751 -0
- package/dist/chunk-RJIEKEN4.js.map +1 -0
- package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
- package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
- package/dist/{chunk-IOEFNHHP.js → chunk-VDBJQ75K.js} +2 -2
- package/dist/chunk-VDBJQ75K.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-OUQDC5Z6.js → chunk-YT7MRFCP.js} +55 -35
- package/dist/chunk-YT7MRFCP.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/{export-audit-VPGQWNR6.js → export-audit-ODGQTQIK.js} +3 -3
- package/dist/index.cjs +923 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +443 -6
- package/dist/index.d.ts +443 -6
- package/dist/index.js +737 -49
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +69 -43
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +12 -0
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +1 -1
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +1 -1
- package/dist/{types-CyjXwrjW.d.cts → types-BOO9EPt1.d.ts} +10 -3
- package/dist/{types-CJOVKxLw.d.ts → types-BslDOTk0.d.cts} +32 -2
- package/dist/{types-CJOVKxLw.d.cts → types-BslDOTk0.d.ts} +32 -2
- package/dist/{types-DAxCladl.d.ts → types-C_GDavG-.d.cts} +10 -3
- package/package.json +13 -12
- package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -68
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.js +0 -66
- package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
- package/dist/chunk-5R4QMY3Z.js.map +0 -1
- package/dist/chunk-EIVXWF4B.js.map +0 -1
- package/dist/chunk-ETDJEH2I.js.map +0 -1
- package/dist/chunk-IOEFNHHP.js.map +0 -1
- package/dist/chunk-OUQDC5Z6.js.map +0 -1
- package/dist/chunk-RBPVEAVM.js +0 -425
- package/dist/chunk-RBPVEAVM.js.map +0 -1
- package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
- /package/dist/{backup-5XZ7THNJ.js.map → backup-5S7K2Z35.js.map} +0 -0
- /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-ODGQTQIK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
|
@@ -21,7 +21,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
21
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
22
|
|
|
23
23
|
// src/errors.ts
|
|
24
|
-
var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, PersistenceError;
|
|
24
|
+
var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError, PersistenceError;
|
|
25
25
|
var init_errors = __esm({
|
|
26
26
|
"src/errors.ts"() {
|
|
27
27
|
"use strict";
|
|
@@ -48,6 +48,16 @@ var init_errors = __esm({
|
|
|
48
48
|
this.name = "WorkerTimeoutError";
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
NoLeaderError = class extends import_core.KoraError {
|
|
52
|
+
constructor(operation) {
|
|
53
|
+
super(
|
|
54
|
+
`No live leader tab is answering multi-tab storage RPC for operation "${operation}"`,
|
|
55
|
+
"NO_LEADER",
|
|
56
|
+
{ operation }
|
|
57
|
+
);
|
|
58
|
+
this.name = "NoLeaderError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
51
61
|
PersistenceError = class extends import_core.KoraError {
|
|
52
62
|
constructor(message, context) {
|
|
53
63
|
super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
|
|
@@ -190,6 +200,7 @@ __export(indexeddb_exports, {
|
|
|
190
200
|
module.exports = __toCommonJS(indexeddb_exports);
|
|
191
201
|
|
|
192
202
|
// src/adapters/indexeddb-adapter.ts
|
|
203
|
+
var import_core3 = require("@korajs/core");
|
|
193
204
|
init_errors();
|
|
194
205
|
|
|
195
206
|
// src/adapters/indexeddb-persistence-scheduler.ts
|
|
@@ -271,130 +282,86 @@ var IndexedDbPersistenceScheduler = class {
|
|
|
271
282
|
var import_core2 = require("@korajs/core");
|
|
272
283
|
init_errors();
|
|
273
284
|
|
|
274
|
-
// src/multi-tab/shared-worker-bridge.ts
|
|
275
|
-
init_errors();
|
|
276
|
-
var SW_REQUEST = "kora-sw-request";
|
|
277
|
-
var SW_RESPONSE = "kora-sw-response";
|
|
278
|
-
var SharedWorkerClientBridge = class {
|
|
279
|
-
constructor(sharedWorker, dbName, workerUrl, timeoutMs = 3e4) {
|
|
280
|
-
this.dbName = dbName;
|
|
281
|
-
this.workerUrl = workerUrl;
|
|
282
|
-
this.timeoutMs = timeoutMs;
|
|
283
|
-
this.port = sharedWorker.port;
|
|
284
|
-
this.port.start();
|
|
285
|
-
this.port.addEventListener("message", (event) => {
|
|
286
|
-
const data = event.data;
|
|
287
|
-
if (data?.type !== SW_RESPONSE) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const entry = this.pending.get(data.requestId);
|
|
291
|
-
if (entry) {
|
|
292
|
-
this.pending.delete(data.requestId);
|
|
293
|
-
entry.resolve(data.response);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
dbName;
|
|
298
|
-
workerUrl;
|
|
299
|
-
port;
|
|
300
|
-
pending = /* @__PURE__ */ new Map();
|
|
301
|
-
timeoutMs;
|
|
302
|
-
terminated = false;
|
|
303
|
-
async send(request) {
|
|
304
|
-
if (this.terminated) {
|
|
305
|
-
return {
|
|
306
|
-
id: request.id,
|
|
307
|
-
type: "error",
|
|
308
|
-
message: "SharedWorker bridge terminated",
|
|
309
|
-
code: "BRIDGE_TERMINATED"
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
const requestId = crypto.randomUUID();
|
|
313
|
-
const msg = {
|
|
314
|
-
type: SW_REQUEST,
|
|
315
|
-
requestId,
|
|
316
|
-
dbName: this.dbName,
|
|
317
|
-
workerUrl: this.workerUrl,
|
|
318
|
-
request
|
|
319
|
-
};
|
|
320
|
-
return new Promise((resolve, reject) => {
|
|
321
|
-
const timer = setTimeout(() => {
|
|
322
|
-
this.pending.delete(requestId);
|
|
323
|
-
reject(new WorkerTimeoutError(`shared-worker:${request.type}`, this.timeoutMs));
|
|
324
|
-
}, this.timeoutMs);
|
|
325
|
-
this.pending.set(requestId, {
|
|
326
|
-
resolve: (response) => {
|
|
327
|
-
clearTimeout(timer);
|
|
328
|
-
resolve(response);
|
|
329
|
-
},
|
|
330
|
-
reject: (error) => {
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
reject(error);
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
this.port.postMessage(msg);
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
terminate() {
|
|
339
|
-
if (this.terminated) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
this.terminated = true;
|
|
343
|
-
this.port.close();
|
|
344
|
-
for (const [, entry] of this.pending) {
|
|
345
|
-
entry.reject(new Error("SharedWorker bridge terminated"));
|
|
346
|
-
}
|
|
347
|
-
this.pending.clear();
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
285
|
// src/multi-tab/tab-storage.ts
|
|
352
286
|
init_errors();
|
|
353
287
|
var RPC_REQUEST = "kora-worker-request";
|
|
354
288
|
var RPC_RESPONSE = "kora-worker-response";
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
289
|
+
var CLIENT_LEAVE = "kora-client-leave";
|
|
290
|
+
var LEADER_PING = "kora-leader-ping";
|
|
291
|
+
var LEADER_PONG = "kora-leader-pong";
|
|
292
|
+
var DEFAULT_LIVENESS_PROBE_MS = 2e3;
|
|
293
|
+
var DEFAULT_TRANSACTION_IDLE_TIMEOUT_MS = 1e4;
|
|
358
294
|
function isMultiTabStorageSupported() {
|
|
359
295
|
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
360
296
|
}
|
|
361
|
-
async function acquireTabStorageSession(dbName) {
|
|
297
|
+
async function acquireTabStorageSession(dbName, options) {
|
|
362
298
|
const channelName = `kora-storage-${dbName}`;
|
|
299
|
+
const lockName = `kora-leader-${dbName}`;
|
|
363
300
|
if (!isMultiTabStorageSupported()) {
|
|
364
301
|
return { role: "leader", channelName };
|
|
365
302
|
}
|
|
366
303
|
return new Promise((resolve) => {
|
|
367
304
|
let releaseHeld;
|
|
368
|
-
void navigator.locks.request(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (lock === null) {
|
|
373
|
-
resolve({ role: "follower", channelName });
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
resolve({
|
|
377
|
-
role: "leader",
|
|
378
|
-
channelName,
|
|
379
|
-
releaseLock: async () => {
|
|
380
|
-
releaseHeld?.();
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
return new Promise((release) => {
|
|
384
|
-
releaseHeld = release;
|
|
385
|
-
});
|
|
305
|
+
void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
|
|
306
|
+
if (lock === null) {
|
|
307
|
+
resolve(startFollowerSession(channelName, lockName, options));
|
|
308
|
+
return;
|
|
386
309
|
}
|
|
387
|
-
|
|
310
|
+
resolve({
|
|
311
|
+
role: "leader",
|
|
312
|
+
channelName,
|
|
313
|
+
releaseLock: async () => {
|
|
314
|
+
releaseHeld?.();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return new Promise((release) => {
|
|
318
|
+
releaseHeld = release;
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function startFollowerSession(channelName, lockName, options) {
|
|
324
|
+
let promoted = false;
|
|
325
|
+
let releasePromotedLock;
|
|
326
|
+
const abort = new AbortController();
|
|
327
|
+
void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
|
|
328
|
+
promoted = true;
|
|
329
|
+
options?.onPromote?.();
|
|
330
|
+
return new Promise((release) => {
|
|
331
|
+
releasePromotedLock = release;
|
|
332
|
+
});
|
|
333
|
+
}).catch(() => {
|
|
388
334
|
});
|
|
335
|
+
return {
|
|
336
|
+
role: "follower",
|
|
337
|
+
channelName,
|
|
338
|
+
cancelPromotionWatch: () => {
|
|
339
|
+
if (promoted) {
|
|
340
|
+
releasePromotedLock?.();
|
|
341
|
+
} else {
|
|
342
|
+
abort.abort();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
389
346
|
}
|
|
390
347
|
function startLeaderRpcRelay(channelName, bridge) {
|
|
391
348
|
const channel = new BroadcastChannel(channelName);
|
|
392
349
|
const onMessage = (event) => {
|
|
393
350
|
const data = event.data;
|
|
351
|
+
if (data?.type === LEADER_PING) {
|
|
352
|
+
channel.postMessage({ type: LEADER_PONG });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (data?.type === CLIENT_LEAVE) {
|
|
356
|
+
if (hasReclaimClient(bridge)) {
|
|
357
|
+
bridge.reclaimClient(data.clientId, "client-left");
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
394
361
|
if (data?.type !== RPC_REQUEST) {
|
|
395
362
|
return;
|
|
396
363
|
}
|
|
397
|
-
void bridge.send(data.request).then((response) => {
|
|
364
|
+
void bridge.send(data.request, data.clientId).then((response) => {
|
|
398
365
|
const msg = {
|
|
399
366
|
type: RPC_RESPONSE,
|
|
400
367
|
requestId: data.requestId,
|
|
@@ -424,12 +391,21 @@ function startLeaderRpcRelay(channelName, bridge) {
|
|
|
424
391
|
}
|
|
425
392
|
var FollowerBroadcastBridge = class {
|
|
426
393
|
channel;
|
|
394
|
+
clientId = createClientId();
|
|
395
|
+
onPageHide = () => {
|
|
396
|
+
this.leaveLeader();
|
|
397
|
+
};
|
|
427
398
|
pending = /* @__PURE__ */ new Map();
|
|
428
399
|
timeoutMs;
|
|
400
|
+
livenessProbeMs;
|
|
429
401
|
terminated = false;
|
|
430
|
-
constructor(channelName, timeoutMs = 3e4) {
|
|
402
|
+
constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
|
|
431
403
|
this.timeoutMs = timeoutMs;
|
|
404
|
+
this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
|
|
432
405
|
this.channel = new BroadcastChannel(channelName);
|
|
406
|
+
if (typeof addEventListener === "function") {
|
|
407
|
+
addEventListener("pagehide", this.onPageHide);
|
|
408
|
+
}
|
|
433
409
|
this.channel.addEventListener("message", (event) => {
|
|
434
410
|
const data = event.data;
|
|
435
411
|
if (data?.type !== RPC_RESPONSE) {
|
|
@@ -442,6 +418,48 @@ var FollowerBroadcastBridge = class {
|
|
|
442
418
|
}
|
|
443
419
|
});
|
|
444
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
423
|
+
* or `false` if none answers within the budget. The adapter calls this before
|
|
424
|
+
* its first RPC so a follower created before a leader relay is live retries the
|
|
425
|
+
* handshake instead of firing into the void and waiting out the full timeout.
|
|
426
|
+
*/
|
|
427
|
+
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
428
|
+
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
429
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
430
|
+
if (this.terminated) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (await this.pingLeader(perAttempt)) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
/** Sends one liveness ping and resolves whether a leader answered in time. */
|
|
440
|
+
pingLeader(timeoutMs) {
|
|
441
|
+
if (this.terminated) {
|
|
442
|
+
return Promise.resolve(false);
|
|
443
|
+
}
|
|
444
|
+
return new Promise((resolve) => {
|
|
445
|
+
const onPong = (event) => {
|
|
446
|
+
if (event.data?.type === LEADER_PONG) {
|
|
447
|
+
cleanup();
|
|
448
|
+
resolve(true);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const cleanup = () => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
this.channel.removeEventListener("message", onPong);
|
|
454
|
+
};
|
|
455
|
+
const timer = setTimeout(() => {
|
|
456
|
+
cleanup();
|
|
457
|
+
resolve(false);
|
|
458
|
+
}, timeoutMs);
|
|
459
|
+
this.channel.addEventListener("message", onPong);
|
|
460
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
461
|
+
});
|
|
462
|
+
}
|
|
445
463
|
async send(request) {
|
|
446
464
|
if (this.terminated) {
|
|
447
465
|
return {
|
|
@@ -451,22 +469,33 @@ var FollowerBroadcastBridge = class {
|
|
|
451
469
|
code: "BRIDGE_TERMINATED"
|
|
452
470
|
};
|
|
453
471
|
}
|
|
454
|
-
const requestId =
|
|
455
|
-
const msg = {
|
|
472
|
+
const requestId = createClientId();
|
|
473
|
+
const msg = {
|
|
474
|
+
type: RPC_REQUEST,
|
|
475
|
+
requestId,
|
|
476
|
+
clientId: this.clientId,
|
|
477
|
+
request
|
|
478
|
+
};
|
|
456
479
|
return new Promise((resolve, reject) => {
|
|
457
|
-
const
|
|
480
|
+
const settle = (fn) => {
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
clearTimeout(probe);
|
|
458
483
|
this.pending.delete(requestId);
|
|
459
|
-
|
|
484
|
+
fn();
|
|
485
|
+
};
|
|
486
|
+
const timer = setTimeout(() => {
|
|
487
|
+
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
|
|
460
488
|
}, this.timeoutMs);
|
|
489
|
+
const probe = setTimeout(() => {
|
|
490
|
+
void this.pingLeader(this.livenessProbeMs).then((alive) => {
|
|
491
|
+
if (!alive && this.pending.has(requestId)) {
|
|
492
|
+
settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}, this.livenessProbeMs);
|
|
461
496
|
this.pending.set(requestId, {
|
|
462
|
-
resolve: (response) =>
|
|
463
|
-
|
|
464
|
-
resolve(response);
|
|
465
|
-
},
|
|
466
|
-
reject: (error) => {
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
reject(error);
|
|
469
|
-
}
|
|
497
|
+
resolve: (response) => settle(() => resolve(response)),
|
|
498
|
+
reject: (error) => settle(() => reject(error))
|
|
470
499
|
});
|
|
471
500
|
this.channel.postMessage(msg);
|
|
472
501
|
});
|
|
@@ -476,16 +505,213 @@ var FollowerBroadcastBridge = class {
|
|
|
476
505
|
return;
|
|
477
506
|
}
|
|
478
507
|
this.terminated = true;
|
|
508
|
+
this.leaveLeader();
|
|
509
|
+
if (typeof removeEventListener === "function") {
|
|
510
|
+
removeEventListener("pagehide", this.onPageHide);
|
|
511
|
+
}
|
|
479
512
|
this.channel.close();
|
|
480
513
|
for (const [, entry] of this.pending) {
|
|
481
514
|
entry.reject(new Error("Follower bridge terminated"));
|
|
482
515
|
}
|
|
483
516
|
this.pending.clear();
|
|
484
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
|
+
}
|
|
485
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
|
+
}
|
|
486
711
|
|
|
487
712
|
// src/adapters/sqlite-wasm-adapter.ts
|
|
488
713
|
init_sqlite_wasm_channel();
|
|
714
|
+
var warnedSharedWorkerDeprecated = false;
|
|
489
715
|
var SqliteWasmAdapter = class {
|
|
490
716
|
bridge = null;
|
|
491
717
|
opened = false;
|
|
@@ -495,43 +721,76 @@ var SqliteWasmAdapter = class {
|
|
|
495
721
|
sharedWorkerUrl;
|
|
496
722
|
workerResponseTimeoutMs;
|
|
497
723
|
dbName;
|
|
724
|
+
emitter;
|
|
498
725
|
tabSession = null;
|
|
499
|
-
|
|
726
|
+
emitNonPersistentDiagnostic;
|
|
727
|
+
storageOpenState = null;
|
|
728
|
+
/** Retained so a follower promoted to leader can re-open its own worker. */
|
|
729
|
+
schema = null;
|
|
730
|
+
promoting = false;
|
|
500
731
|
constructor(options = {}) {
|
|
501
732
|
this.injectedBridge = options.bridge;
|
|
502
733
|
this.workerUrl = options.workerUrl;
|
|
503
734
|
this.sharedWorkerUrl = options.sharedWorkerUrl;
|
|
504
735
|
this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
|
|
505
736
|
this.dbName = options.dbName ?? "kora-db";
|
|
737
|
+
this.emitter = options.emitter;
|
|
738
|
+
this.emitNonPersistentDiagnostic = options.emitNonPersistentDiagnostic ?? true;
|
|
506
739
|
}
|
|
507
740
|
async open(schema) {
|
|
508
741
|
if (this.opened) return;
|
|
742
|
+
const ddlStatements = (0, import_core2.generateFullDDL)(schema);
|
|
509
743
|
if (this.injectedBridge) {
|
|
510
744
|
this.bridge = this.injectedBridge;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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();
|
|
528
770
|
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
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)
|
|
532
776
|
);
|
|
777
|
+
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
778
|
+
this.bridge = workerBridge;
|
|
779
|
+
return;
|
|
533
780
|
}
|
|
534
|
-
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) {
|
|
535
794
|
const response = await this.sendRequest({
|
|
536
795
|
id: 0,
|
|
537
796
|
type: "open",
|
|
@@ -544,7 +803,40 @@ var SqliteWasmAdapter = class {
|
|
|
544
803
|
dbName: this.dbName
|
|
545
804
|
});
|
|
546
805
|
}
|
|
547
|
-
|
|
806
|
+
return response;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Surface a `store:opfs-unavailable` diagnostic when the worker reported that
|
|
810
|
+
* persistence silently degraded to an in-memory database, so a data-loss
|
|
811
|
+
* condition is observable instead of failing silently. A persistent open (or a
|
|
812
|
+
* bridge that does not report a mode, e.g. the Node mock) emits nothing.
|
|
813
|
+
*/
|
|
814
|
+
reportStorageMode(data) {
|
|
815
|
+
this.storageOpenState = parseStorageOpenState(data);
|
|
816
|
+
if (!this.emitNonPersistentDiagnostic || !this.emitter || !this.storageOpenState) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (this.storageOpenState.persistent === false) {
|
|
820
|
+
const reason = this.storageOpenState.fallbackReason ?? "unsupported";
|
|
821
|
+
this.emitter.emit({
|
|
822
|
+
type: "store:opfs-unavailable",
|
|
823
|
+
dbName: this.dbName,
|
|
824
|
+
reason,
|
|
825
|
+
message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
|
|
826
|
+
});
|
|
827
|
+
}
|
|
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
|
+
);
|
|
548
840
|
}
|
|
549
841
|
async close() {
|
|
550
842
|
if (!this.bridge) return;
|
|
@@ -552,16 +844,42 @@ var SqliteWasmAdapter = class {
|
|
|
552
844
|
await this.sendRequest({ id: 0, type: "close" });
|
|
553
845
|
} finally {
|
|
554
846
|
this.tabSession?.stopRelay?.();
|
|
847
|
+
this.tabSession?.cancelPromotionWatch?.();
|
|
555
848
|
if (this.tabSession?.releaseLock) {
|
|
556
849
|
await this.tabSession.releaseLock();
|
|
557
850
|
}
|
|
558
851
|
this.tabSession = null;
|
|
559
|
-
this.sharedWorker = null;
|
|
560
852
|
this.bridge.terminate();
|
|
561
853
|
this.bridge = null;
|
|
562
854
|
this.opened = false;
|
|
563
855
|
}
|
|
564
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Promotes a follower to leader after the previous leader released the storage
|
|
859
|
+
* lock (its tab closed or crashed). The lock is only granted once the old leader
|
|
860
|
+
* is gone, so opening our own worker here is safe from the OPFS single-writer
|
|
861
|
+
* rule. In-flight follower RPCs are rejected and left to the caller / reactive
|
|
862
|
+
* layer to retry against the new leader.
|
|
863
|
+
*/
|
|
864
|
+
async promoteToLeader() {
|
|
865
|
+
if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
this.promoting = true;
|
|
869
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
870
|
+
const workerBridge = new TransactionSerializingWorkerBridge(
|
|
871
|
+
new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs)
|
|
872
|
+
);
|
|
873
|
+
const previousBridge = this.bridge;
|
|
874
|
+
this.bridge = workerBridge;
|
|
875
|
+
if (this.tabSession) {
|
|
876
|
+
this.tabSession.role = "leader";
|
|
877
|
+
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
878
|
+
}
|
|
879
|
+
previousBridge?.terminate();
|
|
880
|
+
const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
|
|
881
|
+
await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
|
|
882
|
+
}
|
|
565
883
|
async execute(sql, params) {
|
|
566
884
|
this.guardOpen();
|
|
567
885
|
const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
|
|
@@ -649,7 +967,10 @@ var SqliteWasmAdapter = class {
|
|
|
649
967
|
this.guardOpen();
|
|
650
968
|
const response = await this.sendRequest({ id: 0, type: "export" });
|
|
651
969
|
if (response.type === "error") {
|
|
652
|
-
throw new AdapterError(`Export failed: ${response.message}
|
|
970
|
+
throw new AdapterError(`Export failed: ${response.message}`, {
|
|
971
|
+
code: response.code,
|
|
972
|
+
context: response.context
|
|
973
|
+
});
|
|
653
974
|
}
|
|
654
975
|
return response.data;
|
|
655
976
|
}
|
|
@@ -682,6 +1003,20 @@ var SqliteWasmAdapter = class {
|
|
|
682
1003
|
}
|
|
683
1004
|
}
|
|
684
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
|
+
}
|
|
685
1020
|
|
|
686
1021
|
// src/adapters/sqlite-wasm-persistence.ts
|
|
687
1022
|
init_errors();
|
|
@@ -816,6 +1151,24 @@ async function loadDumpFromIndexedDB(dbName) {
|
|
|
816
1151
|
idb.close();
|
|
817
1152
|
}
|
|
818
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
|
+
}
|
|
819
1172
|
|
|
820
1173
|
// src/adapters/indexeddb-adapter.ts
|
|
821
1174
|
var IndexedDbAdapter = class {
|
|
@@ -823,6 +1176,7 @@ var IndexedDbAdapter = class {
|
|
|
823
1176
|
dbName;
|
|
824
1177
|
emitter;
|
|
825
1178
|
scheduler;
|
|
1179
|
+
storageOpenState = null;
|
|
826
1180
|
constructor(options = {}) {
|
|
827
1181
|
this.dbName = options.dbName ?? "kora-db";
|
|
828
1182
|
this.emitter = options.emitter;
|
|
@@ -840,8 +1194,12 @@ var IndexedDbAdapter = class {
|
|
|
840
1194
|
}
|
|
841
1195
|
async open(schema) {
|
|
842
1196
|
await this.inner.open(schema);
|
|
1197
|
+
this.storageOpenState = { persistent: true, mode: "indexeddb" };
|
|
843
1198
|
const persisted = await loadFromIndexedDB(this.dbName);
|
|
844
|
-
if (!persisted)
|
|
1199
|
+
if (!persisted) {
|
|
1200
|
+
await this.restoreFromDumpFallback();
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
845
1203
|
try {
|
|
846
1204
|
await this.inner.importDatabase(persisted);
|
|
847
1205
|
} catch {
|
|
@@ -868,6 +1226,9 @@ var IndexedDbAdapter = class {
|
|
|
868
1226
|
await this.inner.migrate(from, to, migration);
|
|
869
1227
|
this.scheduler.schedule();
|
|
870
1228
|
}
|
|
1229
|
+
getStorageOpenState() {
|
|
1230
|
+
return this.storageOpenState;
|
|
1231
|
+
}
|
|
871
1232
|
/**
|
|
872
1233
|
* Force an immediate snapshot write to IndexedDB (skips debounce).
|
|
873
1234
|
* Useful before tab unload or in tests.
|
|
@@ -876,10 +1237,17 @@ var IndexedDbAdapter = class {
|
|
|
876
1237
|
await this.scheduler.flushNow();
|
|
877
1238
|
}
|
|
878
1239
|
async writeSnapshot() {
|
|
879
|
-
const data = await this.inner.exportDatabase();
|
|
880
|
-
await saveToIndexedDB(this.dbName, data);
|
|
881
1240
|
const dump = await this.exportDump();
|
|
882
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
|
+
}
|
|
883
1251
|
}
|
|
884
1252
|
handlePersistenceError(error) {
|
|
885
1253
|
const message = error instanceof Error ? error.message : "IndexedDB persistence failed";
|
|
@@ -904,7 +1272,7 @@ var IndexedDbAdapter = class {
|
|
|
904
1272
|
if (!dump) return;
|
|
905
1273
|
for (const table of dump.tables) {
|
|
906
1274
|
const name = ensureSafeIdentifier(table.name);
|
|
907
|
-
await this.inner.execute(`DELETE FROM ${name}`);
|
|
1275
|
+
await this.inner.execute(`DELETE FROM ${(0, import_core3.quoteIdent)(name)}`);
|
|
908
1276
|
if (table.rows.length === 0) continue;
|
|
909
1277
|
for (const row of table.rows) {
|
|
910
1278
|
const columns = table.columns.filter(
|
|
@@ -912,10 +1280,10 @@ var IndexedDbAdapter = class {
|
|
|
912
1280
|
);
|
|
913
1281
|
if (columns.length === 0) continue;
|
|
914
1282
|
const placeholders = columns.map(() => "?").join(", ");
|
|
915
|
-
const quotedColumns = columns.map((column) => ensureSafeIdentifier(column)).join(", ");
|
|
1283
|
+
const quotedColumns = columns.map((column) => (0, import_core3.quoteIdent)(ensureSafeIdentifier(column))).join(", ");
|
|
916
1284
|
const values = columns.map((column) => row[column]);
|
|
917
1285
|
await this.inner.execute(
|
|
918
|
-
`INSERT INTO ${name} (${quotedColumns}) VALUES (${placeholders})`,
|
|
1286
|
+
`INSERT INTO ${(0, import_core3.quoteIdent)(name)} (${quotedColumns}) VALUES (${placeholders})`,
|
|
919
1287
|
values
|
|
920
1288
|
);
|
|
921
1289
|
}
|
|
@@ -928,9 +1296,13 @@ var IndexedDbAdapter = class {
|
|
|
928
1296
|
const tables = [];
|
|
929
1297
|
for (const tableRow of tableRows) {
|
|
930
1298
|
const tableName = ensureSafeIdentifier(tableRow.name);
|
|
931
|
-
const columns = await this.inner.query(
|
|
1299
|
+
const columns = await this.inner.query(
|
|
1300
|
+
`PRAGMA table_info(${(0, import_core3.quoteIdent)(tableName)})`
|
|
1301
|
+
);
|
|
932
1302
|
const columnNames = columns.map((column) => column.name);
|
|
933
|
-
const rows = await this.inner.query(
|
|
1303
|
+
const rows = await this.inner.query(
|
|
1304
|
+
`SELECT * FROM ${(0, import_core3.quoteIdent)(tableName)}`
|
|
1305
|
+
);
|
|
934
1306
|
tables.push({
|
|
935
1307
|
name: tableName,
|
|
936
1308
|
columns: columnNames,
|
|
@@ -949,6 +1321,9 @@ function ensureSafeIdentifier(identifier) {
|
|
|
949
1321
|
}
|
|
950
1322
|
return identifier;
|
|
951
1323
|
}
|
|
1324
|
+
function isUnsupportedWorkerExport(error) {
|
|
1325
|
+
return error instanceof AdapterError && error.context?.code === "EXPORT_NOT_SUPPORTED";
|
|
1326
|
+
}
|
|
952
1327
|
// Annotate the CommonJS export names for ESM import in node:
|
|
953
1328
|
0 && (module.exports = {
|
|
954
1329
|
IndexedDbAdapter
|