@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
|
@@ -5994,10 +5994,11 @@
|
|
|
5994
5994
|
*/
|
|
5995
5995
|
/** Connection timeout in milliseconds */
|
|
5996
5996
|
const CONNECTION_TIMEOUT_MS = 30000;
|
|
5997
|
-
/**
|
|
5998
|
-
const RECONNECT_DELAY_MS = 5000;
|
|
5999
|
-
/** Default query timeout in milliseconds */
|
|
5997
|
+
/** Default options */
|
|
6000
5998
|
const DEFAULT_QUERY_TIMEOUT_MS = 5000;
|
|
5999
|
+
const DEFAULT_RECONNECT_INTERVAL_MS = 1000;
|
|
6000
|
+
const DEFAULT_MAX_RECONNECT_INTERVAL_MS = 30000;
|
|
6001
|
+
const DEFAULT_PING_INTERVAL_MS = 30000;
|
|
6001
6002
|
/**
|
|
6002
6003
|
* NostrClient provides the main interface for Nostr protocol operations.
|
|
6003
6004
|
*/
|
|
@@ -6009,7 +6010,14 @@
|
|
|
6009
6010
|
pendingOks = new Map();
|
|
6010
6011
|
subscriptionCounter = 0;
|
|
6011
6012
|
closed = false;
|
|
6013
|
+
// Configuration options
|
|
6012
6014
|
queryTimeoutMs;
|
|
6015
|
+
autoReconnect;
|
|
6016
|
+
reconnectIntervalMs;
|
|
6017
|
+
maxReconnectIntervalMs;
|
|
6018
|
+
pingIntervalMs;
|
|
6019
|
+
// Connection event listeners
|
|
6020
|
+
connectionListeners = [];
|
|
6013
6021
|
/**
|
|
6014
6022
|
* Create a NostrClient instance.
|
|
6015
6023
|
* @param keyManager Key manager with signing keys
|
|
@@ -6018,6 +6026,53 @@
|
|
|
6018
6026
|
constructor(keyManager, options) {
|
|
6019
6027
|
this.keyManager = keyManager;
|
|
6020
6028
|
this.queryTimeoutMs = options?.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
|
|
6029
|
+
this.autoReconnect = options?.autoReconnect ?? true;
|
|
6030
|
+
this.reconnectIntervalMs = options?.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS;
|
|
6031
|
+
this.maxReconnectIntervalMs = options?.maxReconnectIntervalMs ?? DEFAULT_MAX_RECONNECT_INTERVAL_MS;
|
|
6032
|
+
this.pingIntervalMs = options?.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
6033
|
+
}
|
|
6034
|
+
/**
|
|
6035
|
+
* Add a connection event listener.
|
|
6036
|
+
* @param listener Listener for connection events
|
|
6037
|
+
*/
|
|
6038
|
+
addConnectionListener(listener) {
|
|
6039
|
+
this.connectionListeners.push(listener);
|
|
6040
|
+
}
|
|
6041
|
+
/**
|
|
6042
|
+
* Remove a connection event listener.
|
|
6043
|
+
* @param listener Listener to remove
|
|
6044
|
+
*/
|
|
6045
|
+
removeConnectionListener(listener) {
|
|
6046
|
+
const index = this.connectionListeners.indexOf(listener);
|
|
6047
|
+
if (index !== -1) {
|
|
6048
|
+
this.connectionListeners.splice(index, 1);
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
/**
|
|
6052
|
+
* Emit a connection event to all listeners.
|
|
6053
|
+
*/
|
|
6054
|
+
emitConnectionEvent(eventType, relayUrl, extra) {
|
|
6055
|
+
for (const listener of this.connectionListeners) {
|
|
6056
|
+
try {
|
|
6057
|
+
switch (eventType) {
|
|
6058
|
+
case 'connect':
|
|
6059
|
+
listener.onConnect?.(relayUrl);
|
|
6060
|
+
break;
|
|
6061
|
+
case 'disconnect':
|
|
6062
|
+
listener.onDisconnect?.(relayUrl, extra);
|
|
6063
|
+
break;
|
|
6064
|
+
case 'reconnecting':
|
|
6065
|
+
listener.onReconnecting?.(relayUrl, extra);
|
|
6066
|
+
break;
|
|
6067
|
+
case 'reconnected':
|
|
6068
|
+
listener.onReconnected?.(relayUrl);
|
|
6069
|
+
break;
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
catch {
|
|
6073
|
+
// Ignore listener errors
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6021
6076
|
}
|
|
6022
6077
|
/**
|
|
6023
6078
|
* Get the key manager.
|
|
@@ -6054,13 +6109,12 @@
|
|
|
6054
6109
|
}
|
|
6055
6110
|
/**
|
|
6056
6111
|
* Connect to a single relay.
|
|
6112
|
+
* @param isReconnect Whether this is a reconnection attempt
|
|
6057
6113
|
*/
|
|
6058
|
-
async connectToRelay(url) {
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
return;
|
|
6063
|
-
}
|
|
6114
|
+
async connectToRelay(url, isReconnect = false) {
|
|
6115
|
+
const existingRelay = this.relays.get(url);
|
|
6116
|
+
if (existingRelay?.connected) {
|
|
6117
|
+
return;
|
|
6064
6118
|
}
|
|
6065
6119
|
return new Promise((resolve, reject) => {
|
|
6066
6120
|
const timeoutId = setTimeout(() => {
|
|
@@ -6073,11 +6127,28 @@
|
|
|
6073
6127
|
socket,
|
|
6074
6128
|
connected: false,
|
|
6075
6129
|
reconnecting: false,
|
|
6130
|
+
reconnectAttempts: 0,
|
|
6131
|
+
reconnectTimer: null,
|
|
6132
|
+
pingTimer: null,
|
|
6133
|
+
lastPongTime: Date.now(),
|
|
6134
|
+
wasConnected: existingRelay?.wasConnected ?? false,
|
|
6076
6135
|
};
|
|
6077
6136
|
socket.onopen = () => {
|
|
6078
6137
|
clearTimeout(timeoutId);
|
|
6079
6138
|
relay.connected = true;
|
|
6139
|
+
relay.reconnectAttempts = 0; // Reset on successful connection
|
|
6140
|
+
relay.lastPongTime = Date.now();
|
|
6080
6141
|
this.relays.set(url, relay);
|
|
6142
|
+
// Emit appropriate connection event
|
|
6143
|
+
if (isReconnect && relay.wasConnected) {
|
|
6144
|
+
this.emitConnectionEvent('reconnected', url);
|
|
6145
|
+
}
|
|
6146
|
+
else {
|
|
6147
|
+
this.emitConnectionEvent('connect', url);
|
|
6148
|
+
}
|
|
6149
|
+
relay.wasConnected = true;
|
|
6150
|
+
// Start ping health check
|
|
6151
|
+
this.startPingTimer(url);
|
|
6081
6152
|
// Re-establish subscriptions
|
|
6082
6153
|
this.resubscribeAll(url);
|
|
6083
6154
|
// Flush queued events
|
|
@@ -6087,15 +6158,26 @@
|
|
|
6087
6158
|
socket.onmessage = (event) => {
|
|
6088
6159
|
try {
|
|
6089
6160
|
const data = extractMessageData(event);
|
|
6161
|
+
// Update last pong time on any message (relay is alive)
|
|
6162
|
+
const r = this.relays.get(url);
|
|
6163
|
+
if (r) {
|
|
6164
|
+
r.lastPongTime = Date.now();
|
|
6165
|
+
}
|
|
6090
6166
|
this.handleRelayMessage(url, data);
|
|
6091
6167
|
}
|
|
6092
6168
|
catch (error) {
|
|
6093
6169
|
console.error(`Error handling message from ${url}:`, error);
|
|
6094
6170
|
}
|
|
6095
6171
|
};
|
|
6096
|
-
socket.onclose = () => {
|
|
6172
|
+
socket.onclose = (event) => {
|
|
6173
|
+
const wasConnected = relay.connected;
|
|
6097
6174
|
relay.connected = false;
|
|
6098
|
-
|
|
6175
|
+
this.stopPingTimer(url);
|
|
6176
|
+
if (wasConnected) {
|
|
6177
|
+
const reason = event?.reason || 'Connection closed';
|
|
6178
|
+
this.emitConnectionEvent('disconnect', url, reason);
|
|
6179
|
+
}
|
|
6180
|
+
if (!this.closed && this.autoReconnect && !relay.reconnecting) {
|
|
6099
6181
|
this.scheduleReconnect(url);
|
|
6100
6182
|
}
|
|
6101
6183
|
};
|
|
@@ -6114,24 +6196,103 @@
|
|
|
6114
6196
|
});
|
|
6115
6197
|
}
|
|
6116
6198
|
/**
|
|
6117
|
-
* Schedule a reconnection attempt for a relay.
|
|
6199
|
+
* Schedule a reconnection attempt for a relay with exponential backoff.
|
|
6118
6200
|
*/
|
|
6119
6201
|
scheduleReconnect(url) {
|
|
6120
6202
|
const relay = this.relays.get(url);
|
|
6121
|
-
if (!relay || this.closed)
|
|
6203
|
+
if (!relay || this.closed || !this.autoReconnect)
|
|
6122
6204
|
return;
|
|
6205
|
+
// Clear any existing reconnect timer
|
|
6206
|
+
if (relay.reconnectTimer) {
|
|
6207
|
+
clearTimeout(relay.reconnectTimer);
|
|
6208
|
+
}
|
|
6123
6209
|
relay.reconnecting = true;
|
|
6124
|
-
|
|
6210
|
+
relay.reconnectAttempts++;
|
|
6211
|
+
// Calculate delay with exponential backoff
|
|
6212
|
+
const baseDelay = this.reconnectIntervalMs;
|
|
6213
|
+
const exponentialDelay = baseDelay * Math.pow(2, relay.reconnectAttempts - 1);
|
|
6214
|
+
const delay = Math.min(exponentialDelay, this.maxReconnectIntervalMs);
|
|
6215
|
+
this.emitConnectionEvent('reconnecting', url, relay.reconnectAttempts);
|
|
6216
|
+
relay.reconnectTimer = setTimeout(async () => {
|
|
6125
6217
|
if (this.closed)
|
|
6126
6218
|
return;
|
|
6219
|
+
relay.reconnectTimer = null;
|
|
6127
6220
|
try {
|
|
6128
6221
|
relay.reconnecting = false;
|
|
6129
|
-
await this.connectToRelay(url);
|
|
6222
|
+
await this.connectToRelay(url, true);
|
|
6223
|
+
}
|
|
6224
|
+
catch {
|
|
6225
|
+
// Connection failed, schedule another attempt
|
|
6226
|
+
if (!this.closed && this.autoReconnect) {
|
|
6227
|
+
this.scheduleReconnect(url);
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
}, delay);
|
|
6231
|
+
}
|
|
6232
|
+
/**
|
|
6233
|
+
* Start the ping timer for a relay to detect stale connections.
|
|
6234
|
+
*/
|
|
6235
|
+
startPingTimer(url) {
|
|
6236
|
+
if (this.pingIntervalMs <= 0)
|
|
6237
|
+
return;
|
|
6238
|
+
const relay = this.relays.get(url);
|
|
6239
|
+
if (!relay)
|
|
6240
|
+
return;
|
|
6241
|
+
// Stop existing timer if any
|
|
6242
|
+
this.stopPingTimer(url);
|
|
6243
|
+
relay.pingTimer = setInterval(() => {
|
|
6244
|
+
if (!relay.connected || !relay.socket) {
|
|
6245
|
+
this.stopPingTimer(url);
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
// Check if we've received any message recently
|
|
6249
|
+
const timeSinceLastPong = Date.now() - relay.lastPongTime;
|
|
6250
|
+
if (timeSinceLastPong > this.pingIntervalMs * 2) {
|
|
6251
|
+
// Connection is stale - force close and reconnect
|
|
6252
|
+
console.warn(`Relay ${url} appears stale (no response for ${timeSinceLastPong}ms), reconnecting...`);
|
|
6253
|
+
this.stopPingTimer(url);
|
|
6254
|
+
try {
|
|
6255
|
+
relay.socket.close();
|
|
6256
|
+
}
|
|
6257
|
+
catch {
|
|
6258
|
+
// Ignore close errors
|
|
6259
|
+
}
|
|
6260
|
+
return;
|
|
6261
|
+
}
|
|
6262
|
+
// Send a subscription request as a ping (relays respond with EOSE)
|
|
6263
|
+
// Use a single fixed subscription ID per relay to avoid accumulating subscriptions
|
|
6264
|
+
// Note: limit:1 is used because some relays don't respond to limit:0
|
|
6265
|
+
try {
|
|
6266
|
+
const pingSubId = `ping`;
|
|
6267
|
+
// First close any existing ping subscription to ensure we don't accumulate
|
|
6268
|
+
const closeMessage = JSON.stringify(['CLOSE', pingSubId]);
|
|
6269
|
+
relay.socket.send(closeMessage);
|
|
6270
|
+
// Then send the new ping request (limit:1 ensures relay sends EOSE)
|
|
6271
|
+
const pingMessage = JSON.stringify(['REQ', pingSubId, { limit: 1 }]);
|
|
6272
|
+
relay.socket.send(pingMessage);
|
|
6130
6273
|
}
|
|
6131
6274
|
catch {
|
|
6132
|
-
//
|
|
6275
|
+
// Send failed, connection likely dead
|
|
6276
|
+
console.warn(`Ping to ${url} failed, reconnecting...`);
|
|
6277
|
+
this.stopPingTimer(url);
|
|
6278
|
+
try {
|
|
6279
|
+
relay.socket.close();
|
|
6280
|
+
}
|
|
6281
|
+
catch {
|
|
6282
|
+
// Ignore close errors
|
|
6283
|
+
}
|
|
6133
6284
|
}
|
|
6134
|
-
},
|
|
6285
|
+
}, this.pingIntervalMs);
|
|
6286
|
+
}
|
|
6287
|
+
/**
|
|
6288
|
+
* Stop the ping timer for a relay.
|
|
6289
|
+
*/
|
|
6290
|
+
stopPingTimer(url) {
|
|
6291
|
+
const relay = this.relays.get(url);
|
|
6292
|
+
if (relay?.pingTimer) {
|
|
6293
|
+
clearInterval(relay.pingTimer);
|
|
6294
|
+
relay.pingTimer = null;
|
|
6295
|
+
}
|
|
6135
6296
|
}
|
|
6136
6297
|
/**
|
|
6137
6298
|
* Re-establish all subscriptions for a relay.
|
|
@@ -6278,11 +6439,23 @@
|
|
|
6278
6439
|
item.reject(new Error('Client disconnected'));
|
|
6279
6440
|
}
|
|
6280
6441
|
this.eventQueue = [];
|
|
6281
|
-
// Close all relay connections
|
|
6282
|
-
for (const [, relay] of this.relays) {
|
|
6442
|
+
// Close all relay connections and clean up timers
|
|
6443
|
+
for (const [url, relay] of this.relays) {
|
|
6444
|
+
// Stop ping timer
|
|
6445
|
+
if (relay.pingTimer) {
|
|
6446
|
+
clearInterval(relay.pingTimer);
|
|
6447
|
+
relay.pingTimer = null;
|
|
6448
|
+
}
|
|
6449
|
+
// Stop reconnect timer
|
|
6450
|
+
if (relay.reconnectTimer) {
|
|
6451
|
+
clearTimeout(relay.reconnectTimer);
|
|
6452
|
+
relay.reconnectTimer = null;
|
|
6453
|
+
}
|
|
6454
|
+
// Close socket
|
|
6283
6455
|
if (relay.socket && relay.socket.readyState !== CLOSED) {
|
|
6284
6456
|
relay.socket.close(1000, 'Client disconnected');
|
|
6285
6457
|
}
|
|
6458
|
+
this.emitConnectionEvent('disconnect', url, 'Client disconnected');
|
|
6286
6459
|
}
|
|
6287
6460
|
this.relays.clear();
|
|
6288
6461
|
this.subscriptions.clear();
|