@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.
Files changed (69) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +410 -17
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +23 -5
  6. package/dist/adapters/indexeddb.d.ts +23 -5
  7. package/dist/adapters/indexeddb.js +163 -18
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
  10. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
  11. package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
  12. package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
  13. package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
  14. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
  15. package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
  16. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm-worker.js +34 -5
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  19. package/dist/adapters/sqlite-wasm.cjs +249 -2
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm.d.cts +15 -3
  22. package/dist/adapters/sqlite-wasm.d.ts +15 -3
  23. package/dist/adapters/sqlite-wasm.js +2 -2
  24. package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
  25. package/dist/backup-JN3LB4YT.js +14 -0
  26. package/dist/backup-JN3LB4YT.js.map +1 -0
  27. package/dist/chunk-56LL4XP6.js +205 -0
  28. package/dist/chunk-56LL4XP6.js.map +1 -0
  29. package/dist/chunk-6AHYGQQT.js +425 -0
  30. package/dist/chunk-6AHYGQQT.js.map +1 -0
  31. package/dist/chunk-7BPETY5Y.js +570 -0
  32. package/dist/chunk-7BPETY5Y.js.map +1 -0
  33. package/dist/chunk-AGN3CIPB.js +188 -0
  34. package/dist/chunk-AGN3CIPB.js.map +1 -0
  35. package/dist/chunk-EZSTD2XC.js +380 -0
  36. package/dist/chunk-EZSTD2XC.js.map +1 -0
  37. package/dist/chunk-MYSJN3DF.js +117 -0
  38. package/dist/chunk-MYSJN3DF.js.map +1 -0
  39. package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
  40. package/dist/chunk-XO4LMPAH.js.map +1 -0
  41. package/dist/export-audit-5GV6VRDZ.js +15 -0
  42. package/dist/export-audit-5GV6VRDZ.js.map +1 -0
  43. package/dist/index.cjs +2183 -587
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +423 -187
  46. package/dist/index.d.ts +423 -187
  47. package/dist/index.js +886 -726
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.cjs +789 -0
  50. package/dist/internal.cjs.map +1 -0
  51. package/dist/internal.d.cts +154 -0
  52. package/dist/internal.d.ts +154 -0
  53. package/dist/internal.js +48 -0
  54. package/dist/internal.js.map +1 -0
  55. package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
  56. package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
  57. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
  58. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
  59. package/dist/types-BMyHmwvn.d.ts +163 -0
  60. package/dist/types-CTr00g_d.d.cts +163 -0
  61. package/dist/types-DepnRDk_.d.cts +212 -0
  62. package/dist/types-DepnRDk_.d.ts +212 -0
  63. package/package.json +23 -2
  64. package/dist/chunk-DXKLAQ6P.js.map +0 -1
  65. package/dist/chunk-ZP5AXQ3Z.js +0 -179
  66. package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
  67. package/dist/types-DF-KDSK1.d.cts +0 -106
  68. package/dist/types-DF-KDSK1.d.ts +0 -106
  69. /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-DF-KDSK1.cjs';
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-DF-KDSK1.js';
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.bridge = new WebWorkerBridge2(this.workerUrl);
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({ id: 0, type: "open", ddlStatements });
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 = () => reject(new PersistenceError(`Failed to save database "${dbName}" to IndexedDB`, { dbName }));
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 = () => reject(new PersistenceError(`Failed to save dump for database "${dbName}"`, { dbName }));
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.persistSnapshot();
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
- await this.persistSnapshot();
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
- await this.persistSnapshot();
854
+ this.scheduler.schedule();
487
855
  }
488
856
  async migrate(from, to, migration) {
489
857
  await this.inner.migrate(from, to, migration);
490
- await this.persistSnapshot();
858
+ this.scheduler.schedule();
491
859
  }
492
- async persistSnapshot() {
493
- try {
494
- const data = await this.inner.exportDatabase();
495
- await saveToIndexedDB(this.dbName, data);
496
- const dump = await this.exportDump();
497
- await saveDumpToIndexedDB(this.dbName, dump);
498
- } catch {
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 AdapterError(`Unsafe SQL identifier: ${identifier}`);
934
+ throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
935
+ code: "UNSAFE_IDENTIFIER",
936
+ identifier
937
+ });
545
938
  }
546
939
  return identifier;
547
940
  }