@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 +23 -1
- package/dist/browser/index.js +210 -22
- 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 +210 -22
- 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 +210 -22
- package/dist/cjs/client/NostrClient.js.map +1 -1
- package/dist/cjs/client/index.js.map +1 -1
- package/dist/esm/client/NostrClient.js +210 -22
- package/dist/esm/client/NostrClient.js.map +1 -1
- package/dist/esm/client/index.js.map +1 -1
- package/dist/types/client/NostrClient.d.ts +70 -2
- package/dist/types/client/NostrClient.d.ts.map +1 -1
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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
|
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
|
|
5993
|
-
|
|
5994
|
-
const
|
|
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
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
},
|
|
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
|
-
},
|
|
6633
|
+
}, this.queryTimeoutMs);
|
|
6446
6634
|
let result = null;
|
|
6447
6635
|
let latestCreatedAt = 0;
|
|
6448
6636
|
const subscriptionId = this.subscribe(filter, {
|