@unicitylabs/nostr-js-sdk 0.2.2 → 0.2.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.
package/README.md CHANGED
@@ -44,11 +44,30 @@ console.log(keyManager.getPublicKeyHex());
44
44
  ### Connecting to Relays
45
45
 
46
46
  ```typescript
47
- import { NostrClient, NostrKeyManager } from '@unicitylabs/nostr-sdk';
47
+ import { NostrClient, NostrKeyManager, ConnectionEventListener } from '@unicitylabs/nostr-sdk';
48
48
 
49
49
  const keyManager = NostrKeyManager.generate();
50
+
51
+ // Create client with default options (auto-reconnect enabled)
50
52
  const client = new NostrClient(keyManager);
51
53
 
54
+ // Or configure with custom options
55
+ const client = new NostrClient(keyManager, {
56
+ queryTimeoutMs: 15000, // Query timeout (default: 5000ms)
57
+ autoReconnect: true, // Auto-reconnect on connection loss (default: true)
58
+ reconnectIntervalMs: 1000, // Initial reconnect delay (default: 1000ms)
59
+ maxReconnectIntervalMs: 30000, // Max backoff interval (default: 30000ms)
60
+ pingIntervalMs: 30000, // Health check interval (default: 30000ms, 0 to disable)
61
+ });
62
+
63
+ // Monitor connection events
64
+ client.addConnectionListener({
65
+ onConnect: (url) => console.log(`Connected to ${url}`),
66
+ onDisconnect: (url, reason) => console.log(`Disconnected from ${url}: ${reason}`),
67
+ onReconnecting: (url, attempt) => console.log(`Reconnecting to ${url} (attempt ${attempt})...`),
68
+ onReconnected: (url) => console.log(`Reconnected to ${url}`),
69
+ });
70
+
52
71
  // Connect to relays
53
72
  await client.connect(
54
73
  'wss://relay.damus.io',
@@ -58,6 +77,9 @@ await client.connect(
58
77
  // Check connection status
59
78
  console.log(client.isConnected());
60
79
  console.log(client.getConnectedRelays());
80
+
81
+ // Adjust timeout dynamically
82
+ client.setQueryTimeout(30000); // 30 seconds
61
83
  ```
62
84
 
63
85
  ### Publishing Events
@@ -5988,10 +5988,11 @@ var nip17 = /*#__PURE__*/Object.freeze({
5988
5988
  */
5989
5989
  /** Connection timeout in milliseconds */
5990
5990
  const CONNECTION_TIMEOUT_MS = 30000;
5991
- /** Reconnection delay in milliseconds */
5992
- const RECONNECT_DELAY_MS = 5000;
5993
- /** Query timeout in milliseconds */
5994
- const QUERY_TIMEOUT_MS = 5000;
5991
+ /** Default options */
5992
+ const DEFAULT_QUERY_TIMEOUT_MS = 5000;
5993
+ const DEFAULT_RECONNECT_INTERVAL_MS = 1000;
5994
+ const DEFAULT_MAX_RECONNECT_INTERVAL_MS = 30000;
5995
+ const DEFAULT_PING_INTERVAL_MS = 30000;
5995
5996
  /**
5996
5997
  * NostrClient provides the main interface for Nostr protocol operations.
5997
5998
  */
@@ -6003,12 +6004,69 @@ class NostrClient {
6003
6004
  pendingOks = new Map();
6004
6005
  subscriptionCounter = 0;
6005
6006
  closed = false;
6007
+ // Configuration options
6008
+ queryTimeoutMs;
6009
+ autoReconnect;
6010
+ reconnectIntervalMs;
6011
+ maxReconnectIntervalMs;
6012
+ pingIntervalMs;
6013
+ // Connection event listeners
6014
+ connectionListeners = [];
6006
6015
  /**
6007
6016
  * Create a NostrClient instance.
6008
6017
  * @param keyManager Key manager with signing keys
6018
+ * @param options Optional configuration options
6009
6019
  */
6010
- constructor(keyManager) {
6020
+ constructor(keyManager, options) {
6011
6021
  this.keyManager = keyManager;
6022
+ this.queryTimeoutMs = options?.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
6023
+ this.autoReconnect = options?.autoReconnect ?? true;
6024
+ this.reconnectIntervalMs = options?.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS;
6025
+ this.maxReconnectIntervalMs = options?.maxReconnectIntervalMs ?? DEFAULT_MAX_RECONNECT_INTERVAL_MS;
6026
+ this.pingIntervalMs = options?.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
6027
+ }
6028
+ /**
6029
+ * Add a connection event listener.
6030
+ * @param listener Listener for connection events
6031
+ */
6032
+ addConnectionListener(listener) {
6033
+ this.connectionListeners.push(listener);
6034
+ }
6035
+ /**
6036
+ * Remove a connection event listener.
6037
+ * @param listener Listener to remove
6038
+ */
6039
+ removeConnectionListener(listener) {
6040
+ const index = this.connectionListeners.indexOf(listener);
6041
+ if (index !== -1) {
6042
+ this.connectionListeners.splice(index, 1);
6043
+ }
6044
+ }
6045
+ /**
6046
+ * Emit a connection event to all listeners.
6047
+ */
6048
+ emitConnectionEvent(eventType, relayUrl, extra) {
6049
+ for (const listener of this.connectionListeners) {
6050
+ try {
6051
+ switch (eventType) {
6052
+ case 'connect':
6053
+ listener.onConnect?.(relayUrl);
6054
+ break;
6055
+ case 'disconnect':
6056
+ listener.onDisconnect?.(relayUrl, extra);
6057
+ break;
6058
+ case 'reconnecting':
6059
+ listener.onReconnecting?.(relayUrl, extra);
6060
+ break;
6061
+ case 'reconnected':
6062
+ listener.onReconnected?.(relayUrl);
6063
+ break;
6064
+ }
6065
+ }
6066
+ catch {
6067
+ // Ignore listener errors
6068
+ }
6069
+ }
6012
6070
  }
6013
6071
  /**
6014
6072
  * Get the key manager.
@@ -6017,6 +6075,20 @@ class NostrClient {
6017
6075
  getKeyManager() {
6018
6076
  return this.keyManager;
6019
6077
  }
6078
+ /**
6079
+ * Get the current query timeout in milliseconds.
6080
+ * @returns Query timeout in milliseconds
6081
+ */
6082
+ getQueryTimeout() {
6083
+ return this.queryTimeoutMs;
6084
+ }
6085
+ /**
6086
+ * Set the query timeout for nametag lookups and other queries.
6087
+ * @param timeoutMs Timeout in milliseconds
6088
+ */
6089
+ setQueryTimeout(timeoutMs) {
6090
+ this.queryTimeoutMs = timeoutMs;
6091
+ }
6020
6092
  /**
6021
6093
  * Connect to one or more relay WebSocket URLs.
6022
6094
  * @param relayUrls Relay URLs to connect to
@@ -6031,13 +6103,12 @@ class NostrClient {
6031
6103
  }
6032
6104
  /**
6033
6105
  * Connect to a single relay.
6106
+ * @param isReconnect Whether this is a reconnection attempt
6034
6107
  */
6035
- async connectToRelay(url) {
6036
- if (this.relays.has(url)) {
6037
- const relay = this.relays.get(url);
6038
- if (relay.connected) {
6039
- return;
6040
- }
6108
+ async connectToRelay(url, isReconnect = false) {
6109
+ const existingRelay = this.relays.get(url);
6110
+ if (existingRelay?.connected) {
6111
+ return;
6041
6112
  }
6042
6113
  return new Promise((resolve, reject) => {
6043
6114
  const timeoutId = setTimeout(() => {
@@ -6050,11 +6121,28 @@ class NostrClient {
6050
6121
  socket,
6051
6122
  connected: false,
6052
6123
  reconnecting: false,
6124
+ reconnectAttempts: 0,
6125
+ reconnectTimer: null,
6126
+ pingTimer: null,
6127
+ lastPongTime: Date.now(),
6128
+ wasConnected: existingRelay?.wasConnected ?? false,
6053
6129
  };
6054
6130
  socket.onopen = () => {
6055
6131
  clearTimeout(timeoutId);
6056
6132
  relay.connected = true;
6133
+ relay.reconnectAttempts = 0; // Reset on successful connection
6134
+ relay.lastPongTime = Date.now();
6057
6135
  this.relays.set(url, relay);
6136
+ // Emit appropriate connection event
6137
+ if (isReconnect && relay.wasConnected) {
6138
+ this.emitConnectionEvent('reconnected', url);
6139
+ }
6140
+ else {
6141
+ this.emitConnectionEvent('connect', url);
6142
+ }
6143
+ relay.wasConnected = true;
6144
+ // Start ping health check
6145
+ this.startPingTimer(url);
6058
6146
  // Re-establish subscriptions
6059
6147
  this.resubscribeAll(url);
6060
6148
  // Flush queued events
@@ -6064,15 +6152,26 @@ class NostrClient {
6064
6152
  socket.onmessage = (event) => {
6065
6153
  try {
6066
6154
  const data = extractMessageData(event);
6155
+ // Update last pong time on any message (relay is alive)
6156
+ const r = this.relays.get(url);
6157
+ if (r) {
6158
+ r.lastPongTime = Date.now();
6159
+ }
6067
6160
  this.handleRelayMessage(url, data);
6068
6161
  }
6069
6162
  catch (error) {
6070
6163
  console.error(`Error handling message from ${url}:`, error);
6071
6164
  }
6072
6165
  };
6073
- socket.onclose = () => {
6166
+ socket.onclose = (event) => {
6167
+ const wasConnected = relay.connected;
6074
6168
  relay.connected = false;
6075
- if (!this.closed && !relay.reconnecting) {
6169
+ this.stopPingTimer(url);
6170
+ if (wasConnected) {
6171
+ const reason = event?.reason || 'Connection closed';
6172
+ this.emitConnectionEvent('disconnect', url, reason);
6173
+ }
6174
+ if (!this.closed && this.autoReconnect && !relay.reconnecting) {
6076
6175
  this.scheduleReconnect(url);
6077
6176
  }
6078
6177
  };
@@ -6091,24 +6190,101 @@ class NostrClient {
6091
6190
  });
6092
6191
  }
6093
6192
  /**
6094
- * Schedule a reconnection attempt for a relay.
6193
+ * Schedule a reconnection attempt for a relay with exponential backoff.
6095
6194
  */
6096
6195
  scheduleReconnect(url) {
6097
6196
  const relay = this.relays.get(url);
6098
- if (!relay || this.closed)
6197
+ if (!relay || this.closed || !this.autoReconnect)
6099
6198
  return;
6199
+ // Clear any existing reconnect timer
6200
+ if (relay.reconnectTimer) {
6201
+ clearTimeout(relay.reconnectTimer);
6202
+ }
6100
6203
  relay.reconnecting = true;
6101
- setTimeout(async () => {
6204
+ relay.reconnectAttempts++;
6205
+ // Calculate delay with exponential backoff
6206
+ const baseDelay = this.reconnectIntervalMs;
6207
+ const exponentialDelay = baseDelay * Math.pow(2, relay.reconnectAttempts - 1);
6208
+ const delay = Math.min(exponentialDelay, this.maxReconnectIntervalMs);
6209
+ this.emitConnectionEvent('reconnecting', url, relay.reconnectAttempts);
6210
+ relay.reconnectTimer = setTimeout(async () => {
6102
6211
  if (this.closed)
6103
6212
  return;
6213
+ relay.reconnectTimer = null;
6104
6214
  try {
6105
6215
  relay.reconnecting = false;
6106
- await this.connectToRelay(url);
6216
+ await this.connectToRelay(url, true);
6107
6217
  }
6108
6218
  catch {
6109
- // Will trigger another reconnect via onclose
6219
+ // Connection failed, schedule another attempt
6220
+ if (!this.closed && this.autoReconnect) {
6221
+ this.scheduleReconnect(url);
6222
+ }
6223
+ }
6224
+ }, delay);
6225
+ }
6226
+ /**
6227
+ * Start the ping timer for a relay to detect stale connections.
6228
+ */
6229
+ startPingTimer(url) {
6230
+ if (this.pingIntervalMs <= 0)
6231
+ return;
6232
+ const relay = this.relays.get(url);
6233
+ if (!relay)
6234
+ return;
6235
+ // Stop existing timer if any
6236
+ this.stopPingTimer(url);
6237
+ relay.pingTimer = setInterval(() => {
6238
+ if (!relay.connected || !relay.socket) {
6239
+ this.stopPingTimer(url);
6240
+ return;
6241
+ }
6242
+ // Check if we've received any message recently
6243
+ const timeSinceLastPong = Date.now() - relay.lastPongTime;
6244
+ if (timeSinceLastPong > this.pingIntervalMs * 2) {
6245
+ // Connection is stale - force close and reconnect
6246
+ console.warn(`Relay ${url} appears stale (no response for ${timeSinceLastPong}ms), reconnecting...`);
6247
+ this.stopPingTimer(url);
6248
+ try {
6249
+ relay.socket.close();
6250
+ }
6251
+ catch {
6252
+ // Ignore close errors
6253
+ }
6254
+ return;
6255
+ }
6256
+ // Send a subscription request as a ping (relays respond with EOSE)
6257
+ // Using a unique subscription ID that we immediately close
6258
+ try {
6259
+ const pingSubId = `ping-${Date.now()}`;
6260
+ const pingMessage = JSON.stringify(['REQ', pingSubId, { limit: 0 }]);
6261
+ relay.socket.send(pingMessage);
6262
+ // Immediately close the subscription
6263
+ const closeMessage = JSON.stringify(['CLOSE', pingSubId]);
6264
+ relay.socket.send(closeMessage);
6265
+ }
6266
+ catch {
6267
+ // Send failed, connection likely dead
6268
+ console.warn(`Ping to ${url} failed, reconnecting...`);
6269
+ this.stopPingTimer(url);
6270
+ try {
6271
+ relay.socket.close();
6272
+ }
6273
+ catch {
6274
+ // Ignore close errors
6275
+ }
6110
6276
  }
6111
- }, RECONNECT_DELAY_MS);
6277
+ }, this.pingIntervalMs);
6278
+ }
6279
+ /**
6280
+ * Stop the ping timer for a relay.
6281
+ */
6282
+ stopPingTimer(url) {
6283
+ const relay = this.relays.get(url);
6284
+ if (relay?.pingTimer) {
6285
+ clearInterval(relay.pingTimer);
6286
+ relay.pingTimer = null;
6287
+ }
6112
6288
  }
6113
6289
  /**
6114
6290
  * Re-establish all subscriptions for a relay.
@@ -6255,11 +6431,23 @@ class NostrClient {
6255
6431
  item.reject(new Error('Client disconnected'));
6256
6432
  }
6257
6433
  this.eventQueue = [];
6258
- // Close all relay connections
6259
- for (const [, relay] of this.relays) {
6434
+ // Close all relay connections and clean up timers
6435
+ for (const [url, relay] of this.relays) {
6436
+ // Stop ping timer
6437
+ if (relay.pingTimer) {
6438
+ clearInterval(relay.pingTimer);
6439
+ relay.pingTimer = null;
6440
+ }
6441
+ // Stop reconnect timer
6442
+ if (relay.reconnectTimer) {
6443
+ clearTimeout(relay.reconnectTimer);
6444
+ relay.reconnectTimer = null;
6445
+ }
6446
+ // Close socket
6260
6447
  if (relay.socket && relay.socket.readyState !== CLOSED) {
6261
6448
  relay.socket.close(1000, 'Client disconnected');
6262
6449
  }
6450
+ this.emitConnectionEvent('disconnect', url, 'Client disconnected');
6263
6451
  }
6264
6452
  this.relays.clear();
6265
6453
  this.subscriptions.clear();
@@ -6442,7 +6630,7 @@ class NostrClient {
6442
6630
  const timeoutId = setTimeout(() => {
6443
6631
  this.unsubscribe(subscriptionId);
6444
6632
  resolve(null);
6445
- }, QUERY_TIMEOUT_MS);
6633
+ }, this.queryTimeoutMs);
6446
6634
  let result = null;
6447
6635
  let latestCreatedAt = 0;
6448
6636
  const subscriptionId = this.subscribe(filter, {