@korajs/store 0.4.0 → 0.5.0
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 +410 -17
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +23 -5
- package/dist/adapters/indexeddb.d.ts +23 -5
- package/dist/adapters/indexeddb.js +163 -18
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
- package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
- package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +34 -5
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +249 -2
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +15 -3
- package/dist/adapters/sqlite-wasm.d.ts +15 -3
- package/dist/adapters/sqlite-wasm.js +2 -2
- package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
- package/dist/backup-JN3LB4YT.js +14 -0
- package/dist/backup-JN3LB4YT.js.map +1 -0
- package/dist/chunk-56LL4XP6.js +205 -0
- package/dist/chunk-56LL4XP6.js.map +1 -0
- package/dist/chunk-6AHYGQQT.js +425 -0
- package/dist/chunk-6AHYGQQT.js.map +1 -0
- package/dist/chunk-7BPETY5Y.js +570 -0
- package/dist/chunk-7BPETY5Y.js.map +1 -0
- package/dist/chunk-AGN3CIPB.js +188 -0
- package/dist/chunk-AGN3CIPB.js.map +1 -0
- package/dist/chunk-EZSTD2XC.js +380 -0
- package/dist/chunk-EZSTD2XC.js.map +1 -0
- package/dist/chunk-MYSJN3DF.js +117 -0
- package/dist/chunk-MYSJN3DF.js.map +1 -0
- package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
- package/dist/chunk-XO4LMPAH.js.map +1 -0
- package/dist/export-audit-5GV6VRDZ.js +15 -0
- package/dist/export-audit-5GV6VRDZ.js.map +1 -0
- package/dist/index.cjs +2183 -587
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +423 -187
- package/dist/index.d.ts +423 -187
- package/dist/index.js +886 -726
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +789 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +154 -0
- package/dist/internal.d.ts +154 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
- package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
- package/dist/types-BMyHmwvn.d.ts +163 -0
- package/dist/types-CTr00g_d.d.cts +163 -0
- package/dist/types-DepnRDk_.d.cts +212 -0
- package/dist/types-DepnRDk_.d.ts +212 -0
- package/package.json +23 -2
- package/dist/chunk-DXKLAQ6P.js.map +0 -1
- package/dist/chunk-ZP5AXQ3Z.js +0 -179
- package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
- package/dist/types-DF-KDSK1.d.cts +0 -106
- package/dist/types-DF-KDSK1.d.ts +0 -106
- /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.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-DepnRDk_.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-DepnRDk_.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Storage adapter backed by better-sqlite3 for Node.js environments.
|
|
@@ -181,9 +181,299 @@ module.exports = __toCommonJS(indexeddb_exports);
|
|
|
181
181
|
// src/adapters/indexeddb-adapter.ts
|
|
182
182
|
init_errors();
|
|
183
183
|
|
|
184
|
+
// src/adapters/indexeddb-persistence-scheduler.ts
|
|
185
|
+
var DEFAULT_DEBOUNCE_MS = 500;
|
|
186
|
+
var IndexedDbPersistenceScheduler = class {
|
|
187
|
+
debounceMs;
|
|
188
|
+
flush;
|
|
189
|
+
onError;
|
|
190
|
+
timer = null;
|
|
191
|
+
inFlight = null;
|
|
192
|
+
disposed = false;
|
|
193
|
+
onVisibilityChange;
|
|
194
|
+
constructor(options) {
|
|
195
|
+
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
196
|
+
this.flush = options.flush;
|
|
197
|
+
this.onError = options.onError;
|
|
198
|
+
this.onVisibilityChange = () => {
|
|
199
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
200
|
+
void this.flushNow();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
if (typeof document !== "undefined") {
|
|
204
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Schedule a debounced snapshot write. */
|
|
208
|
+
schedule() {
|
|
209
|
+
if (this.disposed) return;
|
|
210
|
+
if (this.debounceMs <= 0) {
|
|
211
|
+
void this.flushNow();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this.timer !== null) {
|
|
215
|
+
clearTimeout(this.timer);
|
|
216
|
+
}
|
|
217
|
+
this.timer = setTimeout(() => {
|
|
218
|
+
this.timer = null;
|
|
219
|
+
void this.flushNow();
|
|
220
|
+
}, this.debounceMs);
|
|
221
|
+
}
|
|
222
|
+
/** Cancel any pending debounce and persist immediately. */
|
|
223
|
+
async flushNow() {
|
|
224
|
+
if (this.disposed) return;
|
|
225
|
+
if (this.timer !== null) {
|
|
226
|
+
clearTimeout(this.timer);
|
|
227
|
+
this.timer = null;
|
|
228
|
+
}
|
|
229
|
+
if (this.inFlight) {
|
|
230
|
+
await this.inFlight;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.inFlight = this.runFlush();
|
|
234
|
+
try {
|
|
235
|
+
await this.inFlight;
|
|
236
|
+
} finally {
|
|
237
|
+
this.inFlight = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
dispose() {
|
|
241
|
+
this.disposed = true;
|
|
242
|
+
if (this.timer !== null) {
|
|
243
|
+
clearTimeout(this.timer);
|
|
244
|
+
this.timer = null;
|
|
245
|
+
}
|
|
246
|
+
if (typeof document !== "undefined") {
|
|
247
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async runFlush() {
|
|
251
|
+
try {
|
|
252
|
+
await this.flush();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.onError?.(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
184
259
|
// src/adapters/sqlite-wasm-adapter.ts
|
|
185
260
|
var import_core2 = require("@korajs/core");
|
|
186
261
|
init_errors();
|
|
262
|
+
|
|
263
|
+
// src/multi-tab/shared-worker-bridge.ts
|
|
264
|
+
init_errors();
|
|
265
|
+
var SW_REQUEST = "kora-sw-request";
|
|
266
|
+
var SW_RESPONSE = "kora-sw-response";
|
|
267
|
+
var SharedWorkerClientBridge = class {
|
|
268
|
+
constructor(sharedWorker, dbName, workerUrl, timeoutMs = 3e4) {
|
|
269
|
+
this.dbName = dbName;
|
|
270
|
+
this.workerUrl = workerUrl;
|
|
271
|
+
this.timeoutMs = timeoutMs;
|
|
272
|
+
this.port = sharedWorker.port;
|
|
273
|
+
this.port.start();
|
|
274
|
+
this.port.addEventListener("message", (event) => {
|
|
275
|
+
const data = event.data;
|
|
276
|
+
if (data?.type !== SW_RESPONSE) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const entry = this.pending.get(data.requestId);
|
|
280
|
+
if (entry) {
|
|
281
|
+
this.pending.delete(data.requestId);
|
|
282
|
+
entry.resolve(data.response);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
dbName;
|
|
287
|
+
workerUrl;
|
|
288
|
+
port;
|
|
289
|
+
pending = /* @__PURE__ */ new Map();
|
|
290
|
+
timeoutMs;
|
|
291
|
+
terminated = false;
|
|
292
|
+
async send(request) {
|
|
293
|
+
if (this.terminated) {
|
|
294
|
+
return {
|
|
295
|
+
id: request.id,
|
|
296
|
+
type: "error",
|
|
297
|
+
message: "SharedWorker bridge terminated",
|
|
298
|
+
code: "BRIDGE_TERMINATED"
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const requestId = crypto.randomUUID();
|
|
302
|
+
const msg = {
|
|
303
|
+
type: SW_REQUEST,
|
|
304
|
+
requestId,
|
|
305
|
+
dbName: this.dbName,
|
|
306
|
+
workerUrl: this.workerUrl,
|
|
307
|
+
request
|
|
308
|
+
};
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const timer = setTimeout(() => {
|
|
311
|
+
this.pending.delete(requestId);
|
|
312
|
+
reject(new WorkerTimeoutError(`shared-worker:${request.type}`, this.timeoutMs));
|
|
313
|
+
}, this.timeoutMs);
|
|
314
|
+
this.pending.set(requestId, {
|
|
315
|
+
resolve: (response) => {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
resolve(response);
|
|
318
|
+
},
|
|
319
|
+
reject: (error) => {
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
reject(error);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
this.port.postMessage(msg);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
terminate() {
|
|
328
|
+
if (this.terminated) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.terminated = true;
|
|
332
|
+
this.port.close();
|
|
333
|
+
for (const [, entry] of this.pending) {
|
|
334
|
+
entry.reject(new Error("SharedWorker bridge terminated"));
|
|
335
|
+
}
|
|
336
|
+
this.pending.clear();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/multi-tab/tab-storage.ts
|
|
341
|
+
init_errors();
|
|
342
|
+
var RPC_REQUEST = "kora-worker-request";
|
|
343
|
+
var RPC_RESPONSE = "kora-worker-response";
|
|
344
|
+
function isSharedWorkerStorageSupported() {
|
|
345
|
+
return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
|
|
346
|
+
}
|
|
347
|
+
function isMultiTabStorageSupported() {
|
|
348
|
+
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
349
|
+
}
|
|
350
|
+
async function acquireTabStorageSession(dbName) {
|
|
351
|
+
const channelName = `kora-storage-${dbName}`;
|
|
352
|
+
if (!isMultiTabStorageSupported()) {
|
|
353
|
+
return { role: "leader", channelName };
|
|
354
|
+
}
|
|
355
|
+
return new Promise((resolve) => {
|
|
356
|
+
let releaseHeld;
|
|
357
|
+
void navigator.locks.request(
|
|
358
|
+
`kora-leader-${dbName}`,
|
|
359
|
+
{ mode: "exclusive", ifAvailable: true },
|
|
360
|
+
(lock) => {
|
|
361
|
+
if (lock === null) {
|
|
362
|
+
resolve({ role: "follower", channelName });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
resolve({
|
|
366
|
+
role: "leader",
|
|
367
|
+
channelName,
|
|
368
|
+
releaseLock: async () => {
|
|
369
|
+
releaseHeld?.();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return new Promise((release) => {
|
|
373
|
+
releaseHeld = release;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function startLeaderRpcRelay(channelName, bridge) {
|
|
380
|
+
const channel = new BroadcastChannel(channelName);
|
|
381
|
+
const onMessage = (event) => {
|
|
382
|
+
const data = event.data;
|
|
383
|
+
if (data?.type !== RPC_REQUEST) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
void bridge.send(data.request).then((response) => {
|
|
387
|
+
const msg = {
|
|
388
|
+
type: RPC_RESPONSE,
|
|
389
|
+
requestId: data.requestId,
|
|
390
|
+
response
|
|
391
|
+
};
|
|
392
|
+
channel.postMessage(msg);
|
|
393
|
+
}).catch((error) => {
|
|
394
|
+
const message = error instanceof Error ? error.message : "Worker RPC failed";
|
|
395
|
+
const msg = {
|
|
396
|
+
type: RPC_RESPONSE,
|
|
397
|
+
requestId: data.requestId,
|
|
398
|
+
response: {
|
|
399
|
+
id: data.request.id,
|
|
400
|
+
type: "error",
|
|
401
|
+
message,
|
|
402
|
+
code: "LEADER_RPC_ERROR"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
channel.postMessage(msg);
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
channel.addEventListener("message", onMessage);
|
|
409
|
+
return () => {
|
|
410
|
+
channel.removeEventListener("message", onMessage);
|
|
411
|
+
channel.close();
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
var FollowerBroadcastBridge = class {
|
|
415
|
+
channel;
|
|
416
|
+
pending = /* @__PURE__ */ new Map();
|
|
417
|
+
timeoutMs;
|
|
418
|
+
terminated = false;
|
|
419
|
+
constructor(channelName, timeoutMs = 3e4) {
|
|
420
|
+
this.timeoutMs = timeoutMs;
|
|
421
|
+
this.channel = new BroadcastChannel(channelName);
|
|
422
|
+
this.channel.addEventListener("message", (event) => {
|
|
423
|
+
const data = event.data;
|
|
424
|
+
if (data?.type !== RPC_RESPONSE) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const entry = this.pending.get(data.requestId);
|
|
428
|
+
if (entry) {
|
|
429
|
+
this.pending.delete(data.requestId);
|
|
430
|
+
entry.resolve(data.response);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async send(request) {
|
|
435
|
+
if (this.terminated) {
|
|
436
|
+
return {
|
|
437
|
+
id: request.id,
|
|
438
|
+
type: "error",
|
|
439
|
+
message: "Follower bridge terminated",
|
|
440
|
+
code: "BRIDGE_TERMINATED"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const requestId = crypto.randomUUID();
|
|
444
|
+
const msg = { type: RPC_REQUEST, requestId, request };
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
const timer = setTimeout(() => {
|
|
447
|
+
this.pending.delete(requestId);
|
|
448
|
+
reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs));
|
|
449
|
+
}, this.timeoutMs);
|
|
450
|
+
this.pending.set(requestId, {
|
|
451
|
+
resolve: (response) => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
resolve(response);
|
|
454
|
+
},
|
|
455
|
+
reject: (error) => {
|
|
456
|
+
clearTimeout(timer);
|
|
457
|
+
reject(error);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
this.channel.postMessage(msg);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
terminate() {
|
|
464
|
+
if (this.terminated) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.terminated = true;
|
|
468
|
+
this.channel.close();
|
|
469
|
+
for (const [, entry] of this.pending) {
|
|
470
|
+
entry.reject(new Error("Follower bridge terminated"));
|
|
471
|
+
}
|
|
472
|
+
this.pending.clear();
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/adapters/sqlite-wasm-adapter.ts
|
|
187
477
|
init_sqlite_wasm_channel();
|
|
188
478
|
var SqliteWasmAdapter = class {
|
|
189
479
|
bridge = null;
|
|
@@ -191,26 +481,52 @@ var SqliteWasmAdapter = class {
|
|
|
191
481
|
mutex = new Mutex();
|
|
192
482
|
injectedBridge;
|
|
193
483
|
workerUrl;
|
|
484
|
+
sharedWorkerUrl;
|
|
485
|
+
workerResponseTimeoutMs;
|
|
194
486
|
dbName;
|
|
487
|
+
tabSession = null;
|
|
488
|
+
sharedWorker = null;
|
|
195
489
|
constructor(options = {}) {
|
|
196
490
|
this.injectedBridge = options.bridge;
|
|
197
491
|
this.workerUrl = options.workerUrl;
|
|
492
|
+
this.sharedWorkerUrl = options.sharedWorkerUrl;
|
|
493
|
+
this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
|
|
198
494
|
this.dbName = options.dbName ?? "kora-db";
|
|
199
495
|
}
|
|
200
496
|
async open(schema) {
|
|
201
497
|
if (this.opened) return;
|
|
202
498
|
if (this.injectedBridge) {
|
|
203
499
|
this.bridge = this.injectedBridge;
|
|
500
|
+
} else if (this.sharedWorkerUrl && this.workerUrl && isSharedWorkerStorageSupported()) {
|
|
501
|
+
const workerUrlString = typeof this.workerUrl === "string" ? this.workerUrl : this.workerUrl.href;
|
|
502
|
+
const sharedUrl = typeof this.sharedWorkerUrl === "string" ? this.sharedWorkerUrl : this.sharedWorkerUrl.href;
|
|
503
|
+
this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
|
|
504
|
+
this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
|
|
204
505
|
} else if (this.workerUrl) {
|
|
506
|
+
this.tabSession = await acquireTabStorageSession(this.dbName);
|
|
205
507
|
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
206
|
-
this.
|
|
508
|
+
if (this.tabSession.role === "leader") {
|
|
509
|
+
const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
|
|
510
|
+
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
511
|
+
this.bridge = workerBridge;
|
|
512
|
+
} else {
|
|
513
|
+
this.bridge = new FollowerBroadcastBridge(
|
|
514
|
+
this.tabSession.channelName,
|
|
515
|
+
this.workerResponseTimeoutMs
|
|
516
|
+
);
|
|
517
|
+
}
|
|
207
518
|
} else {
|
|
208
519
|
throw new AdapterError(
|
|
209
520
|
'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
|
|
210
521
|
);
|
|
211
522
|
}
|
|
212
523
|
const ddlStatements = (0, import_core2.generateFullDDL)(schema);
|
|
213
|
-
const response = await this.sendRequest({
|
|
524
|
+
const response = await this.sendRequest({
|
|
525
|
+
id: 0,
|
|
526
|
+
type: "open",
|
|
527
|
+
ddlStatements,
|
|
528
|
+
dbName: this.dbName
|
|
529
|
+
});
|
|
214
530
|
if (response.type === "error") {
|
|
215
531
|
throw new AdapterError(`Failed to open database: ${response.message}`, {
|
|
216
532
|
code: response.code,
|
|
@@ -224,6 +540,12 @@ var SqliteWasmAdapter = class {
|
|
|
224
540
|
try {
|
|
225
541
|
await this.sendRequest({ id: 0, type: "close" });
|
|
226
542
|
} finally {
|
|
543
|
+
this.tabSession?.stopRelay?.();
|
|
544
|
+
if (this.tabSession?.releaseLock) {
|
|
545
|
+
await this.tabSession.releaseLock();
|
|
546
|
+
}
|
|
547
|
+
this.tabSession = null;
|
|
548
|
+
this.sharedWorker = null;
|
|
227
549
|
this.bridge.terminate();
|
|
228
550
|
this.bridge = null;
|
|
229
551
|
this.opened = false;
|
|
@@ -356,6 +678,32 @@ var IDB_DATABASE_NAME = "kora-persistence";
|
|
|
356
678
|
var IDB_STORE_NAME = "databases";
|
|
357
679
|
var IDB_VERSION = 1;
|
|
358
680
|
var DUMP_SUFFIX = "::dump";
|
|
681
|
+
function isIndexedDbQuotaError(error) {
|
|
682
|
+
if (error instanceof DOMException && error.name === "QuotaExceededError") {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
if (error instanceof PersistenceError && error.context?.quotaExceeded === true) {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
if (error instanceof Error && /quota/i.test(error.message)) {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
function rejectWithIdbError(reject, message, context, event) {
|
|
694
|
+
const domError = event.target?.error;
|
|
695
|
+
if (domError && isIndexedDbQuotaError(domError)) {
|
|
696
|
+
reject(
|
|
697
|
+
new PersistenceError(message, {
|
|
698
|
+
...context,
|
|
699
|
+
quotaExceeded: true,
|
|
700
|
+
cause: domError.message
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
reject(new PersistenceError(message, context));
|
|
706
|
+
}
|
|
359
707
|
function openIdb() {
|
|
360
708
|
return new Promise((resolve, reject) => {
|
|
361
709
|
const request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
|
|
@@ -382,7 +730,12 @@ async function saveToIndexedDB(dbName, data) {
|
|
|
382
730
|
const store = tx.objectStore(IDB_STORE_NAME);
|
|
383
731
|
store.put(data, dbName);
|
|
384
732
|
tx.oncomplete = () => resolve();
|
|
385
|
-
tx.onerror = () =>
|
|
733
|
+
tx.onerror = (event) => rejectWithIdbError(
|
|
734
|
+
reject,
|
|
735
|
+
`Failed to save database "${dbName}" to IndexedDB`,
|
|
736
|
+
{ dbName },
|
|
737
|
+
event
|
|
738
|
+
);
|
|
386
739
|
});
|
|
387
740
|
} finally {
|
|
388
741
|
idb.close();
|
|
@@ -396,7 +749,12 @@ async function saveDumpToIndexedDB(dbName, dump) {
|
|
|
396
749
|
const store = tx.objectStore(IDB_STORE_NAME);
|
|
397
750
|
store.put(dump, `${dbName}${DUMP_SUFFIX}`);
|
|
398
751
|
tx.oncomplete = () => resolve();
|
|
399
|
-
tx.onerror = () =>
|
|
752
|
+
tx.onerror = (event) => rejectWithIdbError(
|
|
753
|
+
reject,
|
|
754
|
+
`Failed to save dump for database "${dbName}"`,
|
|
755
|
+
{ dbName },
|
|
756
|
+
event
|
|
757
|
+
);
|
|
400
758
|
});
|
|
401
759
|
} finally {
|
|
402
760
|
idb.close();
|
|
@@ -452,12 +810,21 @@ async function loadDumpFromIndexedDB(dbName) {
|
|
|
452
810
|
var IndexedDbAdapter = class {
|
|
453
811
|
inner;
|
|
454
812
|
dbName;
|
|
813
|
+
emitter;
|
|
814
|
+
scheduler;
|
|
455
815
|
constructor(options = {}) {
|
|
456
816
|
this.dbName = options.dbName ?? "kora-db";
|
|
817
|
+
this.emitter = options.emitter;
|
|
457
818
|
this.inner = new SqliteWasmAdapter({
|
|
458
819
|
bridge: options.bridge,
|
|
459
820
|
workerUrl: options.workerUrl,
|
|
460
|
-
dbName: this.dbName
|
|
821
|
+
dbName: this.dbName,
|
|
822
|
+
workerResponseTimeoutMs: options.workerResponseTimeoutMs
|
|
823
|
+
});
|
|
824
|
+
this.scheduler = new IndexedDbPersistenceScheduler({
|
|
825
|
+
debounceMs: options.persistenceDebounceMs,
|
|
826
|
+
flush: () => this.writeSnapshot(),
|
|
827
|
+
onError: (error) => this.handlePersistenceError(error)
|
|
461
828
|
});
|
|
462
829
|
}
|
|
463
830
|
async open(schema) {
|
|
@@ -471,32 +838,55 @@ var IndexedDbAdapter = class {
|
|
|
471
838
|
}
|
|
472
839
|
}
|
|
473
840
|
async close() {
|
|
474
|
-
await this.
|
|
841
|
+
await this.scheduler.flushNow();
|
|
842
|
+
this.scheduler.dispose();
|
|
475
843
|
await this.inner.close();
|
|
476
844
|
}
|
|
477
845
|
async execute(sql, params) {
|
|
478
846
|
await this.inner.execute(sql, params);
|
|
479
|
-
|
|
847
|
+
this.scheduler.schedule();
|
|
480
848
|
}
|
|
481
849
|
async query(sql, params) {
|
|
482
850
|
return this.inner.query(sql, params);
|
|
483
851
|
}
|
|
484
852
|
async transaction(fn) {
|
|
485
853
|
await this.inner.transaction(fn);
|
|
486
|
-
|
|
854
|
+
this.scheduler.schedule();
|
|
487
855
|
}
|
|
488
856
|
async migrate(from, to, migration) {
|
|
489
857
|
await this.inner.migrate(from, to, migration);
|
|
490
|
-
|
|
858
|
+
this.scheduler.schedule();
|
|
491
859
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Force an immediate snapshot write to IndexedDB (skips debounce).
|
|
862
|
+
* Useful before tab unload or in tests.
|
|
863
|
+
*/
|
|
864
|
+
async flushPersistence() {
|
|
865
|
+
await this.scheduler.flushNow();
|
|
866
|
+
}
|
|
867
|
+
async writeSnapshot() {
|
|
868
|
+
const data = await this.inner.exportDatabase();
|
|
869
|
+
await saveToIndexedDB(this.dbName, data);
|
|
870
|
+
const dump = await this.exportDump();
|
|
871
|
+
await saveDumpToIndexedDB(this.dbName, dump);
|
|
872
|
+
}
|
|
873
|
+
handlePersistenceError(error) {
|
|
874
|
+
const message = error instanceof Error ? error.message : "IndexedDB persistence failed";
|
|
875
|
+
const code = error instanceof PersistenceError ? error.code : "PERSISTENCE_FAILED";
|
|
876
|
+
const quotaExceeded = isIndexedDbQuotaError(error);
|
|
877
|
+
if (quotaExceeded) {
|
|
878
|
+
this.emitter?.emit({
|
|
879
|
+
type: "store:quota-exceeded",
|
|
880
|
+
dbName: this.dbName,
|
|
881
|
+
message
|
|
882
|
+
});
|
|
499
883
|
}
|
|
884
|
+
this.emitter?.emit({
|
|
885
|
+
type: "store:persistence-error",
|
|
886
|
+
dbName: this.dbName,
|
|
887
|
+
message,
|
|
888
|
+
code: quotaExceeded ? "QUOTA_EXCEEDED" : code
|
|
889
|
+
});
|
|
500
890
|
}
|
|
501
891
|
async restoreFromDumpFallback() {
|
|
502
892
|
const dump = await loadDumpFromIndexedDB(this.dbName);
|
|
@@ -541,7 +931,10 @@ var IndexedDbAdapter = class {
|
|
|
541
931
|
};
|
|
542
932
|
function ensureSafeIdentifier(identifier) {
|
|
543
933
|
if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
|
|
544
|
-
throw new
|
|
934
|
+
throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
|
|
935
|
+
code: "UNSAFE_IDENTIFIER",
|
|
936
|
+
identifier
|
|
937
|
+
});
|
|
545
938
|
}
|
|
546
939
|
return identifier;
|
|
547
940
|
}
|