@unicitylabs/sphere-sdk 0.4.2 → 0.4.4

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.
@@ -354,6 +354,7 @@ function createLocalStorageProvider(config) {
354
354
  var DB_NAME = "sphere-storage";
355
355
  var DB_VERSION = 1;
356
356
  var STORE_NAME = "kv";
357
+ var connectionSeq = 0;
357
358
  var IndexedDBStorageProvider = class {
358
359
  id = "indexeddb-storage";
359
360
  name = "IndexedDB Storage";
@@ -365,6 +366,8 @@ var IndexedDBStorageProvider = class {
365
366
  identity = null;
366
367
  status = "disconnected";
367
368
  db = null;
369
+ /** Monotonic connection ID for tracing open/close pairs */
370
+ connId = 0;
368
371
  constructor(config) {
369
372
  this.prefix = config?.prefix ?? "sphere_";
370
373
  this.dbName = config?.dbName ?? DB_NAME;
@@ -375,24 +378,35 @@ var IndexedDBStorageProvider = class {
375
378
  // ===========================================================================
376
379
  async connect() {
377
380
  if (this.status === "connected" && this.db) return;
378
- this.status = "connecting";
379
- console.log(`[IndexedDBStorage] connect: opening db=${this.dbName}`);
380
- try {
381
- this.db = await Promise.race([
382
- this.openDatabase(),
383
- new Promise(
384
- (_, reject) => setTimeout(() => reject(new Error("IndexedDB open timed out after 5s")), 5e3)
385
- )
386
- ]);
387
- this.status = "connected";
388
- console.log(`[IndexedDBStorage] connect: connected to db=${this.dbName}`);
389
- } catch (error) {
390
- this.status = "error";
391
- throw new Error(`IndexedDB not available: ${error}`);
381
+ for (let attempt = 0; attempt < 2; attempt++) {
382
+ this.status = "connecting";
383
+ const t0 = Date.now();
384
+ console.log(`[IndexedDBStorage] connect: opening db=${this.dbName}, attempt=${attempt + 1}/2`);
385
+ try {
386
+ this.db = await Promise.race([
387
+ this.openDatabase(),
388
+ new Promise(
389
+ (_, reject) => setTimeout(() => reject(new Error("IndexedDB open timed out after 5s")), 5e3)
390
+ )
391
+ ]);
392
+ this.status = "connected";
393
+ console.log(`[IndexedDBStorage] connect: connected db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
394
+ return;
395
+ } catch (error) {
396
+ console.warn(`[IndexedDBStorage] connect: open failed db=${this.dbName} attempt=${attempt + 1} (${Date.now() - t0}ms):`, error);
397
+ if (attempt === 0) {
398
+ this.status = "disconnected";
399
+ await new Promise((r) => setTimeout(r, 1e3));
400
+ continue;
401
+ }
402
+ this.status = "error";
403
+ throw new Error(`IndexedDB not available: ${error}`);
404
+ }
392
405
  }
393
406
  }
394
407
  async disconnect() {
395
- console.log(`[IndexedDBStorage] disconnect: closing db=${this.dbName}, wasConnected=${!!this.db}`);
408
+ const cid = this.connId;
409
+ console.log(`[IndexedDBStorage] disconnect: db=${this.dbName} connId=${cid} wasConnected=${!!this.db}`);
396
410
  if (this.db) {
397
411
  this.db.close();
398
412
  this.db = null;
@@ -449,32 +463,36 @@ var IndexedDBStorageProvider = class {
449
463
  }
450
464
  async clear(prefix) {
451
465
  if (!prefix) {
452
- console.log(`[IndexedDBStorage] clear: starting, db=${this.dbName}, wasConnected=${!!this.db}`);
453
- if (this.db) {
454
- this.db.close();
455
- this.db = null;
456
- }
457
- this.status = "disconnected";
458
- await new Promise((resolve) => {
459
- try {
460
- const req = indexedDB.deleteDatabase(this.dbName);
461
- req.onsuccess = () => {
462
- console.log(`[IndexedDBStorage] clear: deleted db=${this.dbName}`);
463
- resolve();
464
- };
465
- req.onerror = () => {
466
- console.warn(`[IndexedDBStorage] clear: error deleting db=${this.dbName}`, req.error);
467
- resolve();
468
- };
469
- req.onblocked = () => {
470
- console.warn(`[IndexedDBStorage] clear: deleteDatabase blocked for db=${this.dbName}`);
471
- resolve();
472
- };
473
- } catch {
474
- resolve();
466
+ const t0 = Date.now();
467
+ const prevConnId = this.connId;
468
+ console.log(`[IndexedDBStorage] clear: starting db=${this.dbName} connId=${prevConnId} status=${this.status} hasDb=${!!this.db}`);
469
+ try {
470
+ if (!this.db || this.status !== "connected") {
471
+ if (this.db) {
472
+ console.log(`[IndexedDBStorage] clear: closing stale handle connId=${prevConnId}`);
473
+ this.db.close();
474
+ this.db = null;
475
+ }
476
+ console.log(`[IndexedDBStorage] clear: opening fresh connection for wipe`);
477
+ this.db = await Promise.race([
478
+ this.openDatabase(),
479
+ new Promise(
480
+ (_, reject) => setTimeout(() => reject(new Error("open timed out")), 3e3)
481
+ )
482
+ ]);
483
+ this.status = "connected";
475
484
  }
476
- });
477
- this.log("Database deleted:", this.dbName);
485
+ await this.idbClear();
486
+ console.log(`[IndexedDBStorage] clear: store cleared db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
487
+ } catch (err) {
488
+ console.warn(`[IndexedDBStorage] clear: failed db=${this.dbName} (${Date.now() - t0}ms)`, err);
489
+ } finally {
490
+ if (this.db) {
491
+ this.db.close();
492
+ this.db = null;
493
+ }
494
+ this.status = "disconnected";
495
+ }
478
496
  return;
479
497
  }
480
498
  this.ensureConnected();
@@ -540,9 +558,22 @@ var IndexedDBStorageProvider = class {
540
558
  return new Promise((resolve, reject) => {
541
559
  const request = indexedDB.open(this.dbName, DB_VERSION);
542
560
  request.onerror = () => reject(request.error);
543
- request.onsuccess = () => resolve(request.result);
561
+ request.onsuccess = () => {
562
+ const db = request.result;
563
+ const cid = ++connectionSeq;
564
+ this.connId = cid;
565
+ db.onversionchange = () => {
566
+ console.log(`[IndexedDBStorage] onversionchange: auto-closing db=${this.dbName} connId=${cid}`);
567
+ db.close();
568
+ if (this.db === db) {
569
+ this.db = null;
570
+ this.status = "disconnected";
571
+ }
572
+ };
573
+ resolve(db);
574
+ };
544
575
  request.onblocked = () => {
545
- console.warn("[IndexedDBStorageProvider] open blocked by another connection");
576
+ console.warn(`[IndexedDBStorage] open blocked by another connection, db=${this.dbName}`);
546
577
  };
547
578
  request.onupgradeneeded = (event) => {
548
579
  const db = event.target.result;
@@ -621,18 +652,23 @@ var DB_NAME2 = "sphere-token-storage";
621
652
  var DB_VERSION2 = 1;
622
653
  var STORE_TOKENS = "tokens";
623
654
  var STORE_META = "meta";
655
+ var connectionSeq2 = 0;
624
656
  var IndexedDBTokenStorageProvider = class {
625
657
  id = "indexeddb-token-storage";
626
658
  name = "IndexedDB Token Storage";
627
659
  type = "local";
628
660
  dbNamePrefix;
629
661
  dbName;
662
+ debug;
630
663
  db = null;
631
664
  status = "disconnected";
632
665
  identity = null;
666
+ /** Monotonic connection ID for tracing open/close pairs */
667
+ connId = 0;
633
668
  constructor(config) {
634
669
  this.dbNamePrefix = config?.dbNamePrefix ?? DB_NAME2;
635
670
  this.dbName = this.dbNamePrefix;
671
+ this.debug = config?.debug ?? false;
636
672
  }
637
673
  setIdentity(identity) {
638
674
  this.identity = identity;
@@ -640,28 +676,31 @@ var IndexedDBTokenStorageProvider = class {
640
676
  const addressId = getAddressId(identity.directAddress);
641
677
  this.dbName = `${this.dbNamePrefix}-${addressId}`;
642
678
  }
643
- console.log(`[IndexedDBTokenStorage] setIdentity \u2192 db=${this.dbName}`);
679
+ console.log(`[IndexedDBTokenStorage] setIdentity: db=${this.dbName}`);
644
680
  }
645
681
  async initialize() {
682
+ const prevConnId = this.connId;
683
+ const t0 = Date.now();
646
684
  try {
647
685
  if (this.db) {
648
- console.log(`[IndexedDBTokenStorage] initialize: closing existing connection before re-open (db=${this.dbName})`);
686
+ console.log(`[IndexedDBTokenStorage] initialize: closing existing connId=${prevConnId} before re-open (db=${this.dbName})`);
649
687
  this.db.close();
650
688
  this.db = null;
651
689
  }
652
690
  console.log(`[IndexedDBTokenStorage] initialize: opening db=${this.dbName}`);
653
691
  this.db = await this.openDatabase();
654
692
  this.status = "connected";
655
- console.log(`[IndexedDBTokenStorage] initialize: connected to db=${this.dbName}`);
693
+ console.log(`[IndexedDBTokenStorage] initialize: connected db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
656
694
  return true;
657
695
  } catch (error) {
658
- console.error("[IndexedDBTokenStorage] Failed to initialize:", error);
696
+ console.error(`[IndexedDBTokenStorage] initialize: failed db=${this.dbName} (${Date.now() - t0}ms):`, error);
659
697
  this.status = "error";
660
698
  return false;
661
699
  }
662
700
  }
663
701
  async shutdown() {
664
- console.log(`[IndexedDBTokenStorage] shutdown: closing db=${this.dbName}, wasConnected=${!!this.db}`);
702
+ const cid = this.connId;
703
+ console.log(`[IndexedDBTokenStorage] shutdown: db=${this.dbName} connId=${cid} wasConnected=${!!this.db}`);
665
704
  if (this.db) {
666
705
  this.db.close();
667
706
  this.db = null;
@@ -819,56 +858,32 @@ var IndexedDBTokenStorageProvider = class {
819
858
  return meta !== null;
820
859
  }
821
860
  async clear() {
861
+ const t0 = Date.now();
822
862
  try {
823
- console.log(`[IndexedDBTokenStorage] clear: starting, db=${this.dbName}, wasConnected=${!!this.db}`);
824
863
  if (this.db) {
825
864
  this.db.close();
826
865
  this.db = null;
827
866
  }
828
867
  this.status = "disconnected";
829
- const dbNames = [this.dbName];
830
- if (typeof indexedDB.databases === "function") {
831
- try {
832
- const dbs = await Promise.race([
833
- indexedDB.databases(),
834
- new Promise(
835
- (_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)
836
- )
837
- ]);
838
- for (const dbInfo of dbs) {
839
- if (dbInfo.name && dbInfo.name.startsWith(this.dbNamePrefix) && dbInfo.name !== this.dbName) {
840
- dbNames.push(dbInfo.name);
841
- }
842
- }
843
- } catch {
844
- }
868
+ const dbNames = /* @__PURE__ */ new Set([this.dbName]);
869
+ for (const name of await this.findPrefixedDatabases()) {
870
+ dbNames.add(name);
845
871
  }
846
- console.log(`[IndexedDBTokenStorage] clear: deleting ${dbNames.length} database(s):`, dbNames);
847
- await Promise.all(dbNames.map(
848
- (name) => new Promise((resolve) => {
849
- try {
850
- const req = indexedDB.deleteDatabase(name);
851
- req.onsuccess = () => {
852
- console.log(`[IndexedDBTokenStorage] clear: deleted db=${name}`);
853
- resolve();
854
- };
855
- req.onerror = () => {
856
- console.warn(`[IndexedDBTokenStorage] clear: error deleting db=${name}`, req.error);
857
- resolve();
858
- };
859
- req.onblocked = () => {
860
- console.warn(`[IndexedDBTokenStorage] clear: deleteDatabase blocked for db=${name}`);
861
- resolve();
862
- };
863
- } catch {
864
- resolve();
865
- }
866
- })
867
- ));
868
- console.log(`[IndexedDBTokenStorage] clear: done`);
869
- return true;
872
+ console.log(`[IndexedDBTokenStorage] clear: clearing ${dbNames.size} database(s) (${[...dbNames].join(", ")})`);
873
+ const results = await Promise.allSettled(
874
+ [...dbNames].map((name) => this.clearDatabaseStores(name))
875
+ );
876
+ const failed = results.filter((r) => r.status === "rejected");
877
+ if (failed.length > 0) {
878
+ console.warn(
879
+ `[IndexedDBTokenStorage] clear: ${failed.length}/${dbNames.size} failed (${Date.now() - t0}ms)`,
880
+ failed.map((r) => r.reason)
881
+ );
882
+ }
883
+ console.log(`[IndexedDBTokenStorage] clear: done ${dbNames.size} database(s) (${Date.now() - t0}ms)`);
884
+ return failed.length === 0;
870
885
  } catch (err) {
871
- console.warn("[IndexedDBTokenStorage] clear() failed:", err);
886
+ console.warn(`[IndexedDBTokenStorage] clear: failed (${Date.now() - t0}ms)`, err);
872
887
  return false;
873
888
  }
874
889
  }
@@ -878,11 +893,23 @@ var IndexedDBTokenStorageProvider = class {
878
893
  openDatabase() {
879
894
  return new Promise((resolve, reject) => {
880
895
  const request = indexedDB.open(this.dbName, DB_VERSION2);
881
- request.onerror = () => {
882
- reject(request.error);
883
- };
896
+ request.onerror = () => reject(request.error);
884
897
  request.onsuccess = () => {
885
- resolve(request.result);
898
+ const db = request.result;
899
+ const cid = ++connectionSeq2;
900
+ this.connId = cid;
901
+ db.onversionchange = () => {
902
+ console.log(`[IndexedDBTokenStorage] onversionchange: auto-closing db=${this.dbName} connId=${cid}`);
903
+ db.close();
904
+ if (this.db === db) {
905
+ this.db = null;
906
+ this.status = "disconnected";
907
+ }
908
+ };
909
+ resolve(db);
910
+ };
911
+ request.onblocked = () => {
912
+ console.warn(`[IndexedDBTokenStorage] open blocked by another connection, db=${this.dbName}`);
886
913
  };
887
914
  request.onupgradeneeded = (event) => {
888
915
  const db = event.target.result;
@@ -947,18 +974,70 @@ var IndexedDBTokenStorageProvider = class {
947
974
  request.onsuccess = () => resolve();
948
975
  });
949
976
  }
950
- clearStore(storeName) {
951
- return new Promise((resolve, reject) => {
952
- if (!this.db) {
953
- resolve();
954
- return;
977
+ /**
978
+ * Find all IndexedDB databases with our prefix.
979
+ * Returns empty array if indexedDB.databases() is unavailable (older browsers).
980
+ */
981
+ async findPrefixedDatabases() {
982
+ if (typeof indexedDB.databases !== "function") return [];
983
+ try {
984
+ const allDbs = await Promise.race([
985
+ indexedDB.databases(),
986
+ new Promise(
987
+ (_, reject) => setTimeout(() => reject(new Error("databases() timed out")), 1500)
988
+ )
989
+ ]);
990
+ return allDbs.map((info) => info.name).filter((name) => !!name && name.startsWith(this.dbNamePrefix));
991
+ } catch {
992
+ return [];
993
+ }
994
+ }
995
+ /**
996
+ * Clear all object stores in a single database.
997
+ * Opens a temporary connection, clears STORE_TOKENS and STORE_META, then closes.
998
+ * Uses IDBObjectStore.clear() which is a normal readwrite transaction — cannot
999
+ * be blocked by other connections (unlike deleteDatabase()).
1000
+ */
1001
+ async clearDatabaseStores(dbName) {
1002
+ const db = await Promise.race([
1003
+ new Promise((resolve, reject) => {
1004
+ const req = indexedDB.open(dbName, DB_VERSION2);
1005
+ req.onerror = () => reject(req.error);
1006
+ req.onsuccess = () => {
1007
+ const db2 = req.result;
1008
+ db2.onversionchange = () => {
1009
+ db2.close();
1010
+ };
1011
+ resolve(db2);
1012
+ };
1013
+ req.onupgradeneeded = (event) => {
1014
+ const db2 = event.target.result;
1015
+ if (!db2.objectStoreNames.contains(STORE_TOKENS)) {
1016
+ db2.createObjectStore(STORE_TOKENS, { keyPath: "id" });
1017
+ }
1018
+ if (!db2.objectStoreNames.contains(STORE_META)) {
1019
+ db2.createObjectStore(STORE_META);
1020
+ }
1021
+ };
1022
+ }),
1023
+ new Promise(
1024
+ (_, reject) => setTimeout(() => reject(new Error(`open timed out: ${dbName}`)), 3e3)
1025
+ )
1026
+ ]);
1027
+ try {
1028
+ for (const storeName of [STORE_TOKENS, STORE_META]) {
1029
+ if (db.objectStoreNames.contains(storeName)) {
1030
+ await new Promise((resolve, reject) => {
1031
+ const tx = db.transaction(storeName, "readwrite");
1032
+ const req = tx.objectStore(storeName).clear();
1033
+ req.onerror = () => reject(req.error);
1034
+ req.onsuccess = () => resolve();
1035
+ });
1036
+ }
955
1037
  }
956
- const transaction = this.db.transaction(storeName, "readwrite");
957
- const store = transaction.objectStore(storeName);
958
- const request = store.clear();
959
- request.onerror = () => reject(request.error);
960
- request.onsuccess = () => resolve();
961
- });
1038
+ } finally {
1039
+ db.close();
1040
+ }
962
1041
  }
963
1042
  };
964
1043
  function createIndexedDBTokenStorageProvider(config) {