@unicitylabs/nostr-js-sdk 0.2.3 → 0.2.5
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 +16 -4
- package/dist/browser/index.js +192 -19
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.min.js +7 -7
- package/dist/browser/index.min.js.map +1 -1
- package/dist/browser/index.umd.js +192 -19
- package/dist/browser/index.umd.js.map +1 -1
- package/dist/browser/index.umd.min.js +7 -7
- package/dist/browser/index.umd.min.js.map +1 -1
- package/dist/cjs/client/NostrClient.js +192 -19
- package/dist/cjs/client/NostrClient.js.map +1 -1
- package/dist/esm/client/NostrClient.js +192 -19
- package/dist/esm/client/NostrClient.js.map +1 -1
- package/dist/types/client/NostrClient.d.ts +50 -1
- package/dist/types/client/NostrClient.d.ts.map +1 -1
- package/dist/types/client/index.d.ts +1 -1
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,16 +44,28 @@ 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
50
|
|
|
51
|
-
// Create client with default options
|
|
51
|
+
// Create client with default options (auto-reconnect enabled)
|
|
52
52
|
const client = new NostrClient(keyManager);
|
|
53
53
|
|
|
54
54
|
// Or configure with custom options
|
|
55
55
|
const client = new NostrClient(keyManager, {
|
|
56
|
-
queryTimeoutMs: 15000,
|
|
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}`),
|
|
57
69
|
});
|
|
58
70
|
|
|
59
71
|
// Connect to relays
|
|
@@ -66,7 +78,7 @@ await client.connect(
|
|
|
66
78
|
console.log(client.isConnected());
|
|
67
79
|
console.log(client.getConnectedRelays());
|
|
68
80
|
|
|
69
|
-
//
|
|
81
|
+
// Adjust timeout dynamically
|
|
70
82
|
client.setQueryTimeout(30000); // 30 seconds
|
|
71
83
|
```
|
|
72
84
|
|
package/dist/browser/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
5992
|
-
const RECONNECT_DELAY_MS = 5000;
|
|
5993
|
-
/** Default query timeout in milliseconds */
|
|
5991
|
+
/** Default options */
|
|
5994
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,7 +6004,14 @@ class NostrClient {
|
|
|
6003
6004
|
pendingOks = new Map();
|
|
6004
6005
|
subscriptionCounter = 0;
|
|
6005
6006
|
closed = false;
|
|
6007
|
+
// Configuration options
|
|
6006
6008
|
queryTimeoutMs;
|
|
6009
|
+
autoReconnect;
|
|
6010
|
+
reconnectIntervalMs;
|
|
6011
|
+
maxReconnectIntervalMs;
|
|
6012
|
+
pingIntervalMs;
|
|
6013
|
+
// Connection event listeners
|
|
6014
|
+
connectionListeners = [];
|
|
6007
6015
|
/**
|
|
6008
6016
|
* Create a NostrClient instance.
|
|
6009
6017
|
* @param keyManager Key manager with signing keys
|
|
@@ -6012,6 +6020,53 @@ class NostrClient {
|
|
|
6012
6020
|
constructor(keyManager, options) {
|
|
6013
6021
|
this.keyManager = keyManager;
|
|
6014
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
|
+
}
|
|
6015
6070
|
}
|
|
6016
6071
|
/**
|
|
6017
6072
|
* Get the key manager.
|
|
@@ -6048,13 +6103,12 @@ class NostrClient {
|
|
|
6048
6103
|
}
|
|
6049
6104
|
/**
|
|
6050
6105
|
* Connect to a single relay.
|
|
6106
|
+
* @param isReconnect Whether this is a reconnection attempt
|
|
6051
6107
|
*/
|
|
6052
|
-
async connectToRelay(url) {
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
return;
|
|
6057
|
-
}
|
|
6108
|
+
async connectToRelay(url, isReconnect = false) {
|
|
6109
|
+
const existingRelay = this.relays.get(url);
|
|
6110
|
+
if (existingRelay?.connected) {
|
|
6111
|
+
return;
|
|
6058
6112
|
}
|
|
6059
6113
|
return new Promise((resolve, reject) => {
|
|
6060
6114
|
const timeoutId = setTimeout(() => {
|
|
@@ -6067,11 +6121,28 @@ class NostrClient {
|
|
|
6067
6121
|
socket,
|
|
6068
6122
|
connected: false,
|
|
6069
6123
|
reconnecting: false,
|
|
6124
|
+
reconnectAttempts: 0,
|
|
6125
|
+
reconnectTimer: null,
|
|
6126
|
+
pingTimer: null,
|
|
6127
|
+
lastPongTime: Date.now(),
|
|
6128
|
+
wasConnected: existingRelay?.wasConnected ?? false,
|
|
6070
6129
|
};
|
|
6071
6130
|
socket.onopen = () => {
|
|
6072
6131
|
clearTimeout(timeoutId);
|
|
6073
6132
|
relay.connected = true;
|
|
6133
|
+
relay.reconnectAttempts = 0; // Reset on successful connection
|
|
6134
|
+
relay.lastPongTime = Date.now();
|
|
6074
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);
|
|
6075
6146
|
// Re-establish subscriptions
|
|
6076
6147
|
this.resubscribeAll(url);
|
|
6077
6148
|
// Flush queued events
|
|
@@ -6081,15 +6152,26 @@ class NostrClient {
|
|
|
6081
6152
|
socket.onmessage = (event) => {
|
|
6082
6153
|
try {
|
|
6083
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
|
+
}
|
|
6084
6160
|
this.handleRelayMessage(url, data);
|
|
6085
6161
|
}
|
|
6086
6162
|
catch (error) {
|
|
6087
6163
|
console.error(`Error handling message from ${url}:`, error);
|
|
6088
6164
|
}
|
|
6089
6165
|
};
|
|
6090
|
-
socket.onclose = () => {
|
|
6166
|
+
socket.onclose = (event) => {
|
|
6167
|
+
const wasConnected = relay.connected;
|
|
6091
6168
|
relay.connected = false;
|
|
6092
|
-
|
|
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) {
|
|
6093
6175
|
this.scheduleReconnect(url);
|
|
6094
6176
|
}
|
|
6095
6177
|
};
|
|
@@ -6108,24 +6190,103 @@ class NostrClient {
|
|
|
6108
6190
|
});
|
|
6109
6191
|
}
|
|
6110
6192
|
/**
|
|
6111
|
-
* Schedule a reconnection attempt for a relay.
|
|
6193
|
+
* Schedule a reconnection attempt for a relay with exponential backoff.
|
|
6112
6194
|
*/
|
|
6113
6195
|
scheduleReconnect(url) {
|
|
6114
6196
|
const relay = this.relays.get(url);
|
|
6115
|
-
if (!relay || this.closed)
|
|
6197
|
+
if (!relay || this.closed || !this.autoReconnect)
|
|
6116
6198
|
return;
|
|
6199
|
+
// Clear any existing reconnect timer
|
|
6200
|
+
if (relay.reconnectTimer) {
|
|
6201
|
+
clearTimeout(relay.reconnectTimer);
|
|
6202
|
+
}
|
|
6117
6203
|
relay.reconnecting = true;
|
|
6118
|
-
|
|
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 () => {
|
|
6119
6211
|
if (this.closed)
|
|
6120
6212
|
return;
|
|
6213
|
+
relay.reconnectTimer = null;
|
|
6121
6214
|
try {
|
|
6122
6215
|
relay.reconnecting = false;
|
|
6123
|
-
await this.connectToRelay(url);
|
|
6216
|
+
await this.connectToRelay(url, true);
|
|
6217
|
+
}
|
|
6218
|
+
catch {
|
|
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
|
+
// Use a single fixed subscription ID per relay to avoid accumulating subscriptions
|
|
6258
|
+
// Note: limit:1 is used because some relays don't respond to limit:0
|
|
6259
|
+
try {
|
|
6260
|
+
const pingSubId = `ping`;
|
|
6261
|
+
// First close any existing ping subscription to ensure we don't accumulate
|
|
6262
|
+
const closeMessage = JSON.stringify(['CLOSE', pingSubId]);
|
|
6263
|
+
relay.socket.send(closeMessage);
|
|
6264
|
+
// Then send the new ping request (limit:1 ensures relay sends EOSE)
|
|
6265
|
+
const pingMessage = JSON.stringify(['REQ', pingSubId, { limit: 1 }]);
|
|
6266
|
+
relay.socket.send(pingMessage);
|
|
6124
6267
|
}
|
|
6125
6268
|
catch {
|
|
6126
|
-
//
|
|
6269
|
+
// Send failed, connection likely dead
|
|
6270
|
+
console.warn(`Ping to ${url} failed, reconnecting...`);
|
|
6271
|
+
this.stopPingTimer(url);
|
|
6272
|
+
try {
|
|
6273
|
+
relay.socket.close();
|
|
6274
|
+
}
|
|
6275
|
+
catch {
|
|
6276
|
+
// Ignore close errors
|
|
6277
|
+
}
|
|
6127
6278
|
}
|
|
6128
|
-
},
|
|
6279
|
+
}, this.pingIntervalMs);
|
|
6280
|
+
}
|
|
6281
|
+
/**
|
|
6282
|
+
* Stop the ping timer for a relay.
|
|
6283
|
+
*/
|
|
6284
|
+
stopPingTimer(url) {
|
|
6285
|
+
const relay = this.relays.get(url);
|
|
6286
|
+
if (relay?.pingTimer) {
|
|
6287
|
+
clearInterval(relay.pingTimer);
|
|
6288
|
+
relay.pingTimer = null;
|
|
6289
|
+
}
|
|
6129
6290
|
}
|
|
6130
6291
|
/**
|
|
6131
6292
|
* Re-establish all subscriptions for a relay.
|
|
@@ -6272,11 +6433,23 @@ class NostrClient {
|
|
|
6272
6433
|
item.reject(new Error('Client disconnected'));
|
|
6273
6434
|
}
|
|
6274
6435
|
this.eventQueue = [];
|
|
6275
|
-
// Close all relay connections
|
|
6276
|
-
for (const [, relay] of this.relays) {
|
|
6436
|
+
// Close all relay connections and clean up timers
|
|
6437
|
+
for (const [url, relay] of this.relays) {
|
|
6438
|
+
// Stop ping timer
|
|
6439
|
+
if (relay.pingTimer) {
|
|
6440
|
+
clearInterval(relay.pingTimer);
|
|
6441
|
+
relay.pingTimer = null;
|
|
6442
|
+
}
|
|
6443
|
+
// Stop reconnect timer
|
|
6444
|
+
if (relay.reconnectTimer) {
|
|
6445
|
+
clearTimeout(relay.reconnectTimer);
|
|
6446
|
+
relay.reconnectTimer = null;
|
|
6447
|
+
}
|
|
6448
|
+
// Close socket
|
|
6277
6449
|
if (relay.socket && relay.socket.readyState !== CLOSED) {
|
|
6278
6450
|
relay.socket.close(1000, 'Client disconnected');
|
|
6279
6451
|
}
|
|
6452
|
+
this.emitConnectionEvent('disconnect', url, 'Client disconnected');
|
|
6280
6453
|
}
|
|
6281
6454
|
this.relays.clear();
|
|
6282
6455
|
this.subscriptions.clear();
|