nostr-websocket-utils 0.2.4 โ†’ 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 CHANGED
@@ -1,25 +1,23 @@
1
- # Nostr Websocket Utils
1
+ # nostr-websocket-utils
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/nostr-websocket-utils.svg)](https://www.npmjs.com/package/nostr-websocket-utils)
4
- [![License](https://img.shields.io/npm/l/nostr-websocket-utils.svg)](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/blob/main/LICENSE)
3
+ [![npm version](https://img.shields.io/npm/v/@humanjavaenterprises/nostr-websocket-utils.svg)](https://www.npmjs.com/package/@humanjavaenterprises/nostr-websocket-utils)
4
+ [![License](https://img.shields.io/npm/l/@humanjavaenterprises/nostr-websocket-utils.svg)](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/blob/main/LICENSE)
5
5
  [![Build Status](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/workflows/CI/badge.svg)](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/actions)
6
+ [![Documentation](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/workflows/Documentation/badge.svg)](https://humanjavaenterprises.github.io/nostr-websocket-utils/)
6
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org)
7
8
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
9
 
9
- A TypeScript library providing WebSocket utilities for Nostr applications, focusing on robust connection handling, automatic reconnection, and channel-based messaging. This library has been streamlined to remove any DOM-related code, enhancing performance and maintainability.
10
+ A TypeScript library for building Nostr protocol WebSocket clients and servers.
10
11
 
11
12
  ## Features
12
13
 
13
- - ๐Ÿ”„ Automatic reconnection with configurable attempts
14
- - ๐Ÿ’“ Heartbeat monitoring with configurable intervals
15
- - ๐Ÿ“จ Message queueing during disconnections
16
- - ๐Ÿ“ข Channel-based broadcasting with filtered subscriptions
17
- - ๐Ÿ”’ Type-safe message handling with required handlers
18
- - ๐Ÿ“ Built-in logging with Winston integration
19
- - ๐Ÿ›ก๏ธ Comprehensive error handling and validation
20
- - ๐Ÿงช 100% test coverage with Jest
21
- - ๐Ÿ“ฆ Zero DOM dependencies
22
- - ๐Ÿ” Full TypeScript type safety
14
+ - ๐Ÿš€ Full Nostr protocol support
15
+ - ๐Ÿ”’ Secure WebSocket connections
16
+ - โ™ฅ๏ธ Heartbeat mechanism for connection health
17
+ - ๐Ÿ”„ Automatic reconnection handling
18
+ - ๐Ÿ“ Comprehensive logging
19
+ - ๐ŸŽฏ Type-safe message handling
20
+ - ๐Ÿ“ฆ Easy to use API
23
21
 
24
22
  ## Installation
25
23
 
@@ -27,118 +25,108 @@ A TypeScript library providing WebSocket utilities for Nostr applications, focus
27
25
  npm install nostr-websocket-utils
28
26
  ```
29
27
 
30
- ## Breaking Changes in v0.2.4
28
+ ## Quick Start
31
29
 
32
- - ๐Ÿ†• Added dedicated Nostr WebSocket server implementation
33
- - ๐Ÿ“ Enhanced TypeScript type definitions for Nostr messages
34
- - ๐Ÿ”„ Improved message handling with strict type checking
35
- - ๐Ÿงช Added comprehensive test suite for Nostr server
36
- - ๐Ÿ›ก๏ธ Strengthened type safety with `unknown` types
37
- - ๐Ÿ”Œ Added support for Nostr EVENT and REQ messages
30
+ ### Creating a Nostr WebSocket Client
38
31
 
39
- ## Usage
32
+ ```typescript
33
+ import { NostrWSClient } from 'nostr-websocket-utils';
34
+
35
+ const client = new NostrWSClient('wss://relay.example.com', {
36
+ logger: console,
37
+ heartbeatInterval: 30000,
38
+ handlers: {
39
+ message: async (msg) => console.log('Received:', msg),
40
+ error: (err) => console.error('Error:', err),
41
+ close: () => console.log('Connection closed')
42
+ }
43
+ });
44
+
45
+ await client.connect();
46
+ ```
40
47
 
41
- ### Nostr Server Example
48
+ ### Creating a Nostr WebSocket Server
42
49
 
43
50
  ```typescript
44
- import { NostrWSServer, createWSServer } from 'nostr-websocket-utils';
45
- import { NostrWSMessageType, NostrWSEvent } from 'nostr-websocket-utils/types/nostr';
51
+ import { createNostrServer } from '@humanjavaenterprises/nostr-websocket-utils';
46
52
 
47
- // Create Nostr WebSocket server
48
- const server = createWSServer({
49
- port: 8080,
50
- heartbeatInterval: 30000, // Optional: 30 seconds
53
+ const server = await createNostrServer(8080, {
54
+ logger: console,
55
+ heartbeatInterval: 30000,
51
56
  handlers: {
52
- // Required: Handle incoming messages
53
- message: async (socket, message) => {
54
- switch (message[0]) {
55
- case NostrWSMessageType.EVENT:
56
- const event = message[1] as NostrWSEvent;
57
- // Handle Nostr event
58
- break;
59
- case NostrWSMessageType.REQ:
60
- const [_type, subscriptionId, filter] = message;
61
- // Handle subscription request
62
- break;
63
- }
64
- },
65
- // Optional: Handle errors
66
- error: (socket, error) => {
67
- console.error('WebSocket error:', error);
68
- },
69
- // Optional: Handle client disconnection
70
- close: (socket) => {
71
- console.info('Client disconnected');
57
+ message: async (ws, msg) => {
58
+ console.log('Received message:', msg);
59
+ // Handle the message
72
60
  }
73
61
  }
74
62
  });
75
-
76
- // Start listening
77
- server.listen();
78
63
  ```
79
64
 
80
- ### Types
65
+ ## Documentation
66
+
67
+ Comprehensive API documentation is available in our [documentation site](https://humanjavaenterprises.github.io/nostr-websocket-utils/). Here's what you'll find:
68
+
69
+ ### Core Components
70
+ - [NostrWSClient](docs/classes/NostrWSClient.md) - WebSocket client implementation
71
+ - [NostrWSServer](docs/classes/NostrWSServer.md) - WebSocket server implementation
72
+ - [NostrServer](docs/classes/NostrServer.md) - High-level Nostr server
73
+
74
+ ### Types and Interfaces
75
+ - [NostrWSMessage](docs/interfaces/NostrWSMessage.md) - Message structure
76
+ - [NostrWSOptions](docs/interfaces/NostrWSOptions.md) - Configuration options
77
+ - [NostrWSSubscription](docs/interfaces/NostrWSSubscription.md) - Subscription interface
78
+ - [ExtendedWebSocket](docs/interfaces/ExtendedWebSocket.md) - Enhanced WebSocket interface
79
+
80
+ ### Utility Functions
81
+ - [createServer](docs/functions/createServer.md) - Server creation helper
82
+ - [getLogger](docs/functions/getLogger.md) - Logging utility
83
+
84
+ ### Type Definitions
85
+ - [MessageType](docs/enumerations/NostrWSMessageType.md) - Message type enumeration
86
+ - [Global Types](docs/globals.md) - Global type definitions
87
+
88
+ ## Examples
89
+
90
+ ### Subscribe to a Channel
81
91
 
82
92
  ```typescript
83
- // Nostr Event Type
84
- interface NostrWSEvent {
85
- id: string;
86
- pubkey: string;
87
- created_at: number;
88
- kind: number;
89
- tags: string[][];
90
- content: string;
91
- sig: string;
92
- }
93
-
94
- // Nostr Filter Type
95
- interface NostrWSFilter {
96
- ids?: string[];
97
- authors?: string[];
98
- kinds?: number[];
99
- '#e'?: string[];
100
- '#p'?: string[];
101
- since?: number;
102
- until?: number;
103
- limit?: number;
104
- }
105
-
106
- // Message Types
107
- enum NostrWSMessageType {
108
- EVENT = 'EVENT',
109
- REQ = 'REQ',
110
- CLOSE = 'CLOSE',
111
- NOTICE = 'NOTICE',
112
- AUTH = 'AUTH',
113
- EOSE = 'EOSE'
114
- }
93
+ client.subscribe('my-channel', {
94
+ filter: {
95
+ authors: ['pubkey1', 'pubkey2'],
96
+ kinds: [1]
97
+ }
98
+ });
115
99
  ```
116
100
 
117
- ## Advanced Configuration
118
-
119
- ### Server Options
101
+ ### Broadcast a Message
120
102
 
121
103
  ```typescript
122
- interface NostrWSServerOptions {
123
- port: number;
124
- heartbeatInterval?: number;
125
- maxPayloadSize?: number;
126
- cors?: {
127
- origin?: string | string[];
128
- methods?: string[];
129
- };
130
- handlers: {
131
- message: MessageHandler;
132
- error?: ErrorHandler;
133
- close?: CloseHandler;
134
- };
135
- }
104
+ server.broadcast({
105
+ type: 'event',
106
+ data: {
107
+ content: 'Hello everyone!',
108
+ kind: 1
109
+ }
110
+ });
136
111
  ```
137
112
 
138
113
  ## Contributing
139
114
 
140
- Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
115
+ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
141
116
 
142
117
  ## License
143
118
 
144
119
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
120
+
121
+ ## Related Projects
122
+
123
+ - [nostr-protocol](https://github.com/nostr-protocol/nostr)
124
+ - [nostr-tools](https://github.com/nbd-wtf/nostr-tools)
125
+
126
+ ## Support
127
+
128
+ If you have any questions or need help, please:
129
+
130
+ 1. Check the [documentation](https://humanjavaenterprises.github.io/nostr-websocket-utils/)
131
+ 2. Open an [issue](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/issues)
132
+ 3. Join our [Discord community](https://discord.gg/your-discord)
package/dist/client.d.ts CHANGED
@@ -1,6 +1,27 @@
1
1
  /// <reference types="node" />
2
2
  import { EventEmitter } from 'events';
3
3
  import type { NostrWSOptions, NostrWSMessage } from './types/index.js';
4
+ /**
5
+ * WebSocket client implementation for Nostr protocol communication
6
+ * Extends EventEmitter to provide event-based message handling
7
+ *
8
+ * @extends EventEmitter
9
+ * @example
10
+ * ```typescript
11
+ * const client = new NostrWSClient('wss://relay.example.com', {
12
+ * logger: console,
13
+ * heartbeatInterval: 30000,
14
+ * handlers: {
15
+ * message: async (msg) => console.log('Received:', msg),
16
+ * error: (err) => console.error('Error:', err),
17
+ * close: () => console.log('Connection closed')
18
+ * }
19
+ * });
20
+ *
21
+ * await client.connect();
22
+ * client.send({ type: 'EVENT', data: { ... } });
23
+ * ```
24
+ */
4
25
  export declare class NostrWSClient extends EventEmitter {
5
26
  private url;
6
27
  private ws;
@@ -10,15 +31,99 @@ export declare class NostrWSClient extends EventEmitter {
10
31
  private reconnectAttempts;
11
32
  private messageQueue;
12
33
  private clientId;
34
+ /**
35
+ * Creates a new NostrWSClient instance
36
+ *
37
+ * @param {string} url - The WebSocket server URL to connect to
38
+ * @param {Partial<NostrWSOptions>} options - Configuration options
39
+ * @param {number} [options.heartbeatInterval=30000] - Interval for sending heartbeat messages in milliseconds
40
+ * @param {object} options.logger - Logger instance (required)
41
+ * @param {Function} [options.WebSocketImpl=WebSocket] - WebSocket implementation to use
42
+ * @param {object} [options.handlers] - Event handlers
43
+ * @param {Function} [options.handlers.message] - Message handler function
44
+ * @param {Function} [options.handlers.error] - Error handler function
45
+ * @param {Function} [options.handlers.close] - Connection close handler function
46
+ * @throws {Error} If logger is not provided
47
+ */
13
48
  constructor(url: string, options?: Partial<NostrWSOptions>);
49
+ /**
50
+ * Establishes a connection to the WebSocket server
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * client.connect();
55
+ * ```
56
+ */
14
57
  connect(): void;
58
+ /**
59
+ * Sets up event handlers for the WebSocket connection
60
+ *
61
+ * @private
62
+ */
15
63
  private setupEventHandlers;
64
+ /**
65
+ * Starts sending heartbeat messages at the specified interval
66
+ *
67
+ * @private
68
+ */
16
69
  private startHeartbeat;
70
+ /**
71
+ * Stops sending heartbeat messages
72
+ *
73
+ * @private
74
+ */
17
75
  private stopHeartbeat;
76
+ /**
77
+ * Handles reconnecting to the WebSocket server after a disconnection
78
+ *
79
+ * @private
80
+ */
18
81
  private handleReconnect;
82
+ /**
83
+ * Subscribes to a channel with optional filter
84
+ *
85
+ * @param {string} channel - Channel name
86
+ * @param {unknown} [filter] - Filter data
87
+ * @example
88
+ * ```typescript
89
+ * client.subscribe('channel-name', { ...filterData });
90
+ * ```
91
+ */
19
92
  subscribe(channel: string, filter?: unknown): void;
93
+ /**
94
+ * Unsubscribes from a channel
95
+ *
96
+ * @param {string} channel - Channel name
97
+ * @example
98
+ * ```typescript
99
+ * client.unsubscribe('channel-name');
100
+ * ```
101
+ */
20
102
  unsubscribe(channel: string): void;
103
+ /**
104
+ * Flushes the message queue by sending pending messages
105
+ *
106
+ * @private
107
+ */
21
108
  private flushMessageQueue;
109
+ /**
110
+ * Sends a message to the WebSocket server
111
+ *
112
+ * @param {NostrWSMessage} message - Message to send
113
+ * @returns {Promise<void>}
114
+ * @example
115
+ * ```typescript
116
+ * client.send({ type: 'EVENT', data: { ... } });
117
+ * ```
118
+ */
22
119
  send(message: NostrWSMessage): Promise<void>;
120
+ /**
121
+ * Closes the WebSocket connection
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * client.close();
126
+ * ```
127
+ */
23
128
  close(): void;
24
129
  }
package/dist/client.js CHANGED
@@ -1,7 +1,42 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
2
  import WebSocket from 'ws';
3
3
  import { EventEmitter } from 'events';
4
+ /**
5
+ * WebSocket client implementation for Nostr protocol communication
6
+ * Extends EventEmitter to provide event-based message handling
7
+ *
8
+ * @extends EventEmitter
9
+ * @example
10
+ * ```typescript
11
+ * const client = new NostrWSClient('wss://relay.example.com', {
12
+ * logger: console,
13
+ * heartbeatInterval: 30000,
14
+ * handlers: {
15
+ * message: async (msg) => console.log('Received:', msg),
16
+ * error: (err) => console.error('Error:', err),
17
+ * close: () => console.log('Connection closed')
18
+ * }
19
+ * });
20
+ *
21
+ * await client.connect();
22
+ * client.send({ type: 'EVENT', data: { ... } });
23
+ * ```
24
+ */
4
25
  export class NostrWSClient extends EventEmitter {
26
+ /**
27
+ * Creates a new NostrWSClient instance
28
+ *
29
+ * @param {string} url - The WebSocket server URL to connect to
30
+ * @param {Partial<NostrWSOptions>} options - Configuration options
31
+ * @param {number} [options.heartbeatInterval=30000] - Interval for sending heartbeat messages in milliseconds
32
+ * @param {object} options.logger - Logger instance (required)
33
+ * @param {Function} [options.WebSocketImpl=WebSocket] - WebSocket implementation to use
34
+ * @param {object} [options.handlers] - Event handlers
35
+ * @param {Function} [options.handlers.message] - Message handler function
36
+ * @param {Function} [options.handlers.error] - Error handler function
37
+ * @param {Function} [options.handlers.close] - Connection close handler function
38
+ * @throws {Error} If logger is not provided
39
+ */
5
40
  constructor(url, options = {}) {
6
41
  super();
7
42
  this.url = url;
@@ -25,6 +60,14 @@ export class NostrWSClient extends EventEmitter {
25
60
  }
26
61
  };
27
62
  }
63
+ /**
64
+ * Establishes a connection to the WebSocket server
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * client.connect();
69
+ * ```
70
+ */
28
71
  connect() {
29
72
  if (this.ws) {
30
73
  this.options.logger.info('WebSocket connection already exists');
@@ -41,6 +84,11 @@ export class NostrWSClient extends EventEmitter {
41
84
  this.handleReconnect();
42
85
  }
43
86
  }
87
+ /**
88
+ * Sets up event handlers for the WebSocket connection
89
+ *
90
+ * @private
91
+ */
44
92
  setupEventHandlers() {
45
93
  if (!this.ws)
46
94
  return;
@@ -81,6 +129,11 @@ export class NostrWSClient extends EventEmitter {
81
129
  }
82
130
  });
83
131
  }
132
+ /**
133
+ * Starts sending heartbeat messages at the specified interval
134
+ *
135
+ * @private
136
+ */
84
137
  startHeartbeat() {
85
138
  if (this.heartbeatInterval)
86
139
  return;
@@ -90,12 +143,22 @@ export class NostrWSClient extends EventEmitter {
90
143
  }
91
144
  }, this.options.heartbeatInterval || 30000);
92
145
  }
146
+ /**
147
+ * Stops sending heartbeat messages
148
+ *
149
+ * @private
150
+ */
93
151
  stopHeartbeat() {
94
152
  if (this.heartbeatInterval) {
95
153
  clearInterval(this.heartbeatInterval);
96
154
  this.heartbeatInterval = null;
97
155
  }
98
156
  }
157
+ /**
158
+ * Handles reconnecting to the WebSocket server after a disconnection
159
+ *
160
+ * @private
161
+ */
99
162
  handleReconnect() {
100
163
  if (this.reconnectTimeout)
101
164
  return;
@@ -106,18 +169,42 @@ export class NostrWSClient extends EventEmitter {
106
169
  this.emit('reconnect');
107
170
  }, 5000);
108
171
  }
172
+ /**
173
+ * Subscribes to a channel with optional filter
174
+ *
175
+ * @param {string} channel - Channel name
176
+ * @param {unknown} [filter] - Filter data
177
+ * @example
178
+ * ```typescript
179
+ * client.subscribe('channel-name', { ...filterData });
180
+ * ```
181
+ */
109
182
  subscribe(channel, filter) {
110
183
  this.send({
111
184
  type: 'subscribe',
112
185
  data: { channel, filter }
113
186
  });
114
187
  }
188
+ /**
189
+ * Unsubscribes from a channel
190
+ *
191
+ * @param {string} channel - Channel name
192
+ * @example
193
+ * ```typescript
194
+ * client.unsubscribe('channel-name');
195
+ * ```
196
+ */
115
197
  unsubscribe(channel) {
116
198
  this.send({
117
199
  type: 'unsubscribe',
118
200
  data: { channel }
119
201
  });
120
202
  }
203
+ /**
204
+ * Flushes the message queue by sending pending messages
205
+ *
206
+ * @private
207
+ */
121
208
  flushMessageQueue() {
122
209
  while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
123
210
  const message = this.messageQueue.shift();
@@ -126,6 +213,16 @@ export class NostrWSClient extends EventEmitter {
126
213
  }
127
214
  }
128
215
  }
216
+ /**
217
+ * Sends a message to the WebSocket server
218
+ *
219
+ * @param {NostrWSMessage} message - Message to send
220
+ * @returns {Promise<void>}
221
+ * @example
222
+ * ```typescript
223
+ * client.send({ type: 'EVENT', data: { ... } });
224
+ * ```
225
+ */
129
226
  async send(message) {
130
227
  message.id = message.id || uuidv4();
131
228
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -140,6 +237,14 @@ export class NostrWSClient extends EventEmitter {
140
237
  this.handleReconnect();
141
238
  }
142
239
  }
240
+ /**
241
+ * Closes the WebSocket connection
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * client.close();
246
+ * ```
247
+ */
143
248
  close() {
144
249
  this.stopHeartbeat();
145
250
  if (this.reconnectTimeout) {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @file Main entry point for the nostr-websocket-utils library
3
+ * @module nostr-websocket-utils
4
+ */
1
5
  export { NostrWSClient } from './client.js';
2
6
  export { NostrWSServer } from './server.js';
3
7
  export { NostrWSServer as NostrServer } from './nostr-server.js';
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @file Main entry point for the nostr-websocket-utils library
3
+ * @module nostr-websocket-utils
4
+ */
1
5
  export { NostrWSClient } from './client.js';
2
6
  export { NostrWSServer } from './server.js';
3
7
  export { NostrWSServer as NostrServer } from './nostr-server.js';
@@ -1,8 +1,31 @@
1
1
  import { NostrWSServerOptions } from './types/nostr';
2
+ /**
3
+ * Represents a Nostr WebSocket server
4
+ */
2
5
  export declare class NostrWSServer {
6
+ /**
7
+ * The underlying WebSocket server instance
8
+ */
3
9
  private server;
10
+ /**
11
+ * Logger instance for this server
12
+ */
4
13
  private logger;
14
+ /**
15
+ * Creates a new Nostr WebSocket server instance
16
+ *
17
+ * @param {NostrWSServerOptions} options - Server configuration options
18
+ */
5
19
  constructor(options: NostrWSServerOptions);
20
+ /**
21
+ * Closes the WebSocket server
22
+ */
6
23
  close(): void;
7
24
  }
25
+ /**
26
+ * Creates a new Nostr WebSocket server instance
27
+ *
28
+ * @param {NostrWSServerOptions} options - Server configuration options
29
+ * @returns {NostrWSServer} The created server instance
30
+ */
8
31
  export declare function createWSServer(options: NostrWSServerOptions): NostrWSServer;
@@ -1,15 +1,36 @@
1
1
  import { Server as WebSocketServer } from 'ws';
2
2
  import { getLogger } from './utils/logger';
3
+ /**
4
+ * Represents a Nostr WebSocket server
5
+ */
3
6
  export class NostrWSServer {
7
+ /**
8
+ * Creates a new Nostr WebSocket server instance
9
+ *
10
+ * @param {NostrWSServerOptions} options - Server configuration options
11
+ */
4
12
  constructor(options) {
13
+ /**
14
+ * Logger instance for this server
15
+ */
5
16
  this.logger = getLogger('NostrWSServer');
6
17
  this.server = new WebSocketServer({ port: options.port });
18
+ /**
19
+ * Handles incoming WebSocket connections
20
+ *
21
+ * @param {NostrWSSocket} socket - The connected WebSocket client
22
+ */
7
23
  this.server.on('connection', (socket) => {
8
24
  socket.subscriptions = new Set();
9
25
  socket.isAlive = true;
10
26
  if (options.onConnection) {
11
27
  options.onConnection(socket);
12
28
  }
29
+ /**
30
+ * Handles incoming messages from the client
31
+ *
32
+ * @param {Buffer | ArrayBuffer | Buffer[]} data - The incoming message data
33
+ */
13
34
  const handleMessage = async (data) => {
14
35
  try {
15
36
  const message = JSON.parse(data.toString());
@@ -24,11 +45,19 @@ export class NostrWSServer {
24
45
  }
25
46
  };
26
47
  socket.on('message', handleMessage);
48
+ /**
49
+ * Handles client disconnections
50
+ */
27
51
  socket.on('close', () => {
28
52
  if (options.handlers?.close) {
29
53
  options.handlers.close(socket);
30
54
  }
31
55
  });
56
+ /**
57
+ * Handles WebSocket errors
58
+ *
59
+ * @param {Error} error - The error that occurred
60
+ */
32
61
  socket.on('error', (error) => {
33
62
  if (options.handlers?.error) {
34
63
  options.handlers.error(socket, error);
@@ -45,19 +74,34 @@ export class NostrWSServer {
45
74
  socket.isAlive = false;
46
75
  socket.ping();
47
76
  }, options.heartbeatInterval);
77
+ /**
78
+ * Handles WebSocket pong responses
79
+ */
48
80
  socket.on('pong', () => {
49
81
  socket.isAlive = true;
50
82
  });
83
+ /**
84
+ * Handles client disconnections (again, to clear the interval)
85
+ */
51
86
  socket.on('close', () => {
52
87
  clearInterval(interval);
53
88
  });
54
89
  }
55
90
  });
56
91
  }
92
+ /**
93
+ * Closes the WebSocket server
94
+ */
57
95
  close() {
58
96
  this.server.close();
59
97
  }
60
98
  }
99
+ /**
100
+ * Creates a new Nostr WebSocket server instance
101
+ *
102
+ * @param {NostrWSServerOptions} options - Server configuration options
103
+ * @returns {NostrWSServer} The created server instance
104
+ */
61
105
  export function createWSServer(options) {
62
106
  return new NostrWSServer(options);
63
107
  }
package/dist/server.d.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /// <reference types="node" />
2
- import { EventEmitter } from 'events';
3
2
  import { WebSocketServer } from 'ws';
4
- import type { NostrWSOptions, NostrWSMessage, ExtendedWebSocket } from './types/index.js';
3
+ import { EventEmitter } from 'events';
4
+ import type { NostrWSOptions, NostrWSMessage } from './types/index.js';
5
+ /**
6
+ * WebSocket server implementation for Nostr protocol
7
+ * Extends EventEmitter to provide event-based message handling
8
+ */
5
9
  export declare class NostrWSServer extends EventEmitter {
6
10
  private wss;
7
11
  private options;
12
+ private clients;
8
13
  private heartbeatInterval;
9
- clients: Map<string, ExtendedWebSocket>;
10
14
  constructor(wss: WebSocketServer, options?: Partial<NostrWSOptions>);
11
15
  private setupServer;
12
16
  private handleConnection;
@@ -14,4 +18,10 @@ export declare class NostrWSServer extends EventEmitter {
14
18
  broadcast(message: NostrWSMessage): void;
15
19
  broadcastToChannel(channel: string, message: NostrWSMessage): void;
16
20
  close(): void;
21
+ /**
22
+ * Check if a client with the given ID exists
23
+ * @param clientId - The ID of the client to check
24
+ * @returns boolean indicating if the client exists
25
+ */
26
+ hasClient(clientId: string): boolean;
17
27
  }
package/dist/server.js CHANGED
@@ -1,50 +1,59 @@
1
- import { EventEmitter } from 'events';
2
1
  import { WebSocket } from 'ws';
2
+ import { EventEmitter } from 'events';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
+ /**
5
+ * WebSocket server implementation for Nostr protocol
6
+ * Extends EventEmitter to provide event-based message handling
7
+ */
4
8
  export class NostrWSServer extends EventEmitter {
5
9
  constructor(wss, options = {}) {
6
10
  super();
7
- this.heartbeatInterval = null;
11
+ this.wss = null;
8
12
  this.clients = new Map();
13
+ this.heartbeatInterval = null;
9
14
  if (!options.logger) {
10
15
  throw new Error('Logger is required');
11
16
  }
12
- if (!options.handlers?.message) {
13
- throw new Error('Message handler is required');
14
- }
17
+ this.wss = wss;
15
18
  this.options = {
16
- heartbeatInterval: options.heartbeatInterval || 30000,
19
+ heartbeatInterval: 30000,
17
20
  logger: options.logger,
18
- WebSocketImpl: options.WebSocketImpl || WebSocket,
21
+ WebSocketImpl: WebSocket,
22
+ ...options,
19
23
  handlers: {
20
- message: options.handlers.message,
21
- error: options.handlers.error || (() => { }),
22
- close: options.handlers.close || (() => { })
23
- }
24
+ message: async (_ws, _message) => { },
25
+ ...options.handlers,
26
+ },
24
27
  };
25
- this.wss = wss;
26
28
  this.setupServer();
27
29
  }
28
30
  setupServer() {
31
+ if (!this.wss)
32
+ return;
29
33
  this.wss.on('connection', (ws) => {
30
- this.handleConnection(ws);
34
+ const extWs = ws;
35
+ this.handleConnection(extWs);
31
36
  });
32
- if (this.options.heartbeatInterval && this.options.heartbeatInterval > 0) {
37
+ if (this.options.heartbeatInterval) {
33
38
  this.startHeartbeat();
34
39
  }
35
40
  }
36
41
  handleConnection(ws) {
37
42
  ws.isAlive = true;
38
43
  ws.subscriptions = new Set();
39
- ws.clientId = ws.clientId || uuidv4();
44
+ ws.clientId = uuidv4();
45
+ ws.messageQueue = [];
40
46
  this.clients.set(ws.clientId, ws);
41
47
  ws.on('message', async (data) => {
42
48
  try {
43
49
  const message = JSON.parse(data.toString());
44
- await this.options.handlers.message(ws, message);
50
+ if (this.options.handlers?.message) {
51
+ await this.options.handlers.message(ws, message);
52
+ }
45
53
  }
46
54
  catch (error) {
47
- if (this.options.handlers.error) {
55
+ this.options.logger.error('Error handling message:', error);
56
+ if (this.options.handlers?.error) {
48
57
  this.options.handlers.error(ws, error);
49
58
  }
50
59
  }
@@ -53,35 +62,39 @@ export class NostrWSServer extends EventEmitter {
53
62
  if (ws.clientId) {
54
63
  this.clients.delete(ws.clientId);
55
64
  }
56
- if (this.options.handlers.close) {
65
+ if (this.options.handlers?.close) {
57
66
  this.options.handlers.close(ws);
58
67
  }
59
68
  });
60
69
  ws.on('error', (error) => {
61
- if (ws.clientId) {
62
- this.clients.delete(ws.clientId);
63
- }
64
- if (this.options.handlers.error) {
70
+ this.options.logger.error('WebSocket error:', error);
71
+ if (this.options.handlers?.error) {
65
72
  this.options.handlers.error(ws, error);
66
73
  }
67
74
  });
68
75
  }
69
76
  startHeartbeat() {
77
+ if (this.heartbeatInterval)
78
+ return;
70
79
  this.heartbeatInterval = setInterval(() => {
71
- this.wss.clients.forEach((ws) => {
72
- const extWs = ws;
73
- if (!extWs.isAlive) {
74
- extWs.terminate();
75
- return;
76
- }
77
- extWs.isAlive = false;
78
- if (ws.readyState === WebSocket.OPEN) {
79
- ws.ping();
80
+ if (!this.wss)
81
+ return;
82
+ this.wss.clients.forEach((client) => {
83
+ const extClient = client;
84
+ if (extClient.isAlive === false) {
85
+ if (extClient.clientId) {
86
+ this.clients.delete(extClient.clientId);
87
+ }
88
+ return extClient.terminate();
80
89
  }
90
+ extClient.isAlive = false;
91
+ extClient.ping();
81
92
  });
82
93
  }, this.options.heartbeatInterval);
83
94
  }
84
95
  broadcast(message) {
96
+ if (!this.wss)
97
+ return;
85
98
  this.wss.clients.forEach((client) => {
86
99
  if (client.readyState === WebSocket.OPEN) {
87
100
  client.send(JSON.stringify(message));
@@ -89,17 +102,31 @@ export class NostrWSServer extends EventEmitter {
89
102
  });
90
103
  }
91
104
  broadcastToChannel(channel, message) {
105
+ if (!this.wss)
106
+ return;
92
107
  this.wss.clients.forEach((ws) => {
93
108
  const extWs = ws;
94
109
  if (extWs.readyState === WebSocket.OPEN && extWs.subscriptions?.has(channel)) {
95
- ws.send(JSON.stringify(message));
110
+ extWs.send(JSON.stringify(message));
96
111
  }
97
112
  });
98
113
  }
99
114
  close() {
100
115
  if (this.heartbeatInterval) {
101
116
  clearInterval(this.heartbeatInterval);
117
+ this.heartbeatInterval = null;
102
118
  }
103
- this.wss.close();
119
+ if (this.wss) {
120
+ this.wss.close();
121
+ this.wss = null;
122
+ }
123
+ }
124
+ /**
125
+ * Check if a client with the given ID exists
126
+ * @param clientId - The ID of the client to check
127
+ * @returns boolean indicating if the client exists
128
+ */
129
+ hasClient(clientId) {
130
+ return this.clients.has(clientId);
104
131
  }
105
132
  }
@@ -1,58 +1,233 @@
1
1
  import type { WebSocket } from 'ws';
2
+ /**
3
+ * Type of message that can be sent through the WebSocket connection
4
+ * @type {('subscribe' | 'unsubscribe' | 'event' | 'request' | 'response' | 'error' | 'status')}
5
+ */
2
6
  export type MessageType = 'subscribe' | 'unsubscribe' | 'event' | 'request' | 'response' | 'error' | 'status';
7
+ /**
8
+ * Configuration options for the NostrWSClient
9
+ * @interface NostrWSOptions
10
+ */
3
11
  export interface NostrWSOptions {
12
+ /**
13
+ * Interval in milliseconds between heartbeat messages
14
+ * @default 30000
15
+ */
4
16
  heartbeatInterval?: number;
17
+ /**
18
+ * Interval in milliseconds between reconnect attempts
19
+ * @default 5000
20
+ */
5
21
  reconnectInterval?: number;
22
+ /**
23
+ * Maximum number of reconnect attempts
24
+ * @default 10
25
+ */
6
26
  maxReconnectAttempts?: number;
27
+ /**
28
+ * Logger instance for handling log messages
29
+ * Must implement debug, info, error, and warn methods
30
+ */
7
31
  logger: Logger;
32
+ /**
33
+ * WebSocket implementation to use
34
+ * Defaults to the native WebSocket implementation
35
+ */
8
36
  WebSocketImpl: typeof WebSocket;
37
+ /**
38
+ * Event handlers for WebSocket events
39
+ */
9
40
  handlers: {
41
+ /**
42
+ * Handler for incoming messages
43
+ * @param ws - The WebSocket instance
44
+ * @param message - The received message
45
+ */
10
46
  message: (ws: ExtendedWebSocket, message: NostrWSMessage) => Promise<void> | void;
47
+ /**
48
+ * Handler for WebSocket errors
49
+ * @param ws - The WebSocket instance
50
+ * @param error - The error object
51
+ */
11
52
  error?: (ws: WebSocket, error: Error) => void;
53
+ /**
54
+ * Handler for WebSocket connection close
55
+ * @param ws - The WebSocket instance
56
+ */
12
57
  close?: (ws: WebSocket) => void;
13
58
  };
14
59
  }
60
+ /**
61
+ * Structure of messages sent through the WebSocket connection
62
+ * @interface NostrWSMessage
63
+ */
15
64
  export interface NostrWSMessage {
16
- type: string;
65
+ /**
66
+ * Type of the message (e.g., 'EVENT', 'subscribe', 'unsubscribe')
67
+ */
68
+ type: MessageType;
69
+ /**
70
+ * Unique identifier for the message
71
+ */
17
72
  id?: string;
73
+ /**
74
+ * Data payload of the message
75
+ */
18
76
  data: Record<string, unknown>;
19
77
  }
78
+ /**
79
+ * Represents a subscription to a Nostr relay
80
+ * @interface NostrWSSubscription
81
+ */
20
82
  export interface NostrWSSubscription {
83
+ /**
84
+ * Channel identifier for the subscription
85
+ */
21
86
  channel: string;
87
+ /**
88
+ * Filter criteria for the subscription
89
+ */
22
90
  filter?: Record<string, unknown>;
23
91
  }
92
+ /**
93
+ * Events emitted by the NostrWSClient
94
+ * @interface NostrWSClientEvents
95
+ */
24
96
  export interface NostrWSClientEvents {
97
+ /**
98
+ * Emitted when the client connects to the relay
99
+ */
25
100
  connect: () => void;
101
+ /**
102
+ * Emitted when the client disconnects from the relay
103
+ */
26
104
  disconnect: () => void;
105
+ /**
106
+ * Emitted when the client reconnects to the relay
107
+ */
27
108
  reconnect: () => void;
109
+ /**
110
+ * Emitted when a message is received from the relay
111
+ * @param message - The received message
112
+ */
28
113
  message: (message: NostrWSMessage) => void;
114
+ /**
115
+ * Emitted when an error occurs
116
+ * @param error - The error object
117
+ */
29
118
  error: (error: Error) => void;
30
119
  }
120
+ /**
121
+ * Events emitted by the NostrWSServer
122
+ * @interface NostrWSServerEvents
123
+ */
31
124
  export interface NostrWSServerEvents {
125
+ /**
126
+ * Emitted when a client connects to the server
127
+ * @param client - The connected client
128
+ */
32
129
  connection: (client: ExtendedWebSocket) => void;
130
+ /**
131
+ * Emitted when a message is received from a client
132
+ * @param message - The received message
133
+ * @param client - The client that sent the message
134
+ */
33
135
  message: (message: NostrWSMessage, client: ExtendedWebSocket) => void;
136
+ /**
137
+ * Emitted when an error occurs
138
+ * @param error - The error object
139
+ */
34
140
  error: (error: Error) => void;
35
141
  }
142
+ /**
143
+ * Extended WebSocket interface with additional properties
144
+ * @interface ExtendedWebSocket
145
+ */
36
146
  export interface ExtendedWebSocket extends WebSocket {
147
+ /**
148
+ * Whether the WebSocket connection is alive
149
+ */
37
150
  isAlive?: boolean;
151
+ /**
152
+ * Set of subscription channels
153
+ */
38
154
  subscriptions?: Set<string>;
155
+ /**
156
+ * Unique client identifier
157
+ */
39
158
  clientId?: string;
159
+ /**
160
+ * Queue of messages to be sent
161
+ */
40
162
  messageQueue?: NostrWSMessage[];
163
+ /**
164
+ * Timestamp of the last ping message
165
+ */
41
166
  lastPing?: number;
167
+ /**
168
+ * Number of reconnect attempts
169
+ */
42
170
  reconnectAttempts?: number;
43
171
  }
172
+ /**
173
+ * Result of validating a NostrWSMessage
174
+ * @interface NostrWSValidationResult
175
+ */
44
176
  export interface NostrWSValidationResult {
177
+ /**
178
+ * Whether the message is valid
179
+ */
45
180
  isValid: boolean;
181
+ /**
182
+ * Error message if the message is invalid
183
+ */
46
184
  error?: string;
47
185
  }
186
+ /**
187
+ * State of the NostrWSClient connection
188
+ * @interface NostrWSConnectionState
189
+ */
48
190
  export interface NostrWSConnectionState {
191
+ /**
192
+ * Whether the client is connected to the relay
193
+ */
49
194
  isConnected: boolean;
195
+ /**
196
+ * Number of reconnect attempts
197
+ */
50
198
  reconnectAttempts: number;
199
+ /**
200
+ * Last error message
201
+ */
51
202
  lastError?: string;
52
203
  }
204
+ /**
205
+ * Logger interface
206
+ * @interface Logger
207
+ */
53
208
  export interface Logger {
209
+ /**
210
+ * Logs a debug message
211
+ * @param message - The message to log
212
+ * @param args - Additional arguments to log
213
+ */
54
214
  debug: (message: string, ...args: unknown[]) => void;
215
+ /**
216
+ * Logs an info message
217
+ * @param message - The message to log
218
+ * @param args - Additional arguments to log
219
+ */
55
220
  info: (message: string, ...args: unknown[]) => void;
221
+ /**
222
+ * Logs a warning message
223
+ * @param message - The message to log
224
+ * @param args - Additional arguments to log
225
+ */
56
226
  warn: (message: string, ...args: unknown[]) => void;
227
+ /**
228
+ * Logs an error message
229
+ * @param message - The message to log
230
+ * @param args - Additional arguments to log
231
+ */
57
232
  error: (message: string, ...args: unknown[]) => void;
58
233
  }
@@ -1,2 +1,6 @@
1
+ /**
2
+ * @file Logger utility for Nostr WebSocket operations
3
+ * @module logger
4
+ */
1
5
  import winston from 'winston';
2
- export declare function getLogger(name?: string): winston.Logger;
6
+ export declare function getLogger(context: string): winston.Logger;
@@ -1,4 +1,21 @@
1
+ /**
2
+ * @file Logger utility for Nostr WebSocket operations
3
+ * @module logger
4
+ */
1
5
  import winston from 'winston';
6
+ /**
7
+ * Creates a logger instance with a specific context
8
+ *
9
+ * @param {string} context - The context identifier for the logger
10
+ * @returns {Logger} A logger instance with debug, info, warn, and error methods
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const logger = getLogger('MyComponent');
15
+ * logger.info('Component initialized');
16
+ * logger.error('An error occurred', new Error('Failed to connect'));
17
+ * ```
18
+ */
2
19
  const logger = winston.createLogger({
3
20
  level: process.env.LOG_LEVEL || 'info',
4
21
  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
@@ -8,9 +25,6 @@ const logger = winston.createLogger({
8
25
  })
9
26
  ]
10
27
  });
11
- export function getLogger(name) {
12
- if (name) {
13
- return logger.child({ service: name });
14
- }
15
- return logger;
28
+ export function getLogger(context) {
29
+ return logger.child({ service: context });
16
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-websocket-utils",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Robust WebSocket utilities for Nostr applications with automatic reconnection, channel-based messaging, and type-safe handlers. Features heartbeat monitoring, message queueing, and comprehensive error handling.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,10 @@
12
12
  "test:coverage": "jest --coverage",
13
13
  "lint": "eslint src --ext .ts",
14
14
  "prepare": "npm run build",
15
- "prepublishOnly": "npm test && npm run lint"
15
+ "prepublishOnly": "npm test && npm run lint",
16
+ "docs": "typedoc",
17
+ "docs:watch": "typedoc --watch",
18
+ "predeploy": "npm run build && npm run docs"
16
19
  },
17
20
  "keywords": [
18
21
  "nostr",
@@ -37,7 +40,7 @@
37
40
  "bugs": {
38
41
  "url": "https://github.com/HumanjavaEnterprises/nostr-websocket-utils/issues"
39
42
  },
40
- "homepage": "https://github.com/HumanjavaEnterprises/nostr-websocket-utils#readme",
43
+ "homepage": "https://humanjavaenterprises.github.io/nostr-websocket-utils",
41
44
  "dependencies": {
42
45
  "@noble/curves": "^1.3.0",
43
46
  "@noble/hashes": "^1.3.3",
@@ -60,6 +63,8 @@
60
63
  "eslint": "^8.56.0",
61
64
  "jest": "^29.7.0",
62
65
  "ts-jest": "^29.1.1",
66
+ "typedoc": "^0.27.5",
67
+ "typedoc-plugin-markdown": "^4.3.2",
63
68
  "typescript": "^5.3.3"
64
69
  },
65
70
  "files": [