nostr-websocket-utils 0.1.0 โ†’ 0.2.2

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,12 +1,12 @@
1
- Nostr Websocket Utils
1
+ # Nostr Websocket Utils
2
2
 
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)
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)
5
5
  [![Build Status](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/workflows/CI/badge.svg)](https://github.com/HumanjavaEnterprises/nostr-websocket-utils/actions)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org)
7
7
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
8
 
9
- A TypeScript library providing WebSocket utilities for Nostr applications, with robust connection handling, automatic reconnection, and channel-based messaging.
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
10
 
11
11
  ## Features
12
12
 
@@ -14,64 +14,115 @@ A TypeScript library providing WebSocket utilities for Nostr applications, with
14
14
  - ๐Ÿ’“ Heartbeat monitoring
15
15
  - ๐Ÿ“จ Message queueing during disconnections
16
16
  - ๐Ÿ“ข Channel-based broadcasting
17
- - ๐Ÿ”’ Type-safe message handling
17
+ - ๐Ÿ”’ Type-safe message handling with required handlers
18
18
  - ๐Ÿ“ Built-in logging
19
+ - ๐Ÿ›ก๏ธ Comprehensive error handling
20
+ - ๐Ÿงช Full test coverage
19
21
 
20
22
  ## Installation
21
23
 
22
24
  ```bash
23
- npm install @humanjavaenterprises/nostr-websocket-utils
25
+ npm install nostr-websocket-utils
24
26
  ```
25
27
 
28
+ ## Breaking Changes in v0.2.2
29
+
30
+ - Removed all DOM-related code to focus solely on WebSocket functionality.
31
+ - Added UUID support for message tracking and correlation.
32
+ - Introduced a new `WebSocketImpl` option for custom WebSocket implementations.
33
+ - Improved TypeScript type safety across the codebase.
34
+ - Enhanced error handling for WebSocket connections.
35
+
26
36
  ## Usage
27
37
 
28
- ### Client Example
38
+ ### Server Example
29
39
 
30
40
  ```typescript
31
- import { NostrWSClient } from '@humanjavaenterprises/nostr-websocket-utils';
41
+ import express from 'express';
42
+ import { createServer } from 'http';
43
+ import { NostrWSServer } from 'nostr-websocket-utils';
44
+ import winston from 'winston';
32
45
 
33
- const client = new NostrWSClient('wss://your-server.com', {
34
- heartbeatInterval: 30000,
35
- reconnectInterval: 5000,
36
- maxReconnectAttempts: 5
37
- });
46
+ const app = express();
47
+ const server = createServer(app);
38
48
 
39
- client.on('connect', () => {
40
- console.log('Connected!');
41
- client.subscribe('my-channel');
49
+ // Create a logger
50
+ const logger = winston.createLogger({
51
+ level: 'info',
52
+ format: winston.format.json(),
53
+ transports: [new winston.transports.Console()]
42
54
  });
43
55
 
44
- client.on('message', (message) => {
45
- console.log('Received:', message);
56
+ // Initialize the WebSocket server with custom WebSocket implementation
57
+ const wsServer = new NostrWSServer(server, {
58
+ logger,
59
+ handlers: {
60
+ message: async (ws, message) => {
61
+ logger.info('Received message:', message);
62
+ // Handle message
63
+ },
64
+ error: (ws, error) => {
65
+ logger.error('WebSocket error:', error);
66
+ },
67
+ close: (ws) => {
68
+ logger.info('Connection closed');
69
+ }
70
+ }
46
71
  });
47
72
 
48
- client.connect();
73
+ server.listen(3000);
49
74
  ```
50
75
 
51
- ### Server Example
76
+ ### Client Example
52
77
 
53
78
  ```typescript
54
- import express from 'express';
55
- import { createServer } from 'http';
56
- import { NostrWSServer } from '@humanjavaenterprises/nostr-websocket-utils';
79
+ import { NostrWSClient } from 'nostr-websocket-utils';
80
+ import winston from 'winston';
57
81
 
58
- const app = express();
59
- const server = createServer(app);
60
- const wss = new NostrWSServer(server, {
61
- heartbeatInterval: 30000
82
+ const logger = winston.createLogger({
83
+ level: 'info',
84
+ format: winston.format.json(),
85
+ transports: [new winston.transports.Console()]
62
86
  });
63
87
 
64
- wss.on('message', (message, client) => {
88
+ const client = new NostrWSClient('ws://localhost:3000', {
89
+ logger,
90
+ WebSocketImpl: CustomWebSocketImplementation // Optional custom WebSocket implementation
91
+ });
92
+
93
+ client.on('message', (message) => {
65
94
  console.log('Received message:', message);
66
-
67
- // Broadcast to specific channel
68
- wss.broadcastToChannel('my-channel', {
69
- type: 'event',
70
- data: { content: 'Hello channel!' }
71
- });
72
95
  });
73
96
 
74
- server.listen(3000);
97
+ client.connect();
98
+ ```
99
+
100
+ ## Interface Reference
101
+
102
+ ### NostrWSOptions
103
+
104
+ ```typescript
105
+ interface NostrWSOptions {
106
+ // Interval for sending heartbeat pings (ms)
107
+ heartbeatInterval?: number;
108
+ // Interval between reconnection attempts (ms)
109
+ reconnectInterval?: number;
110
+ // Maximum number of reconnection attempts
111
+ maxReconnectAttempts?: number;
112
+ // Required logger instance
113
+ logger: Logger;
114
+ // Required handlers object
115
+ handlers: {
116
+ // Required message handler
117
+ message: (ws: ExtendedWebSocket, message: NostrWSMessage) => Promise<void> | void;
118
+ // Optional error handler
119
+ error?: (ws: WebSocket, error: Error) => void;
120
+ // Optional close handler
121
+ close?: (ws: WebSocket) => void;
122
+ };
123
+ // Optional custom WebSocket implementation
124
+ WebSocketImpl?: WebSocketImpl;
125
+ }
75
126
  ```
76
127
 
77
128
  ## Why This Library is Perfect for Nostr Development
@@ -99,54 +150,12 @@ This library is specifically designed to accelerate Nostr application developmen
99
150
  - Full TypeScript support with comprehensive type definitions
100
151
  - Event-driven architecture matching Nostr's event-centric nature
101
152
  - Clear, consistent error handling and validation
102
- - Minimal boilerplate needed to get started
103
-
104
- ### 5. Production-Ready Features
105
- - Built-in logging system
106
- - Memory leak prevention through proper client cleanup
107
- - Scalable client management
108
- - Support for multiple simultaneous subscriptions
109
-
110
- By using this library, you can skip weeks of WebSocket infrastructure development and focus on building your Nostr application's unique features.
111
-
112
- ## API Reference
113
-
114
- ### NostrWSClient
115
-
116
- - `constructor(url: string, options?: NostrWSOptions)`
117
- - `connect(): void`
118
- - `send(message: NostrWSMessage): void`
119
- - `subscribe(channel: string): void`
120
- - `unsubscribe(channel: string): void`
121
- - `close(): void`
122
-
123
- Events:
124
- - `connect`
125
- - `disconnect`
126
- - `reconnect`
127
- - `message`
128
- - `error`
129
-
130
- ### NostrWSServer
131
-
132
- - `constructor(server: any, options?: NostrWSOptions)`
133
- - `broadcast(message: NostrWSMessage): void`
134
- - `broadcastToChannel(channel: string, message: NostrWSMessage): void`
135
- - `sendTo(client: ExtendedWebSocket, message: NostrWSMessage): void`
136
- - `getClients(): Set<ExtendedWebSocket>`
137
- - `close(): void`
138
-
139
- Events:
140
- - `message`
153
+ - Required handlers pattern ensures type safety
141
154
 
142
155
  ## Contributing
143
156
 
144
- 1. Fork the repository
145
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
146
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
147
- 4. Push to the branch (`git push origin feature/amazing-feature`)
148
- 5. Open a Pull Request
157
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
149
158
 
150
159
  ## License
151
160
 
152
- MIT
161
+ MIT License - see the [LICENSE](LICENSE) file for details.
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { EventEmitter } from 'events';
2
3
  import type { NostrWSOptions, NostrWSMessage } from './types/index.js';
3
4
  export declare class NostrWSClient extends EventEmitter {
@@ -8,6 +9,7 @@ export declare class NostrWSClient extends EventEmitter {
8
9
  private heartbeatInterval;
9
10
  private reconnectAttempts;
10
11
  private messageQueue;
12
+ private clientId;
11
13
  constructor(url: string, options?: Partial<NostrWSOptions>);
12
14
  connect(): void;
13
15
  private setupEventHandlers;
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { v4 as uuidv4 } from 'uuid';
1
2
  import WebSocket from 'ws';
2
3
  import { EventEmitter } from 'events';
3
4
  export class NostrWSClient extends EventEmitter {
@@ -12,12 +13,16 @@ export class NostrWSClient extends EventEmitter {
12
13
  if (!options.logger) {
13
14
  throw new Error('Logger is required');
14
15
  }
16
+ this.clientId = uuidv4();
15
17
  this.options = {
16
18
  heartbeatInterval: options.heartbeatInterval || 30000,
17
19
  logger: options.logger,
18
- onMessage: options.onMessage,
19
- onError: options.onError,
20
- onClose: options.onClose
20
+ WebSocketImpl: options.WebSocketImpl || WebSocket,
21
+ handlers: {
22
+ message: options.handlers?.message || (async () => { }),
23
+ error: options.handlers?.error || (() => { }),
24
+ close: options.handlers?.close || (() => { })
25
+ }
21
26
  };
22
27
  }
23
28
  connect() {
@@ -26,7 +31,9 @@ export class NostrWSClient extends EventEmitter {
26
31
  return;
27
32
  }
28
33
  try {
29
- this.ws = new WebSocket(this.url);
34
+ this.options.logger.debug('Creating new WebSocket connection');
35
+ this.ws = new this.options.WebSocketImpl(this.url);
36
+ this.options.logger.debug('WebSocket created successfully');
30
37
  this.setupEventHandlers();
31
38
  }
32
39
  catch (error) {
@@ -48,14 +55,12 @@ export class NostrWSClient extends EventEmitter {
48
55
  try {
49
56
  const message = JSON.parse(data.toString());
50
57
  this.emit('message', message);
51
- if (this.options.onMessage) {
52
- await this.options.onMessage(this.ws, message);
53
- }
58
+ await this.options.handlers.message(this.ws, message);
54
59
  }
55
60
  catch (error) {
56
61
  this.emit('error', error);
57
- if (this.options.onError) {
58
- this.options.onError(this.ws, error);
62
+ if (this.options.handlers.error) {
63
+ this.options.handlers.error(this.ws, error);
59
64
  }
60
65
  }
61
66
  });
@@ -64,15 +69,15 @@ export class NostrWSClient extends EventEmitter {
64
69
  this.stopHeartbeat();
65
70
  this.handleReconnect();
66
71
  this.emit('disconnect');
67
- if (this.options.onClose) {
68
- this.options.onClose(this.ws);
72
+ if (this.options.handlers.close) {
73
+ this.options.handlers.close(this.ws);
69
74
  }
70
75
  });
71
76
  this.ws.on('error', (error) => {
72
77
  this.options.logger.error('WebSocket error:', error);
73
78
  this.emit('error', error);
74
- if (this.options.onError) {
75
- this.options.onError(this.ws, error);
79
+ if (this.options.handlers.error) {
80
+ this.options.handlers.error(this.ws, error);
76
81
  }
77
82
  });
78
83
  }
@@ -122,6 +127,7 @@ export class NostrWSClient extends EventEmitter {
122
127
  }
123
128
  }
124
129
  async send(message) {
130
+ message.id = message.id || uuidv4();
125
131
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
126
132
  this.messageQueue.push(message);
127
133
  return;
package/dist/server.d.ts CHANGED
@@ -1,12 +1,15 @@
1
+ /// <reference types="node" />
1
2
  import { EventEmitter } from 'events';
2
- import { Server as HttpServer } from 'http';
3
- import type { NostrWSOptions, NostrWSMessage } from './types/index.js';
3
+ import { WebSocketServer } from 'ws';
4
+ import type { NostrWSOptions, NostrWSMessage, ExtendedWebSocket } from './types/index.js';
4
5
  export declare class NostrWSServer extends EventEmitter {
5
6
  private wss;
6
7
  private options;
7
8
  private heartbeatInterval;
8
- constructor(server: HttpServer, options?: Partial<NostrWSOptions>);
9
+ clients: Map<string, ExtendedWebSocket>;
10
+ constructor(wss: WebSocketServer, options?: Partial<NostrWSOptions>);
9
11
  private setupServer;
12
+ private handleConnection;
10
13
  private startHeartbeat;
11
14
  broadcast(message: NostrWSMessage): void;
12
15
  broadcastToChannel(channel: string, message: NostrWSMessage): void;
package/dist/server.js CHANGED
@@ -1,69 +1,85 @@
1
1
  import { EventEmitter } from 'events';
2
- import { WebSocketServer, WebSocket } from 'ws';
2
+ import { WebSocket } from 'ws';
3
+ import { v4 as uuidv4 } from 'uuid';
3
4
  export class NostrWSServer extends EventEmitter {
4
- constructor(server, options = {}) {
5
+ constructor(wss, options = {}) {
5
6
  super();
6
7
  this.heartbeatInterval = null;
8
+ this.clients = new Map();
7
9
  if (!options.logger) {
8
10
  throw new Error('Logger is required');
9
11
  }
12
+ if (!options.handlers?.message) {
13
+ throw new Error('Message handler is required');
14
+ }
10
15
  this.options = {
11
16
  heartbeatInterval: options.heartbeatInterval || 30000,
12
17
  logger: options.logger,
13
- onMessage: options.onMessage || (() => { }),
14
- onError: options.onError || (() => { }),
15
- onClose: options.onClose || (() => { })
18
+ WebSocketImpl: options.WebSocketImpl || WebSocket,
19
+ handlers: {
20
+ message: options.handlers.message,
21
+ error: options.handlers.error || (() => { }),
22
+ close: options.handlers.close || (() => { })
23
+ }
16
24
  };
17
- this.wss = new WebSocketServer({ server });
25
+ this.wss = wss;
18
26
  this.setupServer();
19
27
  }
20
28
  setupServer() {
21
29
  this.wss.on('connection', (ws) => {
22
- const extWs = ws;
23
- extWs.subscriptions = new Set();
24
- extWs.isAlive = true;
25
- ws.on('message', async (data) => {
26
- try {
27
- const message = JSON.parse(data.toString());
28
- if (this.options.onMessage) {
29
- await this.options.onMessage(extWs, message);
30
- }
31
- }
32
- catch (error) {
33
- if (this.options.onError) {
34
- this.options.onError(ws, error);
35
- }
36
- }
37
- });
38
- ws.on('close', () => {
39
- extWs.isAlive = false;
40
- if (this.options.onClose) {
41
- this.options.onClose(ws);
42
- }
43
- });
44
- ws.on('error', (error) => {
45
- if (this.options.onError) {
46
- this.options.onError(ws, error);
47
- }
48
- });
30
+ this.handleConnection(ws);
49
31
  });
50
32
  if (this.options.heartbeatInterval && this.options.heartbeatInterval > 0) {
51
33
  this.startHeartbeat();
52
34
  }
53
35
  }
36
+ handleConnection(ws) {
37
+ ws.isAlive = true;
38
+ ws.subscriptions = new Set();
39
+ ws.clientId = ws.clientId || uuidv4();
40
+ this.clients.set(ws.clientId, ws);
41
+ ws.on('message', async (data) => {
42
+ try {
43
+ const message = JSON.parse(data.toString());
44
+ await this.options.handlers.message(ws, message);
45
+ }
46
+ catch (error) {
47
+ if (this.options.handlers.error) {
48
+ this.options.handlers.error(ws, error);
49
+ }
50
+ }
51
+ });
52
+ ws.on('close', () => {
53
+ if (ws.clientId) {
54
+ this.clients.delete(ws.clientId);
55
+ }
56
+ if (this.options.handlers.close) {
57
+ this.options.handlers.close(ws);
58
+ }
59
+ });
60
+ ws.on('error', (error) => {
61
+ if (ws.clientId) {
62
+ this.clients.delete(ws.clientId);
63
+ }
64
+ if (this.options.handlers.error) {
65
+ this.options.handlers.error(ws, error);
66
+ }
67
+ });
68
+ }
54
69
  startHeartbeat() {
55
70
  this.heartbeatInterval = setInterval(() => {
56
71
  this.wss.clients.forEach((ws) => {
72
+ const extWs = ws;
73
+ if (!extWs.isAlive) {
74
+ extWs.terminate();
75
+ return;
76
+ }
77
+ extWs.isAlive = false;
57
78
  if (ws.readyState === WebSocket.OPEN) {
58
- const extWs = ws;
59
- if (!extWs.isAlive) {
60
- return ws.terminate();
61
- }
62
79
  ws.ping();
63
- extWs.isAlive = false;
64
80
  }
65
81
  });
66
- }, this.options.heartbeatInterval || 30000);
82
+ }, this.options.heartbeatInterval);
67
83
  }
68
84
  broadcast(message) {
69
85
  this.wss.clients.forEach((client) => {
@@ -73,10 +89,10 @@ export class NostrWSServer extends EventEmitter {
73
89
  });
74
90
  }
75
91
  broadcastToChannel(channel, message) {
76
- this.wss.clients.forEach((client) => {
77
- const extClient = client;
78
- if (client.readyState === WebSocket.OPEN && extClient.subscriptions?.has(channel)) {
79
- client.send(JSON.stringify(message));
92
+ this.wss.clients.forEach((ws) => {
93
+ const extWs = ws;
94
+ if (extWs.readyState === WebSocket.OPEN && extWs.subscriptions?.has(channel)) {
95
+ ws.send(JSON.stringify(message));
80
96
  }
81
97
  });
82
98
  }
@@ -5,9 +5,12 @@ export interface NostrWSOptions {
5
5
  reconnectInterval?: number;
6
6
  maxReconnectAttempts?: number;
7
7
  logger: Logger;
8
- onMessage?: (ws: ExtendedWebSocket, message: NostrWSMessage) => Promise<void> | void;
9
- onError?: (ws: WebSocket, error: Error) => void;
10
- onClose?: (ws: WebSocket) => void;
8
+ WebSocketImpl: typeof WebSocket;
9
+ handlers: {
10
+ message: (ws: ExtendedWebSocket, message: NostrWSMessage) => Promise<void> | void;
11
+ error?: (ws: WebSocket, error: Error) => void;
12
+ close?: (ws: WebSocket) => void;
13
+ };
11
14
  }
12
15
  export interface NostrWSMessage {
13
16
  type: string;
@@ -33,6 +36,7 @@ export interface NostrWSServerEvents {
33
36
  export interface ExtendedWebSocket extends WebSocket {
34
37
  isAlive?: boolean;
35
38
  subscriptions?: Set<string>;
39
+ clientId?: string;
36
40
  messageQueue?: NostrWSMessage[];
37
41
  lastPing?: number;
38
42
  reconnectAttempts?: number;
@@ -49,5 +53,6 @@ export interface NostrWSConnectionState {
49
53
  export interface Logger {
50
54
  debug: (message: string, ...args: unknown[]) => void;
51
55
  info: (message: string, ...args: unknown[]) => void;
56
+ warn: (message: string, ...args: unknown[]) => void;
52
57
  error: (message: string, ...args: unknown[]) => void;
53
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-websocket-utils",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "WebSocket utilities for Nostr applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,20 +20,30 @@
20
20
  "utils",
21
21
  "maiqr"
22
22
  ],
23
- "author": "Human Java Enterprises",
23
+ "author": "vveerrgg",
24
24
  "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/HumanjavaEnterprises/nostr-websocket-utils.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/HumanjavaEnterprises/nostr-websocket-utils/issues"
31
+ },
32
+ "homepage": "https://github.com/HumanjavaEnterprises/nostr-websocket-utils#readme",
25
33
  "dependencies": {
26
- "ws": "^8.16.0",
27
- "winston": "^3.11.0",
28
34
  "@noble/curves": "^1.3.0",
29
35
  "@noble/hashes": "^1.3.3",
30
- "nostr-tools": "^2.1.4"
36
+ "@types/uuid": "^10.0.0",
37
+ "nostr-tools": "^2.1.4",
38
+ "uuid": "^11.0.3",
39
+ "winston": "^3.11.0",
40
+ "ws": "^8.16.0"
31
41
  },
32
42
  "peerDependencies": {
33
43
  "nostr-tools": "^2.1.4"
34
44
  },
35
45
  "devDependencies": {
36
- "@types/jest": "^29.5.11",
46
+ "@types/jest": "^29.5.14",
37
47
  "@types/node": "^20.11.0",
38
48
  "@types/ws": "^8.5.10",
39
49
  "@typescript-eslint/eslint-plugin": "^6.18.1",
@@ -48,14 +58,6 @@
48
58
  "README.md",
49
59
  "LICENSE"
50
60
  ],
51
- "repository": {
52
- "type": "git",
53
- "url": "git+https://github.com/humanjavaenterprises/nostr-websocket-utils.git"
54
- },
55
- "bugs": {
56
- "url": "https://github.com/humanjavaenterprises/nostr-websocket-utils/issues"
57
- },
58
- "homepage": "https://github.com/humanjavaenterprises/nostr-websocket-utils#readme",
59
61
  "publishConfig": {
60
62
  "access": "public"
61
63
  }