@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.
@@ -414,6 +414,7 @@ function createLocalStorageProvider(config) {
414
414
  var DB_NAME = "sphere-storage";
415
415
  var DB_VERSION = 1;
416
416
  var STORE_NAME = "kv";
417
+ var connectionSeq = 0;
417
418
  var IndexedDBStorageProvider = class {
418
419
  id = "indexeddb-storage";
419
420
  name = "IndexedDB Storage";
@@ -425,6 +426,8 @@ var IndexedDBStorageProvider = class {
425
426
  identity = null;
426
427
  status = "disconnected";
427
428
  db = null;
429
+ /** Monotonic connection ID for tracing open/close pairs */
430
+ connId = 0;
428
431
  constructor(config) {
429
432
  this.prefix = config?.prefix ?? "sphere_";
430
433
  this.dbName = config?.dbName ?? DB_NAME;
@@ -435,24 +438,35 @@ var IndexedDBStorageProvider = class {
435
438
  // ===========================================================================
436
439
  async connect() {
437
440
  if (this.status === "connected" && this.db) return;
438
- this.status = "connecting";
439
- console.log(`[IndexedDBStorage] connect: opening db=${this.dbName}`);
440
- try {
441
- this.db = await Promise.race([
442
- this.openDatabase(),
443
- new Promise(
444
- (_, reject) => setTimeout(() => reject(new Error("IndexedDB open timed out after 5s")), 5e3)
445
- )
446
- ]);
447
- this.status = "connected";
448
- console.log(`[IndexedDBStorage] connect: connected to db=${this.dbName}`);
449
- } catch (error) {
450
- this.status = "error";
451
- throw new Error(`IndexedDB not available: ${error}`);
441
+ for (let attempt = 0; attempt < 2; attempt++) {
442
+ this.status = "connecting";
443
+ const t0 = Date.now();
444
+ console.log(`[IndexedDBStorage] connect: opening db=${this.dbName}, attempt=${attempt + 1}/2`);
445
+ try {
446
+ this.db = await Promise.race([
447
+ this.openDatabase(),
448
+ new Promise(
449
+ (_, reject) => setTimeout(() => reject(new Error("IndexedDB open timed out after 5s")), 5e3)
450
+ )
451
+ ]);
452
+ this.status = "connected";
453
+ console.log(`[IndexedDBStorage] connect: connected db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
454
+ return;
455
+ } catch (error) {
456
+ console.warn(`[IndexedDBStorage] connect: open failed db=${this.dbName} attempt=${attempt + 1} (${Date.now() - t0}ms):`, error);
457
+ if (attempt === 0) {
458
+ this.status = "disconnected";
459
+ await new Promise((r) => setTimeout(r, 1e3));
460
+ continue;
461
+ }
462
+ this.status = "error";
463
+ throw new Error(`IndexedDB not available: ${error}`);
464
+ }
452
465
  }
453
466
  }
454
467
  async disconnect() {
455
- console.log(`[IndexedDBStorage] disconnect: closing db=${this.dbName}, wasConnected=${!!this.db}`);
468
+ const cid = this.connId;
469
+ console.log(`[IndexedDBStorage] disconnect: db=${this.dbName} connId=${cid} wasConnected=${!!this.db}`);
456
470
  if (this.db) {
457
471
  this.db.close();
458
472
  this.db = null;
@@ -509,32 +523,36 @@ var IndexedDBStorageProvider = class {
509
523
  }
510
524
  async clear(prefix) {
511
525
  if (!prefix) {
512
- console.log(`[IndexedDBStorage] clear: starting, db=${this.dbName}, wasConnected=${!!this.db}`);
513
- if (this.db) {
514
- this.db.close();
515
- this.db = null;
516
- }
517
- this.status = "disconnected";
518
- await new Promise((resolve) => {
519
- try {
520
- const req = indexedDB.deleteDatabase(this.dbName);
521
- req.onsuccess = () => {
522
- console.log(`[IndexedDBStorage] clear: deleted db=${this.dbName}`);
523
- resolve();
524
- };
525
- req.onerror = () => {
526
- console.warn(`[IndexedDBStorage] clear: error deleting db=${this.dbName}`, req.error);
527
- resolve();
528
- };
529
- req.onblocked = () => {
530
- console.warn(`[IndexedDBStorage] clear: deleteDatabase blocked for db=${this.dbName}`);
531
- resolve();
532
- };
533
- } catch {
534
- resolve();
526
+ const t0 = Date.now();
527
+ const prevConnId = this.connId;
528
+ console.log(`[IndexedDBStorage] clear: starting db=${this.dbName} connId=${prevConnId} status=${this.status} hasDb=${!!this.db}`);
529
+ try {
530
+ if (!this.db || this.status !== "connected") {
531
+ if (this.db) {
532
+ console.log(`[IndexedDBStorage] clear: closing stale handle connId=${prevConnId}`);
533
+ this.db.close();
534
+ this.db = null;
535
+ }
536
+ console.log(`[IndexedDBStorage] clear: opening fresh connection for wipe`);
537
+ this.db = await Promise.race([
538
+ this.openDatabase(),
539
+ new Promise(
540
+ (_, reject) => setTimeout(() => reject(new Error("open timed out")), 3e3)
541
+ )
542
+ ]);
543
+ this.status = "connected";
535
544
  }
536
- });
537
- this.log("Database deleted:", this.dbName);
545
+ await this.idbClear();
546
+ console.log(`[IndexedDBStorage] clear: store cleared db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
547
+ } catch (err) {
548
+ console.warn(`[IndexedDBStorage] clear: failed db=${this.dbName} (${Date.now() - t0}ms)`, err);
549
+ } finally {
550
+ if (this.db) {
551
+ this.db.close();
552
+ this.db = null;
553
+ }
554
+ this.status = "disconnected";
555
+ }
538
556
  return;
539
557
  }
540
558
  this.ensureConnected();
@@ -600,9 +618,22 @@ var IndexedDBStorageProvider = class {
600
618
  return new Promise((resolve, reject) => {
601
619
  const request = indexedDB.open(this.dbName, DB_VERSION);
602
620
  request.onerror = () => reject(request.error);
603
- request.onsuccess = () => resolve(request.result);
621
+ request.onsuccess = () => {
622
+ const db = request.result;
623
+ const cid = ++connectionSeq;
624
+ this.connId = cid;
625
+ db.onversionchange = () => {
626
+ console.log(`[IndexedDBStorage] onversionchange: auto-closing db=${this.dbName} connId=${cid}`);
627
+ db.close();
628
+ if (this.db === db) {
629
+ this.db = null;
630
+ this.status = "disconnected";
631
+ }
632
+ };
633
+ resolve(db);
634
+ };
604
635
  request.onblocked = () => {
605
- console.warn("[IndexedDBStorageProvider] open blocked by another connection");
636
+ console.warn(`[IndexedDBStorage] open blocked by another connection, db=${this.dbName}`);
606
637
  };
607
638
  request.onupgradeneeded = (event) => {
608
639
  const db = event.target.result;
@@ -681,18 +712,23 @@ var DB_NAME2 = "sphere-token-storage";
681
712
  var DB_VERSION2 = 1;
682
713
  var STORE_TOKENS = "tokens";
683
714
  var STORE_META = "meta";
715
+ var connectionSeq2 = 0;
684
716
  var IndexedDBTokenStorageProvider = class {
685
717
  id = "indexeddb-token-storage";
686
718
  name = "IndexedDB Token Storage";
687
719
  type = "local";
688
720
  dbNamePrefix;
689
721
  dbName;
722
+ debug;
690
723
  db = null;
691
724
  status = "disconnected";
692
725
  identity = null;
726
+ /** Monotonic connection ID for tracing open/close pairs */
727
+ connId = 0;
693
728
  constructor(config) {
694
729
  this.dbNamePrefix = config?.dbNamePrefix ?? DB_NAME2;
695
730
  this.dbName = this.dbNamePrefix;
731
+ this.debug = config?.debug ?? false;
696
732
  }
697
733
  setIdentity(identity) {
698
734
  this.identity = identity;
@@ -700,28 +736,31 @@ var IndexedDBTokenStorageProvider = class {
700
736
  const addressId = getAddressId(identity.directAddress);
701
737
  this.dbName = `${this.dbNamePrefix}-${addressId}`;
702
738
  }
703
- console.log(`[IndexedDBTokenStorage] setIdentity \u2192 db=${this.dbName}`);
739
+ console.log(`[IndexedDBTokenStorage] setIdentity: db=${this.dbName}`);
704
740
  }
705
741
  async initialize() {
742
+ const prevConnId = this.connId;
743
+ const t0 = Date.now();
706
744
  try {
707
745
  if (this.db) {
708
- console.log(`[IndexedDBTokenStorage] initialize: closing existing connection before re-open (db=${this.dbName})`);
746
+ console.log(`[IndexedDBTokenStorage] initialize: closing existing connId=${prevConnId} before re-open (db=${this.dbName})`);
709
747
  this.db.close();
710
748
  this.db = null;
711
749
  }
712
750
  console.log(`[IndexedDBTokenStorage] initialize: opening db=${this.dbName}`);
713
751
  this.db = await this.openDatabase();
714
752
  this.status = "connected";
715
- console.log(`[IndexedDBTokenStorage] initialize: connected to db=${this.dbName}`);
753
+ console.log(`[IndexedDBTokenStorage] initialize: connected db=${this.dbName} connId=${this.connId} (${Date.now() - t0}ms)`);
716
754
  return true;
717
755
  } catch (error) {
718
- console.error("[IndexedDBTokenStorage] Failed to initialize:", error);
756
+ console.error(`[IndexedDBTokenStorage] initialize: failed db=${this.dbName} (${Date.now() - t0}ms):`, error);
719
757
  this.status = "error";
720
758
  return false;
721
759
  }
722
760
  }
723
761
  async shutdown() {
724
- console.log(`[IndexedDBTokenStorage] shutdown: closing db=${this.dbName}, wasConnected=${!!this.db}`);
762
+ const cid = this.connId;
763
+ console.log(`[IndexedDBTokenStorage] shutdown: db=${this.dbName} connId=${cid} wasConnected=${!!this.db}`);
725
764
  if (this.db) {
726
765
  this.db.close();
727
766
  this.db = null;
@@ -879,56 +918,32 @@ var IndexedDBTokenStorageProvider = class {
879
918
  return meta !== null;
880
919
  }
881
920
  async clear() {
921
+ const t0 = Date.now();
882
922
  try {
883
- console.log(`[IndexedDBTokenStorage] clear: starting, db=${this.dbName}, wasConnected=${!!this.db}`);
884
923
  if (this.db) {
885
924
  this.db.close();
886
925
  this.db = null;
887
926
  }
888
927
  this.status = "disconnected";
889
- const dbNames = [this.dbName];
890
- if (typeof indexedDB.databases === "function") {
891
- try {
892
- const dbs = await Promise.race([
893
- indexedDB.databases(),
894
- new Promise(
895
- (_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)
896
- )
897
- ]);
898
- for (const dbInfo of dbs) {
899
- if (dbInfo.name && dbInfo.name.startsWith(this.dbNamePrefix) && dbInfo.name !== this.dbName) {
900
- dbNames.push(dbInfo.name);
901
- }
902
- }
903
- } catch {
904
- }
928
+ const dbNames = /* @__PURE__ */ new Set([this.dbName]);
929
+ for (const name of await this.findPrefixedDatabases()) {
930
+ dbNames.add(name);
905
931
  }
906
- console.log(`[IndexedDBTokenStorage] clear: deleting ${dbNames.length} database(s):`, dbNames);
907
- await Promise.all(dbNames.map(
908
- (name) => new Promise((resolve) => {
909
- try {
910
- const req = indexedDB.deleteDatabase(name);
911
- req.onsuccess = () => {
912
- console.log(`[IndexedDBTokenStorage] clear: deleted db=${name}`);
913
- resolve();
914
- };
915
- req.onerror = () => {
916
- console.warn(`[IndexedDBTokenStorage] clear: error deleting db=${name}`, req.error);
917
- resolve();
918
- };
919
- req.onblocked = () => {
920
- console.warn(`[IndexedDBTokenStorage] clear: deleteDatabase blocked for db=${name}`);
921
- resolve();
922
- };
923
- } catch {
924
- resolve();
925
- }
926
- })
927
- ));
928
- console.log(`[IndexedDBTokenStorage] clear: done`);
929
- return true;
932
+ console.log(`[IndexedDBTokenStorage] clear: clearing ${dbNames.size} database(s) (${[...dbNames].join(", ")})`);
933
+ const results = await Promise.allSettled(
934
+ [...dbNames].map((name) => this.clearDatabaseStores(name))
935
+ );
936
+ const failed = results.filter((r) => r.status === "rejected");
937
+ if (failed.length > 0) {
938
+ console.warn(
939
+ `[IndexedDBTokenStorage] clear: ${failed.length}/${dbNames.size} failed (${Date.now() - t0}ms)`,
940
+ failed.map((r) => r.reason)
941
+ );
942
+ }
943
+ console.log(`[IndexedDBTokenStorage] clear: done ${dbNames.size} database(s) (${Date.now() - t0}ms)`);
944
+ return failed.length === 0;
930
945
  } catch (err) {
931
- console.warn("[IndexedDBTokenStorage] clear() failed:", err);
946
+ console.warn(`[IndexedDBTokenStorage] clear: failed (${Date.now() - t0}ms)`, err);
932
947
  return false;
933
948
  }
934
949
  }
@@ -938,11 +953,23 @@ var IndexedDBTokenStorageProvider = class {
938
953
  openDatabase() {
939
954
  return new Promise((resolve, reject) => {
940
955
  const request = indexedDB.open(this.dbName, DB_VERSION2);
941
- request.onerror = () => {
942
- reject(request.error);
943
- };
956
+ request.onerror = () => reject(request.error);
944
957
  request.onsuccess = () => {
945
- resolve(request.result);
958
+ const db = request.result;
959
+ const cid = ++connectionSeq2;
960
+ this.connId = cid;
961
+ db.onversionchange = () => {
962
+ console.log(`[IndexedDBTokenStorage] onversionchange: auto-closing db=${this.dbName} connId=${cid}`);
963
+ db.close();
964
+ if (this.db === db) {
965
+ this.db = null;
966
+ this.status = "disconnected";
967
+ }
968
+ };
969
+ resolve(db);
970
+ };
971
+ request.onblocked = () => {
972
+ console.warn(`[IndexedDBTokenStorage] open blocked by another connection, db=${this.dbName}`);
946
973
  };
947
974
  request.onupgradeneeded = (event) => {
948
975
  const db = event.target.result;
@@ -1007,18 +1034,70 @@ var IndexedDBTokenStorageProvider = class {
1007
1034
  request.onsuccess = () => resolve();
1008
1035
  });
1009
1036
  }
1010
- clearStore(storeName) {
1011
- return new Promise((resolve, reject) => {
1012
- if (!this.db) {
1013
- resolve();
1014
- return;
1037
+ /**
1038
+ * Find all IndexedDB databases with our prefix.
1039
+ * Returns empty array if indexedDB.databases() is unavailable (older browsers).
1040
+ */
1041
+ async findPrefixedDatabases() {
1042
+ if (typeof indexedDB.databases !== "function") return [];
1043
+ try {
1044
+ const allDbs = await Promise.race([
1045
+ indexedDB.databases(),
1046
+ new Promise(
1047
+ (_, reject) => setTimeout(() => reject(new Error("databases() timed out")), 1500)
1048
+ )
1049
+ ]);
1050
+ return allDbs.map((info) => info.name).filter((name) => !!name && name.startsWith(this.dbNamePrefix));
1051
+ } catch {
1052
+ return [];
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Clear all object stores in a single database.
1057
+ * Opens a temporary connection, clears STORE_TOKENS and STORE_META, then closes.
1058
+ * Uses IDBObjectStore.clear() which is a normal readwrite transaction — cannot
1059
+ * be blocked by other connections (unlike deleteDatabase()).
1060
+ */
1061
+ async clearDatabaseStores(dbName) {
1062
+ const db = await Promise.race([
1063
+ new Promise((resolve, reject) => {
1064
+ const req = indexedDB.open(dbName, DB_VERSION2);
1065
+ req.onerror = () => reject(req.error);
1066
+ req.onsuccess = () => {
1067
+ const db2 = req.result;
1068
+ db2.onversionchange = () => {
1069
+ db2.close();
1070
+ };
1071
+ resolve(db2);
1072
+ };
1073
+ req.onupgradeneeded = (event) => {
1074
+ const db2 = event.target.result;
1075
+ if (!db2.objectStoreNames.contains(STORE_TOKENS)) {
1076
+ db2.createObjectStore(STORE_TOKENS, { keyPath: "id" });
1077
+ }
1078
+ if (!db2.objectStoreNames.contains(STORE_META)) {
1079
+ db2.createObjectStore(STORE_META);
1080
+ }
1081
+ };
1082
+ }),
1083
+ new Promise(
1084
+ (_, reject) => setTimeout(() => reject(new Error(`open timed out: ${dbName}`)), 3e3)
1085
+ )
1086
+ ]);
1087
+ try {
1088
+ for (const storeName of [STORE_TOKENS, STORE_META]) {
1089
+ if (db.objectStoreNames.contains(storeName)) {
1090
+ await new Promise((resolve, reject) => {
1091
+ const tx = db.transaction(storeName, "readwrite");
1092
+ const req = tx.objectStore(storeName).clear();
1093
+ req.onerror = () => reject(req.error);
1094
+ req.onsuccess = () => resolve();
1095
+ });
1096
+ }
1015
1097
  }
1016
- const transaction = this.db.transaction(storeName, "readwrite");
1017
- const store = transaction.objectStore(storeName);
1018
- const request = store.clear();
1019
- request.onerror = () => reject(request.error);
1020
- request.onsuccess = () => resolve();
1021
- });
1098
+ } finally {
1099
+ db.close();
1100
+ }
1022
1101
  }
1023
1102
  };
1024
1103
  function createIndexedDBTokenStorageProvider(config) {