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.
Files changed (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +151 -103
  3. package/dist/__mocks__/extendedWsMock.d.ts +35 -0
  4. package/dist/__mocks__/extendedWsMock.js +156 -0
  5. package/dist/__mocks__/logger.d.ts +9 -0
  6. package/dist/__mocks__/logger.js +6 -0
  7. package/dist/__mocks__/mockLogger.d.ts +41 -0
  8. package/dist/__mocks__/mockLogger.js +47 -0
  9. package/dist/__mocks__/mockserver.d.ts +31 -0
  10. package/dist/__mocks__/mockserver.js +39 -0
  11. package/dist/__mocks__/wsMock.d.ts +26 -0
  12. package/dist/__mocks__/wsMock.js +120 -0
  13. package/dist/client.d.ts +105 -0
  14. package/dist/client.js +105 -0
  15. package/dist/core/client.d.ts +94 -0
  16. package/dist/core/client.js +360 -0
  17. package/dist/core/nostr-server.d.ts +27 -0
  18. package/dist/core/nostr-server.js +95 -0
  19. package/dist/core/queue.d.ts +61 -0
  20. package/dist/core/queue.js +108 -0
  21. package/dist/core/server.d.ts +27 -0
  22. package/dist/core/server.js +114 -0
  23. package/dist/crypto/bech32.d.ts +26 -0
  24. package/dist/crypto/bech32.js +163 -0
  25. package/dist/crypto/handlers.d.ts +11 -0
  26. package/dist/crypto/handlers.js +36 -0
  27. package/dist/crypto/index.d.ts +5 -0
  28. package/dist/crypto/index.js +5 -0
  29. package/dist/crypto/schnorr.d.ts +16 -0
  30. package/dist/crypto/schnorr.js +51 -0
  31. package/dist/endpoints/metrics.d.ts +29 -0
  32. package/dist/endpoints/metrics.js +101 -0
  33. package/dist/index.d.ts +11 -6
  34. package/dist/index.js +16 -4
  35. package/dist/nips/index.d.ts +19 -0
  36. package/dist/nips/index.js +34 -0
  37. package/dist/nips/nip-01.d.ts +34 -0
  38. package/dist/nips/nip-01.js +145 -0
  39. package/dist/nips/nip-02.d.ts +83 -0
  40. package/dist/nips/nip-02.js +123 -0
  41. package/dist/nips/nip-04.d.ts +36 -0
  42. package/dist/nips/nip-04.js +105 -0
  43. package/dist/nips/nip-05.d.ts +86 -0
  44. package/dist/nips/nip-05.js +151 -0
  45. package/dist/nips/nip-09.d.ts +92 -0
  46. package/dist/nips/nip-09.js +190 -0
  47. package/dist/nips/nip-11.d.ts +64 -0
  48. package/dist/nips/nip-11.js +154 -0
  49. package/dist/nips/nip-13.d.ts +73 -0
  50. package/dist/nips/nip-13.js +128 -0
  51. package/dist/nips/nip-15.d.ts +83 -0
  52. package/dist/nips/nip-15.js +101 -0
  53. package/dist/nips/nip-16.d.ts +88 -0
  54. package/dist/nips/nip-16.js +150 -0
  55. package/dist/nips/nip-19.d.ts +28 -0
  56. package/dist/nips/nip-19.js +103 -0
  57. package/dist/nips/nip-20.d.ts +59 -0
  58. package/dist/nips/nip-20.js +95 -0
  59. package/dist/nips/nip-22.d.ts +89 -0
  60. package/dist/nips/nip-22.js +142 -0
  61. package/dist/nips/nip-26.d.ts +52 -0
  62. package/dist/nips/nip-26.js +139 -0
  63. package/dist/nips/nip-28.d.ts +103 -0
  64. package/dist/nips/nip-28.js +170 -0
  65. package/dist/nips/nip-33.d.ts +94 -0
  66. package/dist/nips/nip-33.js +133 -0
  67. package/dist/nostr-server.d.ts +23 -0
  68. package/dist/nostr-server.js +44 -0
  69. package/dist/server.d.ts +13 -3
  70. package/dist/server.js +60 -33
  71. package/dist/transport/base.d.ts +54 -0
  72. package/dist/transport/base.js +104 -0
  73. package/dist/transport/websocket.d.ts +22 -0
  74. package/dist/transport/websocket.js +122 -0
  75. package/dist/types/events.d.ts +63 -0
  76. package/dist/types/events.js +5 -0
  77. package/dist/types/filters.d.ts +19 -0
  78. package/dist/types/filters.js +5 -0
  79. package/dist/types/handlers.d.ts +80 -0
  80. package/dist/types/handlers.js +5 -0
  81. package/dist/types/index.d.ts +118 -39
  82. package/dist/types/index.js +21 -1
  83. package/dist/types/logger.d.ts +40 -0
  84. package/dist/types/logger.js +5 -0
  85. package/dist/types/messages.d.ts +135 -0
  86. package/dist/types/messages.js +40 -0
  87. package/dist/types/nostr.d.ts +120 -39
  88. package/dist/types/nostr.js +5 -10
  89. package/dist/types/options.d.ts +154 -0
  90. package/dist/types/options.js +5 -0
  91. package/dist/types/relays.d.ts +26 -0
  92. package/dist/types/relays.js +5 -0
  93. package/dist/types/scoring.d.ts +47 -0
  94. package/dist/types/scoring.js +29 -0
  95. package/dist/types/socket.d.ts +99 -0
  96. package/dist/types/socket.js +5 -0
  97. package/dist/types/transport.d.ts +97 -0
  98. package/dist/types/transport.js +5 -0
  99. package/dist/types/validation.d.ts +50 -0
  100. package/dist/types/validation.js +5 -0
  101. package/dist/types/websocket.d.ts +172 -0
  102. package/dist/types/websocket.js +5 -0
  103. package/dist/utils/http.d.ts +10 -0
  104. package/dist/utils/http.js +24 -0
  105. package/dist/utils/logger.d.ts +11 -2
  106. package/dist/utils/logger.js +18 -13
  107. package/dist/utils/metrics.d.ts +81 -0
  108. package/dist/utils/metrics.js +206 -0
  109. package/dist/utils/rate-limiter.d.ts +85 -0
  110. package/dist/utils/rate-limiter.js +175 -0
  111. 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
+ }