@workglow/indexeddb 0.2.31 → 0.2.32

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 (31) hide show
  1. package/README.md +34 -0
  2. package/dist/job-queue/IndexedDbQueueStorage.d.ts +16 -11
  3. package/dist/job-queue/IndexedDbQueueStorage.d.ts.map +1 -1
  4. package/dist/job-queue/IndexedDbRateLimiterStorage.d.ts +15 -4
  5. package/dist/job-queue/IndexedDbRateLimiterStorage.d.ts.map +1 -1
  6. package/dist/job-queue/browser.js +399 -351
  7. package/dist/job-queue/browser.js.map +9 -6
  8. package/dist/job-queue/common.d.ts +3 -0
  9. package/dist/job-queue/common.d.ts.map +1 -1
  10. package/dist/job-queue/node.js +399 -351
  11. package/dist/job-queue/node.js.map +9 -6
  12. package/dist/migrations/IndexedDbMigrationRunner.d.ts +93 -0
  13. package/dist/migrations/IndexedDbMigrationRunner.d.ts.map +1 -0
  14. package/dist/migrations/indexedDbQueueMigrations.d.ts +24 -0
  15. package/dist/migrations/indexedDbQueueMigrations.d.ts.map +1 -0
  16. package/dist/migrations/indexedDbRateLimiterMigrations.d.ts +37 -0
  17. package/dist/migrations/indexedDbRateLimiterMigrations.d.ts.map +1 -0
  18. package/dist/storage/IndexedDbTable.d.ts.map +1 -1
  19. package/dist/storage/IndexedDbTabularMigrationApplier.d.ts +84 -0
  20. package/dist/storage/IndexedDbTabularMigrationApplier.d.ts.map +1 -0
  21. package/dist/storage/IndexedDbTabularStorage.d.ts +21 -2
  22. package/dist/storage/IndexedDbTabularStorage.d.ts.map +1 -1
  23. package/dist/storage/browser.js +472 -30
  24. package/dist/storage/browser.js.map +8 -5
  25. package/dist/storage/common.d.ts +3 -0
  26. package/dist/storage/common.d.ts.map +1 -1
  27. package/dist/storage/node.js +472 -30
  28. package/dist/storage/node.js.map +8 -5
  29. package/dist/storage/openIdb.d.ts +19 -0
  30. package/dist/storage/openIdb.d.ts.map +1 -0
  31. package/package.json +7 -7
@@ -1,3 +1,24 @@
1
+ // src/storage/openIdb.ts
2
+ function openIdb(dbName, options = {}) {
3
+ return new Promise((resolve, reject) => {
4
+ const req = indexedDB.open(dbName, options.version);
5
+ req.onsuccess = () => {
6
+ const db = req.result;
7
+ db.onversionchange = () => db.close();
8
+ resolve(db);
9
+ };
10
+ req.onupgradeneeded = (ev) => options.onUpgradeNeeded?.(ev);
11
+ req.onerror = () => {
12
+ const err = req.error;
13
+ if (err && err.name === "VersionError") {
14
+ reject(new Error(`IndexedDB ${dbName} exists at a higher version than ${options.version ?? "current"}`));
15
+ return;
16
+ }
17
+ reject(err);
18
+ };
19
+ req.onblocked = () => reject(new Error(`IndexedDB ${dbName} is blocked — close other tabs using this database.`));
20
+ });
21
+ }
1
22
  // src/storage/IndexedDbTable.ts
2
23
  import { deepEqual } from "@workglow/util";
3
24
  var METADATA_STORE_NAME = "__schema_metadata__";
@@ -15,33 +36,8 @@ async function saveSchemaMetadata(db, tableName, snapshot) {
15
36
  }
16
37
  });
17
38
  }
18
- async function openIndexedDbTable(tableName, version, upgradeNeededCallback) {
19
- return new Promise((resolve, reject) => {
20
- const openRequest = indexedDB.open(tableName, version);
21
- openRequest.onsuccess = (event) => {
22
- const db = event.target.result;
23
- db.onversionchange = () => {
24
- db.close();
25
- };
26
- resolve(db);
27
- };
28
- openRequest.onupgradeneeded = (event) => {
29
- if (upgradeNeededCallback) {
30
- upgradeNeededCallback(event);
31
- }
32
- };
33
- openRequest.onerror = () => {
34
- const error = openRequest.error;
35
- if (error && error.name === "VersionError") {
36
- reject(new Error(`Database ${tableName} exists at a higher version. Cannot open at version ${version || "current"}.`));
37
- } else {
38
- reject(error);
39
- }
40
- };
41
- openRequest.onblocked = () => {
42
- reject(new Error(`Database ${tableName} is blocked. Close all other tabs using this database.`));
43
- };
44
- });
39
+ function openIndexedDbTable(tableName, version, upgradeNeededCallback) {
40
+ return openIdb(tableName, { version, onUpgradeNeeded: upgradeNeededCallback });
45
41
  }
46
42
  async function deleteIndexedDbTable(tableName) {
47
43
  return new Promise((resolve, reject) => {
@@ -325,6 +321,404 @@ import {
325
321
  isSearchCondition,
326
322
  pickCoveringIndex
327
323
  } from "@workglow/storage";
324
+
325
+ // src/migrations/IndexedDbMigrationRunner.ts
326
+ import {
327
+ MIGRATIONS_TABLE,
328
+ sortMigrations
329
+ } from "@workglow/storage";
330
+ function getIndexedDb() {
331
+ const idb = globalThis.indexedDB;
332
+ if (!idb) {
333
+ throw new Error("indexedDB is not available in this environment. Provide one via the IndexedDbMigrationRunner constructor or polyfill globalThis.indexedDB.");
334
+ }
335
+ return idb;
336
+ }
337
+
338
+ class MigrationAbortedByOtherTabError extends Error {
339
+ constructor(dbName) {
340
+ super(`IndexedDB ${dbName} migration aborted: another tab requested a higher version`);
341
+ this.name = "MigrationAbortedByOtherTabError";
342
+ }
343
+ }
344
+ var RUN_LOCKS = new Map;
345
+
346
+ class IndexedDbMigrationRunner {
347
+ dbName;
348
+ idb;
349
+ constructor(dbName, idb = getIndexedDb()) {
350
+ this.dbName = dbName;
351
+ this.idb = idb;
352
+ }
353
+ async probe() {
354
+ return new Promise((resolve, reject) => {
355
+ let settled = false;
356
+ const finalize = (db, outcome) => {
357
+ if (settled)
358
+ return;
359
+ settled = true;
360
+ if (db) {
361
+ try {
362
+ db.close();
363
+ } catch {}
364
+ }
365
+ if (outcome.ok)
366
+ resolve(outcome.value);
367
+ else
368
+ reject(outcome.error);
369
+ };
370
+ const req = this.idb.open(this.dbName);
371
+ req.onupgradeneeded = () => {
372
+ if (settled)
373
+ return;
374
+ const u = req.result;
375
+ if (!u.objectStoreNames.contains(MIGRATIONS_TABLE)) {
376
+ u.createObjectStore(MIGRATIONS_TABLE, { keyPath: ["component", "version"] });
377
+ }
378
+ };
379
+ req.onsuccess = () => {
380
+ if (settled)
381
+ return;
382
+ const db = req.result;
383
+ const currentVersion = db.version;
384
+ if (!db.objectStoreNames.contains(MIGRATIONS_TABLE)) {
385
+ finalize(db, { ok: true, value: { currentVersion, applied: new Set } });
386
+ return;
387
+ }
388
+ const tx = db.transaction(MIGRATIONS_TABLE, "readonly");
389
+ const store = tx.objectStore(MIGRATIONS_TABLE);
390
+ const getAll = store.getAll();
391
+ getAll.onsuccess = () => {
392
+ if (settled)
393
+ return;
394
+ const rows = getAll.result;
395
+ finalize(db, {
396
+ ok: true,
397
+ value: {
398
+ currentVersion,
399
+ applied: new Set(rows.map((r) => `${r.component}@${r.version}`))
400
+ }
401
+ });
402
+ };
403
+ getAll.onerror = () => {
404
+ if (settled)
405
+ return;
406
+ finalize(db, { ok: false, error: getAll.error });
407
+ };
408
+ };
409
+ req.onerror = () => {
410
+ if (settled)
411
+ return;
412
+ finalize(undefined, { ok: false, error: req.error });
413
+ };
414
+ req.onblocked = () => {
415
+ if (settled)
416
+ return;
417
+ finalize(undefined, {
418
+ ok: false,
419
+ error: new Error(`IndexedDB ${this.dbName} blocked while probing for bookkeeping store`)
420
+ });
421
+ };
422
+ });
423
+ }
424
+ async ensureBookkeepingTable() {
425
+ await this.probe();
426
+ }
427
+ async appliedVersions(component) {
428
+ const { applied } = await this.probe();
429
+ const versions = new Set;
430
+ for (const key of applied) {
431
+ const at = key.lastIndexOf("@");
432
+ if (at < 0)
433
+ continue;
434
+ const c = key.slice(0, at);
435
+ const v = Number(key.slice(at + 1));
436
+ if (c === component && Number.isFinite(v))
437
+ versions.add(v);
438
+ }
439
+ return versions;
440
+ }
441
+ async run(migrations, options = {}) {
442
+ const prev = RUN_LOCKS.get(this.dbName) ?? Promise.resolve();
443
+ const next = prev.catch(() => {
444
+ return;
445
+ }).then(() => this.runLocked(migrations, options));
446
+ RUN_LOCKS.set(this.dbName, next);
447
+ try {
448
+ return await next;
449
+ } finally {
450
+ if (RUN_LOCKS.get(this.dbName) === next)
451
+ RUN_LOCKS.delete(this.dbName);
452
+ }
453
+ }
454
+ async runLocked(migrations, options) {
455
+ const sorted = sortMigrations(migrations);
456
+ if (sorted.length === 0)
457
+ return [];
458
+ const { currentVersion, applied: alreadyApplied } = await this.probe();
459
+ const pending = sorted.filter((m) => !alreadyApplied.has(`${m.component}@${m.version}`));
460
+ if (pending.length === 0)
461
+ return [];
462
+ const targetVersion = currentVersion + 1;
463
+ const applied = [];
464
+ const onProgress = options.onProgress;
465
+ const buffered = [];
466
+ const emitLater = onProgress ? (ev) => buffered.push(ev) : undefined;
467
+ await new Promise((resolve, reject) => {
468
+ let settled = false;
469
+ let upgradeDb;
470
+ const finalize = (outcome) => {
471
+ if (settled)
472
+ return;
473
+ settled = true;
474
+ if (upgradeDb) {
475
+ try {
476
+ upgradeDb.close();
477
+ } catch {}
478
+ }
479
+ if (outcome.ok)
480
+ resolve();
481
+ else
482
+ reject(outcome.error);
483
+ };
484
+ const upreq = this.idb.open(this.dbName, targetVersion);
485
+ upreq.onupgradeneeded = (ev) => {
486
+ if (settled)
487
+ return;
488
+ try {
489
+ const db = upreq.result;
490
+ upgradeDb = db;
491
+ const tx = upreq.transaction;
492
+ const oldVersion = ev.oldVersion;
493
+ const newVersion = ev.newVersion ?? targetVersion;
494
+ db.onversionchange = () => {
495
+ try {
496
+ tx.abort();
497
+ } catch {}
498
+ finalize({ ok: false, error: new MigrationAbortedByOtherTabError(this.dbName) });
499
+ };
500
+ if (!db.objectStoreNames.contains(MIGRATIONS_TABLE)) {
501
+ db.createObjectStore(MIGRATIONS_TABLE, { keyPath: ["component", "version"] });
502
+ }
503
+ const meta = tx.objectStore(MIGRATIONS_TABLE);
504
+ for (const m of pending) {
505
+ emitLater?.({
506
+ component: m.component,
507
+ version: m.version,
508
+ phase: "starting",
509
+ description: m.description
510
+ });
511
+ const ctx = { db, tx, oldVersion, newVersion };
512
+ const result = m.up(ctx, (fraction) => {
513
+ emitLater?.({
514
+ component: m.component,
515
+ version: m.version,
516
+ phase: "running",
517
+ description: m.description,
518
+ fraction
519
+ });
520
+ });
521
+ if (result instanceof Promise) {
522
+ throw new Error(`IndexedDB migration "${m.component}@${m.version}" returned a Promise; ` + `IDB upgrade transactions cannot span async work.`);
523
+ }
524
+ meta.add({
525
+ component: m.component,
526
+ version: m.version,
527
+ description: m.description ?? null,
528
+ applied_at: new Date().toISOString()
529
+ });
530
+ applied.push(m);
531
+ emitLater?.({
532
+ component: m.component,
533
+ version: m.version,
534
+ phase: "completed",
535
+ description: m.description,
536
+ fraction: 1
537
+ });
538
+ }
539
+ } catch (err) {
540
+ try {
541
+ upreq.transaction?.abort();
542
+ } catch {}
543
+ const lastStart = [...buffered].reverse().find((e) => e.phase === "starting");
544
+ if (lastStart) {
545
+ emitLater?.({
546
+ component: lastStart.component,
547
+ version: lastStart.version,
548
+ phase: "failed",
549
+ description: lastStart.description,
550
+ error: err
551
+ });
552
+ }
553
+ finalize({ ok: false, error: err });
554
+ }
555
+ };
556
+ upreq.onsuccess = () => {
557
+ if (settled)
558
+ return;
559
+ upgradeDb = upreq.result;
560
+ finalize({ ok: true });
561
+ };
562
+ upreq.onerror = () => {
563
+ if (settled)
564
+ return;
565
+ finalize({ ok: false, error: upreq.error });
566
+ };
567
+ upreq.onblocked = () => {
568
+ if (settled)
569
+ return;
570
+ finalize({
571
+ ok: false,
572
+ error: new Error(`IndexedDB ${this.dbName} upgrade blocked — close other tabs.`)
573
+ });
574
+ };
575
+ }).finally(() => {
576
+ if (onProgress) {
577
+ for (const ev of buffered)
578
+ onProgress(ev);
579
+ }
580
+ });
581
+ return applied;
582
+ }
583
+ }
584
+ async function runIndexedDbMigrationGroups(groups, options = {}) {
585
+ const { idb, onProgress } = options;
586
+ const all = [];
587
+ for (const group of groups) {
588
+ const runner = idb ? new IndexedDbMigrationRunner(group.dbName, idb) : new IndexedDbMigrationRunner(group.dbName);
589
+ const applied = await runner.run(group.migrations, { onProgress });
590
+ all.push(...applied);
591
+ }
592
+ return all;
593
+ }
594
+
595
+ // src/storage/IndexedDbTabularMigrationApplier.ts
596
+ async function idbObjectStoreExists(dbName, storeName) {
597
+ const idb = globalThis.indexedDB;
598
+ if (!idb)
599
+ throw new Error("indexedDB is not available in this environment");
600
+ return new Promise((resolve, reject) => {
601
+ const req = idb.open(dbName);
602
+ req.onsuccess = () => {
603
+ const db = req.result;
604
+ const exists = db.objectStoreNames.contains(storeName);
605
+ db.close();
606
+ resolve(exists);
607
+ };
608
+ req.onerror = () => reject(req.error);
609
+ req.onblocked = () => reject(new Error(`IndexedDB ${dbName} blocked while probing for object store`));
610
+ });
611
+ }
612
+
613
+ class IndexedDbTabularMigrationApplier {
614
+ dbName;
615
+ storeName;
616
+ storage;
617
+ runner;
618
+ constructor(dbName, storeName, storage, runner) {
619
+ this.dbName = dbName;
620
+ this.storeName = storeName;
621
+ this.storage = storage;
622
+ this.runner = runner ?? new IndexedDbMigrationRunner(dbName);
623
+ }
624
+ async ensureBookkeeping() {
625
+ await this.runner.ensureBookkeepingTable();
626
+ }
627
+ async appliedVersions(component) {
628
+ return this.runner.appliedVersions(component);
629
+ }
630
+ async tableExists() {
631
+ return idbObjectStoreExists(this.dbName, this.storeName);
632
+ }
633
+ async markAllApplied(component, versions) {
634
+ if (versions.length === 0)
635
+ return;
636
+ await this.runner.run(versions.map((v) => ({
637
+ component,
638
+ version: v.version,
639
+ description: v.description,
640
+ up: () => {
641
+ return;
642
+ }
643
+ })));
644
+ }
645
+ async applyMigration(component, version, description, ops, onProgress) {
646
+ const ddlOps = [];
647
+ const backfills = [];
648
+ for (const op of ops) {
649
+ switch (op.kind) {
650
+ case "addIndex":
651
+ case "dropIndex":
652
+ ddlOps.push(op);
653
+ break;
654
+ case "backfill":
655
+ backfills.push(op);
656
+ break;
657
+ case "addColumn":
658
+ case "dropColumn":
659
+ case "renameColumn":
660
+ break;
661
+ default: {
662
+ const _exhaustive = op;
663
+ throw new Error(`IndexedDbTabularMigrationApplier: unhandled op kind ${_exhaustive.kind}`);
664
+ }
665
+ }
666
+ }
667
+ let processed = 0;
668
+ const total = Math.max(ops.length, 1);
669
+ for (const op of backfills) {
670
+ const batchSize = op.batchSize ?? 500;
671
+ let cursor;
672
+ while (true) {
673
+ const page = await this.storage.getPage({ limit: batchSize, cursor });
674
+ for (const row of page.items) {
675
+ const out = await op.transform(row);
676
+ if (out === row)
677
+ continue;
678
+ if (out === undefined) {
679
+ await this.storage.delete(row);
680
+ } else {
681
+ await this.storage.put(out);
682
+ }
683
+ }
684
+ if (!page.nextCursor)
685
+ break;
686
+ cursor = page.nextCursor;
687
+ }
688
+ processed++;
689
+ onProgress?.(processed / total);
690
+ }
691
+ const storeName = this.storeName;
692
+ await this.runner.run([
693
+ {
694
+ component,
695
+ version,
696
+ description,
697
+ up: (ctx) => {
698
+ if (!ctx.db.objectStoreNames.contains(storeName))
699
+ return;
700
+ const store = ctx.tx.objectStore(storeName);
701
+ for (const op of ddlOps) {
702
+ if (op.kind === "addIndex") {
703
+ if (store.indexNames.contains(op.name))
704
+ continue;
705
+ const keyPath = op.columns.length === 1 ? op.columns[0] : [...op.columns];
706
+ store.createIndex(op.name, keyPath, { unique: op.unique ?? false });
707
+ } else {
708
+ if (!store.indexNames.contains(op.name))
709
+ continue;
710
+ store.deleteIndex(op.name);
711
+ }
712
+ }
713
+ }
714
+ }
715
+ ]);
716
+ processed += ddlOps.length;
717
+ onProgress?.(processed / total);
718
+ }
719
+ }
720
+
721
+ // src/storage/IndexedDbTabularStorage.ts
328
722
  var IDB_TABULAR_REPOSITORY = createServiceToken("storage.tabularRepository.indexedDb");
329
723
  function compareEntitiesForChange(a, b) {
330
724
  const au = a?.updated_at;
@@ -338,13 +732,14 @@ function compareEntitiesForChange(a, b) {
338
732
  class IndexedDbTabularStorage extends BaseTabularStorage {
339
733
  table;
340
734
  db;
735
+ applyingMigrations = false;
341
736
  setupPromise = null;
342
737
  migrationOptions;
343
738
  hybridManager = null;
344
739
  hybridOptions;
345
740
  cursorSafeIndexes;
346
- constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], migrationOptions = {}, clientProvidedKeys = "if-missing") {
347
- super(schema, primaryKeyNames, indexes, clientProvidedKeys);
741
+ constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], migrationOptions = {}, clientProvidedKeys = "if-missing", tabularMigrations) {
742
+ super(schema, primaryKeyNames, indexes, clientProvidedKeys, tabularMigrations, table);
348
743
  this.table = table;
349
744
  this.migrationOptions = migrationOptions;
350
745
  this.hybridOptions = {
@@ -365,12 +760,46 @@ class IndexedDbTabularStorage extends BaseTabularStorage {
365
760
  await this.setupPromise;
366
761
  return;
367
762
  }
763
+ if (this.applyingMigrations) {
764
+ this.setupPromise = this.performSetup();
765
+ try {
766
+ this.db = await this.setupPromise;
767
+ this.rewireOnVersionChange();
768
+ } finally {
769
+ this.setupPromise = null;
770
+ }
771
+ return;
772
+ }
773
+ const freshTable = this.tabularMigrations && this.tabularMigrations.length > 0 ? !await this.probeObjectStoreExists() : false;
368
774
  this.setupPromise = this.performSetup();
369
775
  try {
370
776
  this.db = await this.setupPromise;
371
777
  } finally {
372
778
  this.setupPromise = null;
373
779
  }
780
+ if (this.tabularMigrations && this.tabularMigrations.length > 0) {
781
+ this.rewireOnVersionChange();
782
+ this.applyingMigrations = true;
783
+ try {
784
+ await this.applyTabularMigrations({ freshTable });
785
+ } finally {
786
+ this.applyingMigrations = false;
787
+ }
788
+ if (!this.db) {
789
+ this.db = await this.performSetup();
790
+ }
791
+ }
792
+ }
793
+ rewireOnVersionChange() {
794
+ if (!this.db)
795
+ return;
796
+ this.db.onversionchange = () => {
797
+ this.db?.close();
798
+ this.db = undefined;
799
+ };
800
+ }
801
+ async probeObjectStoreExists() {
802
+ return idbObjectStoreExists(this.table, this.table);
374
803
  }
375
804
  async performSetup() {
376
805
  const pkColumns = super.primaryKeyColumns();
@@ -394,6 +823,13 @@ class IndexedDbTabularStorage extends BaseTabularStorage {
394
823
  const useAutoIncrement = this.hasAutoGeneratedKey() && this.autoGeneratedKeyStrategy === "autoincrement" && pkColumns.length === 1;
395
824
  return await ensureIndexedDbTable(this.table, primaryKey, expectedIndexes, this.migrationOptions, useAutoIncrement);
396
825
  }
826
+ getMigrationApplier() {
827
+ return new IndexedDbTabularMigrationApplier(this.table, this.table, {
828
+ getPage: (req) => this.getPage(req),
829
+ put: (row) => this.put(row),
830
+ delete: (row) => this.delete(row)
831
+ });
832
+ }
397
833
  generateKeyValue(columnName, strategy) {
398
834
  if (strategy === "uuid") {
399
835
  return uuid4();
@@ -1169,14 +1605,20 @@ class IndexedDbVectorStorage extends IndexedDbTabularStorage {
1169
1605
  }
1170
1606
  }
1171
1607
  export {
1608
+ runIndexedDbMigrationGroups,
1609
+ openIdb,
1610
+ idbObjectStoreExists,
1172
1611
  ensureIndexedDbTable,
1173
1612
  dropIndexedDbTable,
1613
+ MigrationAbortedByOtherTabError,
1174
1614
  IndexedDbVectorStorage,
1175
1615
  IndexedDbTabularStorage,
1616
+ IndexedDbTabularMigrationApplier,
1617
+ IndexedDbMigrationRunner,
1176
1618
  IndexedDbKvStorage,
1177
1619
  IDB_VECTOR_REPOSITORY,
1178
1620
  IDB_TABULAR_REPOSITORY,
1179
1621
  IDB_KV_REPOSITORY
1180
1622
  };
1181
1623
 
1182
- //# debugId=FCA00B1D5717F8F364756E2164756E21
1624
+ //# debugId=CCF175469329EA4F64756E2164756E21