nostr-websocket-utils 0.2.4 → 0.3.0
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/LICENSE +1 -1
- package/README.md +151 -103
- package/dist/__mocks__/extendedWsMock.d.ts +35 -0
- package/dist/__mocks__/extendedWsMock.js +156 -0
- package/dist/__mocks__/logger.d.ts +9 -0
- package/dist/__mocks__/logger.js +6 -0
- package/dist/__mocks__/mockLogger.d.ts +41 -0
- package/dist/__mocks__/mockLogger.js +47 -0
- package/dist/__mocks__/mockserver.d.ts +31 -0
- package/dist/__mocks__/mockserver.js +39 -0
- package/dist/__mocks__/wsMock.d.ts +26 -0
- package/dist/__mocks__/wsMock.js +120 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.js +105 -0
- package/dist/core/client.d.ts +94 -0
- package/dist/core/client.js +360 -0
- package/dist/core/nostr-server.d.ts +27 -0
- package/dist/core/nostr-server.js +95 -0
- package/dist/core/queue.d.ts +61 -0
- package/dist/core/queue.js +108 -0
- package/dist/core/server.d.ts +27 -0
- package/dist/core/server.js +114 -0
- package/dist/crypto/bech32.d.ts +26 -0
- package/dist/crypto/bech32.js +163 -0
- package/dist/crypto/handlers.d.ts +11 -0
- package/dist/crypto/handlers.js +36 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +5 -0
- package/dist/crypto/schnorr.d.ts +16 -0
- package/dist/crypto/schnorr.js +51 -0
- package/dist/endpoints/metrics.d.ts +29 -0
- package/dist/endpoints/metrics.js +101 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +16 -4
- package/dist/nips/index.d.ts +19 -0
- package/dist/nips/index.js +34 -0
- package/dist/nips/nip-01.d.ts +34 -0
- package/dist/nips/nip-01.js +145 -0
- package/dist/nips/nip-02.d.ts +83 -0
- package/dist/nips/nip-02.js +123 -0
- package/dist/nips/nip-04.d.ts +36 -0
- package/dist/nips/nip-04.js +105 -0
- package/dist/nips/nip-05.d.ts +86 -0
- package/dist/nips/nip-05.js +151 -0
- package/dist/nips/nip-09.d.ts +92 -0
- package/dist/nips/nip-09.js +190 -0
- package/dist/nips/nip-11.d.ts +64 -0
- package/dist/nips/nip-11.js +154 -0
- package/dist/nips/nip-13.d.ts +73 -0
- package/dist/nips/nip-13.js +128 -0
- package/dist/nips/nip-15.d.ts +83 -0
- package/dist/nips/nip-15.js +101 -0
- package/dist/nips/nip-16.d.ts +88 -0
- package/dist/nips/nip-16.js +150 -0
- package/dist/nips/nip-19.d.ts +28 -0
- package/dist/nips/nip-19.js +103 -0
- package/dist/nips/nip-20.d.ts +59 -0
- package/dist/nips/nip-20.js +95 -0
- package/dist/nips/nip-22.d.ts +89 -0
- package/dist/nips/nip-22.js +142 -0
- package/dist/nips/nip-26.d.ts +52 -0
- package/dist/nips/nip-26.js +139 -0
- package/dist/nips/nip-28.d.ts +103 -0
- package/dist/nips/nip-28.js +170 -0
- package/dist/nips/nip-33.d.ts +94 -0
- package/dist/nips/nip-33.js +133 -0
- package/dist/nostr-server.d.ts +23 -0
- package/dist/nostr-server.js +44 -0
- package/dist/server.d.ts +13 -3
- package/dist/server.js +60 -33
- package/dist/transport/base.d.ts +54 -0
- package/dist/transport/base.js +104 -0
- package/dist/transport/websocket.d.ts +22 -0
- package/dist/transport/websocket.js +122 -0
- package/dist/types/events.d.ts +63 -0
- package/dist/types/events.js +5 -0
- package/dist/types/filters.d.ts +19 -0
- package/dist/types/filters.js +5 -0
- package/dist/types/handlers.d.ts +80 -0
- package/dist/types/handlers.js +5 -0
- package/dist/types/index.d.ts +118 -39
- package/dist/types/index.js +21 -1
- package/dist/types/logger.d.ts +40 -0
- package/dist/types/logger.js +5 -0
- package/dist/types/messages.d.ts +135 -0
- package/dist/types/messages.js +40 -0
- package/dist/types/nostr.d.ts +120 -39
- package/dist/types/nostr.js +5 -10
- package/dist/types/options.d.ts +154 -0
- package/dist/types/options.js +5 -0
- package/dist/types/relays.d.ts +26 -0
- package/dist/types/relays.js +5 -0
- package/dist/types/scoring.d.ts +47 -0
- package/dist/types/scoring.js +29 -0
- package/dist/types/socket.d.ts +99 -0
- package/dist/types/socket.js +5 -0
- package/dist/types/transport.d.ts +97 -0
- package/dist/types/transport.js +5 -0
- package/dist/types/validation.d.ts +50 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/websocket.d.ts +172 -0
- package/dist/types/websocket.js +5 -0
- package/dist/utils/http.d.ts +10 -0
- package/dist/utils/http.js +24 -0
- package/dist/utils/logger.d.ts +11 -2
- package/dist/utils/logger.js +18 -13
- package/dist/utils/metrics.d.ts +81 -0
- package/dist/utils/metrics.js +206 -0
- package/dist/utils/rate-limiter.d.ts +85 -0
- package/dist/utils/rate-limiter.js +175 -0
- package/package.json +18 -21
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocket client implementation
|
|
3
|
+
* @module core/client
|
|
4
|
+
*/
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import { ConnectionState, MessagePriority } from '../types';
|
|
9
|
+
import { MessageQueue } from './queue';
|
|
10
|
+
import { getLogger } from '../utils/logger';
|
|
11
|
+
const logger = getLogger('client');
|
|
12
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
13
|
+
maxAttempts: 10,
|
|
14
|
+
initialDelay: 1000,
|
|
15
|
+
maxDelay: 30000,
|
|
16
|
+
backoffFactor: 1.5
|
|
17
|
+
};
|
|
18
|
+
const DEFAULT_QUEUE_CONFIG = {
|
|
19
|
+
maxSize: 1000,
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
retryDelay: 1000,
|
|
22
|
+
staleTimeout: 300000 // 5 minutes
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_HEARTBEAT_CONFIG = {
|
|
25
|
+
interval: 30000,
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
maxMissed: 3
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* WebSocket client implementation for Nostr protocol communication
|
|
31
|
+
* Extends EventEmitter to provide event-based message handling
|
|
32
|
+
*/
|
|
33
|
+
export class NostrWSClient extends EventEmitter {
|
|
34
|
+
constructor(url, options = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this.url = url;
|
|
37
|
+
this.ws = null;
|
|
38
|
+
this.state = ConnectionState.DISCONNECTED;
|
|
39
|
+
this.reconnectTimeout = null;
|
|
40
|
+
this.heartbeatInterval = null;
|
|
41
|
+
this.heartbeatTimeout = null;
|
|
42
|
+
this.missedHeartbeats = 0;
|
|
43
|
+
this.reconnectAttempts = 0;
|
|
44
|
+
this.subscriptions = new Map();
|
|
45
|
+
this.clientId = uuidv4();
|
|
46
|
+
// Initialize options with defaults
|
|
47
|
+
this.options = {
|
|
48
|
+
WebSocketImpl: options.WebSocketImpl || WebSocket,
|
|
49
|
+
handlers: {
|
|
50
|
+
message: options.handlers?.message || (async () => { }),
|
|
51
|
+
error: options.handlers?.error || (() => { }),
|
|
52
|
+
close: options.handlers?.close || (() => { }),
|
|
53
|
+
stateChange: options.handlers?.stateChange,
|
|
54
|
+
heartbeat: options.handlers?.heartbeat,
|
|
55
|
+
connect: options.handlers?.connect || (() => { }),
|
|
56
|
+
disconnect: options.handlers?.disconnect || (() => { }),
|
|
57
|
+
reconnect: options.handlers?.reconnect || (() => { })
|
|
58
|
+
},
|
|
59
|
+
retry: { ...DEFAULT_RETRY_CONFIG, ...options.retry },
|
|
60
|
+
queue: { ...DEFAULT_QUEUE_CONFIG, ...options.queue },
|
|
61
|
+
heartbeat: { ...DEFAULT_HEARTBEAT_CONFIG, ...options.heartbeat },
|
|
62
|
+
autoReconnect: options.autoReconnect !== false,
|
|
63
|
+
bufferMessages: options.bufferMessages !== false,
|
|
64
|
+
cleanStaleMessages: options.cleanStaleMessages !== false,
|
|
65
|
+
logger: options.logger || logger
|
|
66
|
+
};
|
|
67
|
+
// Initialize message queue
|
|
68
|
+
this.messageQueue = new MessageQueue(this.options.queue);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Gets the current connection state
|
|
72
|
+
*/
|
|
73
|
+
get connectionState() {
|
|
74
|
+
return this.state;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Updates the connection state and notifies handlers
|
|
78
|
+
*/
|
|
79
|
+
setState(newState) {
|
|
80
|
+
this.state = newState;
|
|
81
|
+
logger.debug({ state: newState }, 'Connection state changed');
|
|
82
|
+
this.options.handlers.stateChange?.(newState);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Establishes a connection to the WebSocket server
|
|
86
|
+
*/
|
|
87
|
+
async connect() {
|
|
88
|
+
if (this.ws) {
|
|
89
|
+
logger.warn('Connection already exists');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
this.setState(ConnectionState.CONNECTING);
|
|
94
|
+
this.ws = new this.options.WebSocketImpl(this.url);
|
|
95
|
+
this.setupEventHandlers();
|
|
96
|
+
// Wait for connection to establish
|
|
97
|
+
await new Promise((resolve, reject) => {
|
|
98
|
+
const onOpen = () => {
|
|
99
|
+
this.ws?.removeEventListener('open', onOpen);
|
|
100
|
+
this.ws?.removeEventListener('error', onError);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
const onError = (error) => {
|
|
104
|
+
this.ws?.removeEventListener('open', onOpen);
|
|
105
|
+
this.ws?.removeEventListener('error', onError);
|
|
106
|
+
reject(new Error(error?.message || 'Failed to connect'));
|
|
107
|
+
};
|
|
108
|
+
this.ws?.addEventListener('open', onOpen);
|
|
109
|
+
this.ws?.addEventListener('error', onError);
|
|
110
|
+
});
|
|
111
|
+
this.setState(ConnectionState.CONNECTED);
|
|
112
|
+
this.reconnectAttempts = 0;
|
|
113
|
+
this.startHeartbeat();
|
|
114
|
+
this.flushMessageQueue();
|
|
115
|
+
this.resubscribeAll();
|
|
116
|
+
this.options.handlers.connect?.();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger.error({ error }, 'Failed to establish connection');
|
|
120
|
+
this.handleConnectionError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sets up event handlers for the WebSocket connection
|
|
125
|
+
*/
|
|
126
|
+
setupEventHandlers() {
|
|
127
|
+
if (!this.ws)
|
|
128
|
+
return;
|
|
129
|
+
this.ws.addEventListener('message', async (event) => {
|
|
130
|
+
try {
|
|
131
|
+
const message = JSON.parse(event.data.toString());
|
|
132
|
+
// Handle heartbeat responses
|
|
133
|
+
if (message.type === 'PONG') {
|
|
134
|
+
this.handleHeartbeatResponse();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await this.options.handlers.message(message);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error({ error }, 'Error handling message');
|
|
141
|
+
this.options.handlers.error(error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.ws.addEventListener('error', (error) => {
|
|
145
|
+
const wsError = error?.error || new Error('WebSocket error');
|
|
146
|
+
logger.error({ error: wsError }, 'WebSocket error');
|
|
147
|
+
this.options.handlers.error(wsError);
|
|
148
|
+
});
|
|
149
|
+
this.ws.addEventListener('close', () => {
|
|
150
|
+
this.handleDisconnection();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts the heartbeat mechanism
|
|
155
|
+
*/
|
|
156
|
+
startHeartbeat() {
|
|
157
|
+
this.stopHeartbeat();
|
|
158
|
+
this.heartbeatInterval = setInterval(() => {
|
|
159
|
+
if (this.state !== ConnectionState.CONNECTED)
|
|
160
|
+
return;
|
|
161
|
+
this.send({
|
|
162
|
+
type: 'PING',
|
|
163
|
+
priority: MessagePriority.LOW
|
|
164
|
+
});
|
|
165
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
166
|
+
this.missedHeartbeats++;
|
|
167
|
+
logger.warn({ missed: this.missedHeartbeats }, 'Missed heartbeat');
|
|
168
|
+
if (this.missedHeartbeats >= (this.options.heartbeat?.maxMissed || DEFAULT_HEARTBEAT_CONFIG.maxMissed)) {
|
|
169
|
+
logger.error('Too many missed heartbeats, reconnecting');
|
|
170
|
+
this.reconnect();
|
|
171
|
+
}
|
|
172
|
+
}, this.options.heartbeat?.timeout || DEFAULT_HEARTBEAT_CONFIG.timeout);
|
|
173
|
+
}, this.options.heartbeat?.interval || DEFAULT_HEARTBEAT_CONFIG.interval);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Handles heartbeat responses
|
|
177
|
+
*/
|
|
178
|
+
handleHeartbeatResponse() {
|
|
179
|
+
if (this.heartbeatTimeout) {
|
|
180
|
+
clearTimeout(this.heartbeatTimeout);
|
|
181
|
+
this.heartbeatTimeout = null;
|
|
182
|
+
}
|
|
183
|
+
this.missedHeartbeats = 0;
|
|
184
|
+
this.options.handlers.heartbeat?.();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Stops the heartbeat mechanism
|
|
188
|
+
*/
|
|
189
|
+
stopHeartbeat() {
|
|
190
|
+
if (this.heartbeatInterval) {
|
|
191
|
+
clearInterval(this.heartbeatInterval);
|
|
192
|
+
this.heartbeatInterval = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.heartbeatTimeout) {
|
|
195
|
+
clearTimeout(this.heartbeatTimeout);
|
|
196
|
+
this.heartbeatTimeout = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handles connection errors
|
|
201
|
+
*/
|
|
202
|
+
handleConnectionError(error) {
|
|
203
|
+
logger.error({ error }, 'Connection error');
|
|
204
|
+
this.options.handlers.error(error);
|
|
205
|
+
this.handleDisconnection();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Handles disconnection and cleanup
|
|
209
|
+
*/
|
|
210
|
+
handleDisconnection() {
|
|
211
|
+
this.stopHeartbeat();
|
|
212
|
+
this.setState(ConnectionState.DISCONNECTED);
|
|
213
|
+
this.ws = null;
|
|
214
|
+
this.options.handlers.disconnect?.();
|
|
215
|
+
this.options.handlers.close();
|
|
216
|
+
if (this.options.autoReconnect) {
|
|
217
|
+
this.reconnect();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Initiates reconnection with exponential backoff
|
|
222
|
+
*/
|
|
223
|
+
reconnect() {
|
|
224
|
+
if (this.reconnectTimeout || this.state === ConnectionState.CONNECTING) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.reconnectAttempts++;
|
|
228
|
+
const maxAttempts = this.options.retry?.maxAttempts || DEFAULT_RETRY_CONFIG.maxAttempts;
|
|
229
|
+
if (this.reconnectAttempts > maxAttempts) {
|
|
230
|
+
this.setState(ConnectionState.FAILED);
|
|
231
|
+
logger.error('Max reconnection attempts exceeded');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const initialDelay = this.options.retry?.initialDelay || DEFAULT_RETRY_CONFIG.initialDelay;
|
|
235
|
+
const backoffFactor = this.options.retry?.backoffFactor || DEFAULT_RETRY_CONFIG.backoffFactor;
|
|
236
|
+
const maxDelay = this.options.retry?.maxDelay || DEFAULT_RETRY_CONFIG.maxDelay;
|
|
237
|
+
const delay = Math.min(initialDelay * Math.pow(backoffFactor, this.reconnectAttempts - 1), maxDelay);
|
|
238
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
239
|
+
logger.info({ attempt: this.reconnectAttempts, delay }, 'Scheduling reconnection');
|
|
240
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
241
|
+
this.reconnectTimeout = null;
|
|
242
|
+
try {
|
|
243
|
+
await this.connect();
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
logger.error({ error }, 'Reconnection failed');
|
|
247
|
+
}
|
|
248
|
+
}, delay);
|
|
249
|
+
this.options.handlers.reconnect?.();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Subscribes to a channel with optional filter
|
|
253
|
+
*/
|
|
254
|
+
subscribe(channel, filter) {
|
|
255
|
+
const subscription = {
|
|
256
|
+
type: 'REQ',
|
|
257
|
+
data: { channel, filter },
|
|
258
|
+
priority: MessagePriority.HIGH
|
|
259
|
+
};
|
|
260
|
+
this.subscriptions.set(channel, subscription);
|
|
261
|
+
this.send(subscription);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resubscribes to all active subscriptions
|
|
265
|
+
*/
|
|
266
|
+
resubscribeAll() {
|
|
267
|
+
for (const subscription of this.subscriptions.values()) {
|
|
268
|
+
this.send(subscription);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Unsubscribes from a channel
|
|
273
|
+
*/
|
|
274
|
+
unsubscribe(channel) {
|
|
275
|
+
const subscription = this.subscriptions.get(channel);
|
|
276
|
+
if (subscription) {
|
|
277
|
+
this.send({
|
|
278
|
+
type: 'CLOSE',
|
|
279
|
+
data: { channel },
|
|
280
|
+
priority: MessagePriority.HIGH
|
|
281
|
+
});
|
|
282
|
+
this.subscriptions.delete(channel);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Flushes the message queue by sending pending messages
|
|
287
|
+
*/
|
|
288
|
+
async flushMessageQueue() {
|
|
289
|
+
if (this.state !== ConnectionState.CONNECTED)
|
|
290
|
+
return;
|
|
291
|
+
while (this.messageQueue.size > 0) {
|
|
292
|
+
const message = this.messageQueue.dequeue();
|
|
293
|
+
if (!message)
|
|
294
|
+
break;
|
|
295
|
+
try {
|
|
296
|
+
await this.sendImmediate(message);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
await this.messageQueue.retry(message);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Sends a message immediately without queueing
|
|
305
|
+
*/
|
|
306
|
+
async sendImmediate(message) {
|
|
307
|
+
if (!this.ws || this.state !== ConnectionState.CONNECTED) {
|
|
308
|
+
throw new Error('Not connected');
|
|
309
|
+
}
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
this.ws.send(JSON.stringify(message), (error) => {
|
|
312
|
+
if (error) {
|
|
313
|
+
logger.error({ error, message }, 'Failed to send message');
|
|
314
|
+
reject(error);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
resolve();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Sends a message to the WebSocket server
|
|
324
|
+
*/
|
|
325
|
+
async send(message) {
|
|
326
|
+
if (this.state === ConnectionState.CONNECTED) {
|
|
327
|
+
try {
|
|
328
|
+
await this.sendImmediate(message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (!this.options.bufferMessages) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (this.options.bufferMessages) {
|
|
338
|
+
this.messageQueue.enqueue(message);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
throw new Error('Not connected and message buffering is disabled');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Closes the WebSocket connection
|
|
346
|
+
*/
|
|
347
|
+
close() {
|
|
348
|
+
this.options.autoReconnect = false;
|
|
349
|
+
if (this.reconnectTimeout) {
|
|
350
|
+
clearTimeout(this.reconnectTimeout);
|
|
351
|
+
this.reconnectTimeout = null;
|
|
352
|
+
}
|
|
353
|
+
if (this.ws) {
|
|
354
|
+
this.ws.close();
|
|
355
|
+
}
|
|
356
|
+
this.handleDisconnection();
|
|
357
|
+
this.messageQueue.clear();
|
|
358
|
+
this.subscriptions.clear();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NostrWSServerOptions } from '../types/websocket';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a Nostr WebSocket server
|
|
4
|
+
*/
|
|
5
|
+
export declare class NostrWSServer {
|
|
6
|
+
/**
|
|
7
|
+
* The underlying WebSocket server instance
|
|
8
|
+
*/
|
|
9
|
+
private server;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new Nostr WebSocket server instance
|
|
12
|
+
*
|
|
13
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
14
|
+
*/
|
|
15
|
+
constructor(options: NostrWSServerOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Closes the WebSocket server
|
|
18
|
+
*/
|
|
19
|
+
stop(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new Nostr WebSocket server instance
|
|
23
|
+
*
|
|
24
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
25
|
+
* @returns {NostrWSServer} The created server instance
|
|
26
|
+
*/
|
|
27
|
+
export declare function createWSServer(options: NostrWSServerOptions): NostrWSServer;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { getLogger } from '../utils/logger';
|
|
4
|
+
const logger = getLogger('NostrWSServer');
|
|
5
|
+
/**
|
|
6
|
+
* Represents a Nostr WebSocket server
|
|
7
|
+
*/
|
|
8
|
+
export class NostrWSServer {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new Nostr WebSocket server instance
|
|
11
|
+
*
|
|
12
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
13
|
+
*/
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.server = new WebSocketServer({
|
|
16
|
+
port: options.port,
|
|
17
|
+
host: options.host
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Handles incoming WebSocket connections
|
|
21
|
+
*
|
|
22
|
+
* @param {WebSocket} ws - The connected WebSocket client
|
|
23
|
+
*/
|
|
24
|
+
this.server.on('connection', async (ws) => {
|
|
25
|
+
const socket = ws;
|
|
26
|
+
socket.clientId = uuidv4();
|
|
27
|
+
socket.subscriptions = new Set();
|
|
28
|
+
socket.isAlive = true;
|
|
29
|
+
logger.info(`Client connected: ${socket.clientId}`);
|
|
30
|
+
/**
|
|
31
|
+
* Calls the onConnection handler if provided
|
|
32
|
+
*/
|
|
33
|
+
await options.onConnection?.(socket);
|
|
34
|
+
/**
|
|
35
|
+
* Handles incoming messages from the client
|
|
36
|
+
*
|
|
37
|
+
* @param {Buffer} data - The incoming message data
|
|
38
|
+
*/
|
|
39
|
+
socket.on('message', async (data) => {
|
|
40
|
+
try {
|
|
41
|
+
const message = JSON.parse(data.toString());
|
|
42
|
+
logger.info('Received message:', message);
|
|
43
|
+
/**
|
|
44
|
+
* Calls the onMessage handler if provided
|
|
45
|
+
*/
|
|
46
|
+
await options.onMessage?.(message, socket);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.error('Error processing message:', error);
|
|
50
|
+
/**
|
|
51
|
+
* Calls the onError handler if provided
|
|
52
|
+
*/
|
|
53
|
+
options.onError?.(error, socket);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Handles WebSocket errors
|
|
58
|
+
*
|
|
59
|
+
* @param {Error} error - The error that occurred
|
|
60
|
+
*/
|
|
61
|
+
socket.on('error', (error) => {
|
|
62
|
+
logger.error(`Client error (${socket.clientId}):`, error);
|
|
63
|
+
/**
|
|
64
|
+
* Calls the onError handler if provided
|
|
65
|
+
*/
|
|
66
|
+
options.onError?.(error, socket);
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Handles client disconnections
|
|
70
|
+
*/
|
|
71
|
+
socket.on('close', () => {
|
|
72
|
+
logger.info(`Client disconnected: ${socket.clientId}`);
|
|
73
|
+
/**
|
|
74
|
+
* Calls the onClose handler if provided
|
|
75
|
+
*/
|
|
76
|
+
options.onClose?.(socket);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Closes the WebSocket server
|
|
82
|
+
*/
|
|
83
|
+
stop() {
|
|
84
|
+
this.server.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Creates a new Nostr WebSocket server instance
|
|
89
|
+
*
|
|
90
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
91
|
+
* @returns {NostrWSServer} The created server instance
|
|
92
|
+
*/
|
|
93
|
+
export function createWSServer(options) {
|
|
94
|
+
return new NostrWSServer(options);
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Message queue implementation for WebSocket communication
|
|
3
|
+
* @module core/queue
|
|
4
|
+
*/
|
|
5
|
+
import { NostrWSMessage } from '../types/messages';
|
|
6
|
+
/**
|
|
7
|
+
* Options for message queue configuration
|
|
8
|
+
*/
|
|
9
|
+
interface QueueOptions {
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
maxRetries?: number;
|
|
12
|
+
retryDelay?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Message queue implementation with priority handling and retry logic
|
|
16
|
+
*/
|
|
17
|
+
export declare class MessageQueue {
|
|
18
|
+
private queue;
|
|
19
|
+
private maxSize;
|
|
20
|
+
private maxRetries;
|
|
21
|
+
private retryDelay;
|
|
22
|
+
constructor(options?: QueueOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Adds a message to the queue with priority handling
|
|
25
|
+
* @param message Message to enqueue
|
|
26
|
+
* @returns true if message was added, false if queue is full
|
|
27
|
+
*/
|
|
28
|
+
enqueue(message: NostrWSMessage): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Gets the next message from the queue
|
|
31
|
+
* @returns Next message or undefined if queue is empty
|
|
32
|
+
*/
|
|
33
|
+
dequeue(): NostrWSMessage | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Handles message retry logic
|
|
36
|
+
* @param message Message that failed to send
|
|
37
|
+
* @returns true if message was requeued, false if max retries exceeded
|
|
38
|
+
*/
|
|
39
|
+
retry(message: NostrWSMessage): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Gets the current size of the queue
|
|
42
|
+
*/
|
|
43
|
+
get size(): number;
|
|
44
|
+
/**
|
|
45
|
+
* Clears all messages from the queue
|
|
46
|
+
*/
|
|
47
|
+
clear(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Gets messages that have been in the queue longer than the specified duration
|
|
50
|
+
* @param duration Duration in milliseconds
|
|
51
|
+
* @returns Array of stale messages
|
|
52
|
+
*/
|
|
53
|
+
getStaleMessages(duration: number): NostrWSMessage[];
|
|
54
|
+
/**
|
|
55
|
+
* Removes messages that have been in the queue longer than the specified duration
|
|
56
|
+
* @param duration Duration in milliseconds
|
|
57
|
+
* @returns Number of messages removed
|
|
58
|
+
*/
|
|
59
|
+
removeStaleMessages(duration: number): number;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Message queue implementation for WebSocket communication
|
|
3
|
+
* @module core/queue
|
|
4
|
+
*/
|
|
5
|
+
import { MessagePriority } from '../types/messages';
|
|
6
|
+
import { getLogger } from '../utils/logger';
|
|
7
|
+
const logger = getLogger('queue');
|
|
8
|
+
/**
|
|
9
|
+
* Message queue implementation with priority handling and retry logic
|
|
10
|
+
*/
|
|
11
|
+
export class MessageQueue {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.queue = [];
|
|
14
|
+
this.maxSize = options.maxSize || 1000;
|
|
15
|
+
this.maxRetries = options.maxRetries || 3;
|
|
16
|
+
this.retryDelay = options.retryDelay || 1000;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Adds a message to the queue with priority handling
|
|
20
|
+
* @param message Message to enqueue
|
|
21
|
+
* @returns true if message was added, false if queue is full
|
|
22
|
+
*/
|
|
23
|
+
enqueue(message) {
|
|
24
|
+
if (this.queue.length >= this.maxSize) {
|
|
25
|
+
logger.warn({ message }, 'Queue is full, message dropped');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const queuedMessage = {
|
|
29
|
+
...message,
|
|
30
|
+
priority: message.priority || MessagePriority.MEDIUM,
|
|
31
|
+
queuedAt: Date.now(),
|
|
32
|
+
retryCount: 0
|
|
33
|
+
};
|
|
34
|
+
// Insert message in priority order
|
|
35
|
+
const insertIndex = this.queue.findIndex(m => (m.priority || MessagePriority.MEDIUM) > queuedMessage.priority);
|
|
36
|
+
if (insertIndex === -1) {
|
|
37
|
+
this.queue.push(queuedMessage);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.queue.splice(insertIndex, 0, queuedMessage);
|
|
41
|
+
}
|
|
42
|
+
logger.debug({ message: queuedMessage }, 'Message enqueued');
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets the next message from the queue
|
|
47
|
+
* @returns Next message or undefined if queue is empty
|
|
48
|
+
*/
|
|
49
|
+
dequeue() {
|
|
50
|
+
return this.queue.shift();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Handles message retry logic
|
|
54
|
+
* @param message Message that failed to send
|
|
55
|
+
* @returns true if message was requeued, false if max retries exceeded
|
|
56
|
+
*/
|
|
57
|
+
async retry(message) {
|
|
58
|
+
const retryCount = (message.retryCount || 0) + 1;
|
|
59
|
+
if (retryCount > this.maxRetries) {
|
|
60
|
+
logger.warn({ message }, 'Max retries exceeded, message dropped');
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
// Wait before retrying
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, this.retryDelay * retryCount));
|
|
65
|
+
return this.enqueue({
|
|
66
|
+
...message,
|
|
67
|
+
retryCount,
|
|
68
|
+
queuedAt: Date.now()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Gets the current size of the queue
|
|
73
|
+
*/
|
|
74
|
+
get size() {
|
|
75
|
+
return this.queue.length;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Clears all messages from the queue
|
|
79
|
+
*/
|
|
80
|
+
clear() {
|
|
81
|
+
this.queue = [];
|
|
82
|
+
logger.info('Queue cleared');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets messages that have been in the queue longer than the specified duration
|
|
86
|
+
* @param duration Duration in milliseconds
|
|
87
|
+
* @returns Array of stale messages
|
|
88
|
+
*/
|
|
89
|
+
getStaleMessages(duration) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
return this.queue.filter(message => message.queuedAt && (now - message.queuedAt) > duration);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Removes messages that have been in the queue longer than the specified duration
|
|
95
|
+
* @param duration Duration in milliseconds
|
|
96
|
+
* @returns Number of messages removed
|
|
97
|
+
*/
|
|
98
|
+
removeStaleMessages(duration) {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const initialSize = this.queue.length;
|
|
101
|
+
this.queue = this.queue.filter(message => message.queuedAt && (now - message.queuedAt) <= duration);
|
|
102
|
+
const removedCount = initialSize - this.queue.length;
|
|
103
|
+
if (removedCount > 0) {
|
|
104
|
+
logger.info({ removedCount }, 'Stale messages removed from queue');
|
|
105
|
+
}
|
|
106
|
+
return removedCount;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocket server implementation
|
|
3
|
+
* @module core/server
|
|
4
|
+
*/
|
|
5
|
+
import { NostrWSServerOptions } from '../types/websocket';
|
|
6
|
+
/**
|
|
7
|
+
* NostrWSServer class for handling WebSocket connections
|
|
8
|
+
*/
|
|
9
|
+
export declare class NostrWSServer {
|
|
10
|
+
private wss;
|
|
11
|
+
private options;
|
|
12
|
+
private rateLimiter?;
|
|
13
|
+
private pingInterval?;
|
|
14
|
+
constructor(options: NostrWSServerOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Set up WebSocket server event handlers
|
|
17
|
+
*/
|
|
18
|
+
private setupServer;
|
|
19
|
+
/**
|
|
20
|
+
* Start ping interval to check client connections
|
|
21
|
+
*/
|
|
22
|
+
private startPingInterval;
|
|
23
|
+
/**
|
|
24
|
+
* Stop the server and clean up resources
|
|
25
|
+
*/
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|