@unicitylabs/sphere-sdk 0.6.1 → 0.6.3

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.
@@ -758,7 +758,7 @@ var STORE_TOKENS = "tokens";
758
758
  var STORE_META = "meta";
759
759
  var STORE_HISTORY = "history";
760
760
  var connectionSeq2 = 0;
761
- var IndexedDBTokenStorageProvider = class {
761
+ var IndexedDBTokenStorageProvider = class _IndexedDBTokenStorageProvider {
762
762
  id = "indexeddb-token-storage";
763
763
  name = "IndexedDB Token Storage";
764
764
  type = "local";
@@ -1206,6 +1206,16 @@ var IndexedDBTokenStorageProvider = class {
1206
1206
  db.close();
1207
1207
  }
1208
1208
  }
1209
+ /**
1210
+ * Create an independent instance for a different address.
1211
+ * The new instance shares the same config but has its own IDB connection.
1212
+ */
1213
+ createForAddress() {
1214
+ return new _IndexedDBTokenStorageProvider({
1215
+ dbNamePrefix: this.dbNamePrefix,
1216
+ debug: this.debug
1217
+ });
1218
+ }
1209
1219
  };
1210
1220
  function createIndexedDBTokenStorageProvider(config) {
1211
1221
  return new IndexedDBTokenStorageProvider(config);
@@ -1634,6 +1644,8 @@ var NostrTransportProvider = class {
1634
1644
  storage = null;
1635
1645
  /** In-memory max event timestamp to avoid read-before-write races in updateLastEventTimestamp. */
1636
1646
  lastEventTs = 0;
1647
+ /** Fallback 'since' timestamp for first-time address subscriptions (consumed once). */
1648
+ fallbackSince = null;
1637
1649
  identity = null;
1638
1650
  keyManager = null;
1639
1651
  status = "disconnected";
@@ -1666,6 +1678,48 @@ var NostrTransportProvider = class {
1666
1678
  };
1667
1679
  this.storage = config.storage ?? null;
1668
1680
  }
1681
+ /**
1682
+ * Get the WebSocket factory (used by MultiAddressTransportMux to share the same factory).
1683
+ */
1684
+ getWebSocketFactory() {
1685
+ return this.config.createWebSocket;
1686
+ }
1687
+ /**
1688
+ * Get the configured relay URLs.
1689
+ */
1690
+ getConfiguredRelays() {
1691
+ return [...this.config.relays];
1692
+ }
1693
+ /**
1694
+ * Get the storage adapter.
1695
+ */
1696
+ getStorageAdapter() {
1697
+ return this.storage;
1698
+ }
1699
+ /**
1700
+ * Suppress event subscriptions — unsubscribe wallet/chat filters
1701
+ * but keep the connection alive for resolve/identity-binding operations.
1702
+ * Used when MultiAddressTransportMux takes over event handling.
1703
+ */
1704
+ suppressSubscriptions() {
1705
+ if (!this.nostrClient) return;
1706
+ if (this.walletSubscriptionId) {
1707
+ this.nostrClient.unsubscribe(this.walletSubscriptionId);
1708
+ this.walletSubscriptionId = null;
1709
+ }
1710
+ if (this.chatSubscriptionId) {
1711
+ this.nostrClient.unsubscribe(this.chatSubscriptionId);
1712
+ this.chatSubscriptionId = null;
1713
+ }
1714
+ if (this.mainSubscriptionId) {
1715
+ this.nostrClient.unsubscribe(this.mainSubscriptionId);
1716
+ this.mainSubscriptionId = null;
1717
+ }
1718
+ this._subscriptionsSuppressed = true;
1719
+ logger.debug("Nostr", "Subscriptions suppressed \u2014 mux handles event routing");
1720
+ }
1721
+ // Flag to prevent re-subscription after suppressSubscriptions()
1722
+ _subscriptionsSuppressed = false;
1669
1723
  // ===========================================================================
1670
1724
  // BaseProvider Implementation
1671
1725
  // ===========================================================================
@@ -1733,6 +1787,7 @@ var NostrTransportProvider = class {
1733
1787
  this.mainSubscriptionId = null;
1734
1788
  this.walletSubscriptionId = null;
1735
1789
  this.chatSubscriptionId = null;
1790
+ this.chatEoseFired = false;
1736
1791
  this.status = "disconnected";
1737
1792
  this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
1738
1793
  logger.debug("Nostr", "Disconnected from all relays");
@@ -1843,6 +1898,8 @@ var NostrTransportProvider = class {
1843
1898
  // ===========================================================================
1844
1899
  async setIdentity(identity) {
1845
1900
  this.identity = identity;
1901
+ this.processedEventIds.clear();
1902
+ this.lastEventTs = 0;
1846
1903
  const secretKey = Buffer2.from(identity.privateKey, "hex");
1847
1904
  this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
1848
1905
  const nostrPubkey = this.keyManager.getPublicKeyHex();
@@ -1885,6 +1942,9 @@ var NostrTransportProvider = class {
1885
1942
  await this.subscribeToEvents();
1886
1943
  }
1887
1944
  }
1945
+ setFallbackSince(sinceSeconds) {
1946
+ this.fallbackSince = sinceSeconds;
1947
+ }
1888
1948
  /**
1889
1949
  * Get the Nostr-format public key (32 bytes / 64 hex chars)
1890
1950
  * This is the x-coordinate only, without the 02/03 prefix.
@@ -2056,6 +2116,20 @@ var NostrTransportProvider = class {
2056
2116
  this.typingIndicatorHandlers.add(handler);
2057
2117
  return () => this.typingIndicatorHandlers.delete(handler);
2058
2118
  }
2119
+ onChatReady(handler) {
2120
+ if (this.chatEoseFired) {
2121
+ try {
2122
+ handler();
2123
+ } catch {
2124
+ }
2125
+ return () => {
2126
+ };
2127
+ }
2128
+ this.chatEoseHandlers.push(handler);
2129
+ return () => {
2130
+ this.chatEoseHandlers = this.chatEoseHandlers.filter((h) => h !== handler);
2131
+ };
2132
+ }
2059
2133
  // ===========================================================================
2060
2134
  // Composing Indicators (NIP-59 kind 25050)
2061
2135
  // ===========================================================================
@@ -2773,8 +2847,15 @@ var NostrTransportProvider = class {
2773
2847
  // Track subscription IDs for cleanup
2774
2848
  walletSubscriptionId = null;
2775
2849
  chatSubscriptionId = null;
2850
+ // Chat EOSE handlers — fired once when relay finishes delivering stored DMs
2851
+ chatEoseHandlers = [];
2852
+ chatEoseFired = false;
2776
2853
  async subscribeToEvents() {
2777
2854
  logger.debug("Nostr", "subscribeToEvents called, identity:", !!this.identity, "keyManager:", !!this.keyManager, "nostrClient:", !!this.nostrClient);
2855
+ if (this._subscriptionsSuppressed) {
2856
+ logger.debug("Nostr", "subscribeToEvents: suppressed \u2014 mux handles event routing");
2857
+ return;
2858
+ }
2778
2859
  if (!this.identity || !this.keyManager || !this.nostrClient) {
2779
2860
  logger.debug("Nostr", "subscribeToEvents: skipped - no identity, keyManager, or nostrClient");
2780
2861
  return;
@@ -2801,7 +2882,13 @@ var NostrTransportProvider = class {
2801
2882
  if (stored) {
2802
2883
  since = parseInt(stored, 10);
2803
2884
  this.lastEventTs = since;
2885
+ this.fallbackSince = null;
2804
2886
  logger.debug("Nostr", "Resuming from stored event timestamp:", since);
2887
+ } else if (this.fallbackSince !== null) {
2888
+ since = this.fallbackSince;
2889
+ this.lastEventTs = since;
2890
+ this.fallbackSince = null;
2891
+ logger.debug("Nostr", "Using fallback since timestamp:", since);
2805
2892
  } else {
2806
2893
  since = Math.floor(Date.now() / 1e3);
2807
2894
  logger.debug("Nostr", "No stored timestamp, starting from now:", since);
@@ -2809,6 +2896,7 @@ var NostrTransportProvider = class {
2809
2896
  } catch (err) {
2810
2897
  logger.debug("Nostr", "Failed to read last event timestamp, falling back to now:", err);
2811
2898
  since = Math.floor(Date.now() / 1e3);
2899
+ this.fallbackSince = null;
2812
2900
  }
2813
2901
  } else {
2814
2902
  since = Math.floor(Date.now() / 1e3) - 86400;
@@ -2862,6 +2950,16 @@ var NostrTransportProvider = class {
2862
2950
  },
2863
2951
  onEndOfStoredEvents: () => {
2864
2952
  logger.debug("Nostr", "Chat subscription ready (EOSE)");
2953
+ if (!this.chatEoseFired) {
2954
+ this.chatEoseFired = true;
2955
+ for (const handler of this.chatEoseHandlers) {
2956
+ try {
2957
+ handler();
2958
+ } catch {
2959
+ }
2960
+ }
2961
+ this.chatEoseHandlers = [];
2962
+ }
2865
2963
  },
2866
2964
  onError: (_subId, error) => {
2867
2965
  logger.debug("Nostr", "Chat subscription error:", error);
@@ -4819,11 +4917,17 @@ var AsyncSerialQueue = class {
4819
4917
  var WriteBuffer = class {
4820
4918
  /** Full TXF data from save() calls — latest wins */
4821
4919
  txfData = null;
4920
+ /** IPNS context captured at save() time — ensures flush writes to the correct
4921
+ * IPNS record even if identity changes between save() and flush(). */
4922
+ capturedIpnsKeyPair = null;
4923
+ capturedIpnsName = null;
4822
4924
  get isEmpty() {
4823
4925
  return this.txfData === null;
4824
4926
  }
4825
4927
  clear() {
4826
4928
  this.txfData = null;
4929
+ this.capturedIpnsKeyPair = null;
4930
+ this.capturedIpnsName = null;
4827
4931
  }
4828
4932
  /**
4829
4933
  * Merge another buffer's contents into this one (for rollback).
@@ -4832,12 +4936,14 @@ var WriteBuffer = class {
4832
4936
  mergeFrom(other) {
4833
4937
  if (other.txfData && !this.txfData) {
4834
4938
  this.txfData = other.txfData;
4939
+ this.capturedIpnsKeyPair = other.capturedIpnsKeyPair;
4940
+ this.capturedIpnsName = other.capturedIpnsName;
4835
4941
  }
4836
4942
  }
4837
4943
  };
4838
4944
 
4839
4945
  // impl/shared/ipfs/ipfs-storage-provider.ts
4840
- var IpfsStorageProvider = class {
4946
+ var IpfsStorageProvider = class _IpfsStorageProvider {
4841
4947
  id = "ipfs";
4842
4948
  name = "IPFS Storage";
4843
4949
  type = "p2p";
@@ -4882,7 +4988,12 @@ var IpfsStorageProvider = class {
4882
4988
  flushDebounceMs;
4883
4989
  /** Set to true during shutdown to prevent new flushes */
4884
4990
  isShuttingDown = false;
4991
+ /** Stored config for createForAddress() cloning */
4992
+ _config;
4993
+ _statePersistenceCtor;
4885
4994
  constructor(config, statePersistence) {
4995
+ this._config = config;
4996
+ this._statePersistenceCtor = statePersistence;
4886
4997
  const gateways = config?.gateways ?? getIpfsGatewayUrls();
4887
4998
  this.debug = config?.debug ?? false;
4888
4999
  this.ipnsLifetimeMs = config?.ipnsLifetimeMs ?? 99 * 365 * 24 * 60 * 60 * 1e3;
@@ -5002,6 +5113,7 @@ var IpfsStorageProvider = class {
5002
5113
  }
5003
5114
  async shutdown() {
5004
5115
  this.isShuttingDown = true;
5116
+ logger.debug("IPFS-Storage", `shutdown: ipnsName=${this.ipnsName?.slice(0, 20)}..., pendingEmpty=${this.pendingBuffer.isEmpty}, capturedIpns=${this.pendingBuffer.capturedIpnsName?.slice(0, 20) ?? "none"}`);
5005
5117
  if (this.flushTimer) {
5006
5118
  clearTimeout(this.flushTimer);
5007
5119
  this.flushTimer = null;
@@ -5034,6 +5146,8 @@ var IpfsStorageProvider = class {
5034
5146
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5035
5147
  }
5036
5148
  this.pendingBuffer.txfData = data;
5149
+ this.pendingBuffer.capturedIpnsKeyPair = this.ipnsKeyPair;
5150
+ this.pendingBuffer.capturedIpnsName = this.ipnsName;
5037
5151
  this.scheduleFlush();
5038
5152
  return { success: true, timestamp: Date.now() };
5039
5153
  }
@@ -5044,8 +5158,12 @@ var IpfsStorageProvider = class {
5044
5158
  * Perform the actual upload + IPNS publish synchronously.
5045
5159
  * Called by executeFlush() and sync() — never by public save().
5046
5160
  */
5047
- async _doSave(data) {
5048
- if (!this.ipnsKeyPair || !this.ipnsName) {
5161
+ async _doSave(data, overrideIpns) {
5162
+ const ipnsKeyPair = overrideIpns?.keyPair ?? this.ipnsKeyPair;
5163
+ const ipnsName = overrideIpns?.name ?? this.ipnsName;
5164
+ const metaAddr = data?._meta?.address;
5165
+ logger.debug("IPFS-Storage", `_doSave: ipnsName=${ipnsName?.slice(0, 20)}..., override=${!!overrideIpns}, meta.address=${metaAddr?.slice(0, 20) ?? "none"}`);
5166
+ if (!ipnsKeyPair || !ipnsName) {
5049
5167
  return { success: false, error: "Not initialized", timestamp: Date.now() };
5050
5168
  }
5051
5169
  this.emitEvent({ type: "storage:saving", timestamp: Date.now() });
@@ -5054,7 +5172,7 @@ var IpfsStorageProvider = class {
5054
5172
  const metaUpdate = {
5055
5173
  ...data._meta,
5056
5174
  version: this.dataVersion,
5057
- ipnsName: this.ipnsName,
5175
+ ipnsName,
5058
5176
  updatedAt: Date.now()
5059
5177
  };
5060
5178
  if (this.remoteCid) {
@@ -5066,13 +5184,13 @@ var IpfsStorageProvider = class {
5066
5184
  const baseSeq = this.ipnsSequenceNumber > this.lastKnownRemoteSequence ? this.ipnsSequenceNumber : this.lastKnownRemoteSequence;
5067
5185
  const newSeq = baseSeq + 1n;
5068
5186
  const marshalledRecord = await createSignedRecord(
5069
- this.ipnsKeyPair,
5187
+ ipnsKeyPair,
5070
5188
  cid,
5071
5189
  newSeq,
5072
5190
  this.ipnsLifetimeMs
5073
5191
  );
5074
5192
  const publishResult = await this.httpClient.publishIpns(
5075
- this.ipnsName,
5193
+ ipnsName,
5076
5194
  marshalledRecord
5077
5195
  );
5078
5196
  if (!publishResult.success) {
@@ -5087,14 +5205,14 @@ var IpfsStorageProvider = class {
5087
5205
  this.ipnsSequenceNumber = newSeq;
5088
5206
  this.lastCid = cid;
5089
5207
  this.remoteCid = cid;
5090
- this.cache.setIpnsRecord(this.ipnsName, {
5208
+ this.cache.setIpnsRecord(ipnsName, {
5091
5209
  cid,
5092
5210
  sequence: newSeq,
5093
5211
  gateway: "local"
5094
5212
  });
5095
5213
  this.cache.setContent(cid, updatedData);
5096
- this.cache.markIpnsFresh(this.ipnsName);
5097
- await this.statePersistence.save(this.ipnsName, {
5214
+ this.cache.markIpnsFresh(ipnsName);
5215
+ await this.statePersistence.save(ipnsName, {
5098
5216
  sequenceNumber: newSeq.toString(),
5099
5217
  lastCid: cid,
5100
5218
  version: this.dataVersion
@@ -5146,7 +5264,8 @@ var IpfsStorageProvider = class {
5146
5264
  const baseData = active.txfData ?? {
5147
5265
  _meta: { version: 0, address: this.identity?.directAddress ?? "", formatVersion: "2.0", updatedAt: 0 }
5148
5266
  };
5149
- const result = await this._doSave(baseData);
5267
+ const overrideIpns = active.capturedIpnsKeyPair && active.capturedIpnsName ? { keyPair: active.capturedIpnsKeyPair, name: active.capturedIpnsName } : void 0;
5268
+ const result = await this._doSave(baseData, overrideIpns);
5150
5269
  if (!result.success) {
5151
5270
  throw new SphereError(result.error ?? "Save failed", "STORAGE_ERROR");
5152
5271
  }
@@ -5449,6 +5568,13 @@ var IpfsStorageProvider = class {
5449
5568
  log(message) {
5450
5569
  logger.debug("IPFS-Storage", message);
5451
5570
  }
5571
+ /**
5572
+ * Create an independent instance for a different address.
5573
+ * Shares the same gateway/timeout config but has fresh IPNS state.
5574
+ */
5575
+ createForAddress() {
5576
+ return new _IpfsStorageProvider(this._config, this._statePersistenceCtor);
5577
+ }
5452
5578
  };
5453
5579
 
5454
5580
  // impl/browser/ipfs/browser-ipfs-state-persistence.ts