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
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.
|
|
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
|
-
|
|
13
|
-
throw new Error('Message handler is required');
|
|
14
|
-
}
|
|
17
|
+
this.wss = wss;
|
|
15
18
|
this.options = {
|
|
16
|
-
heartbeatInterval:
|
|
19
|
+
heartbeatInterval: 30000,
|
|
17
20
|
logger: options.logger,
|
|
18
|
-
WebSocketImpl:
|
|
21
|
+
WebSocketImpl: WebSocket,
|
|
22
|
+
...options,
|
|
19
23
|
handlers: {
|
|
20
|
-
message:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
34
|
+
const extWs = ws;
|
|
35
|
+
this.handleConnection(extWs);
|
|
31
36
|
});
|
|
32
|
-
if (this.options.heartbeatInterval
|
|
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 =
|
|
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
|
-
|
|
50
|
+
if (this.options.handlers?.message) {
|
|
51
|
+
await this.options.handlers.message(ws, message);
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
catch (error) {
|
|
47
|
-
|
|
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
|
|
65
|
+
if (this.options.handlers?.close) {
|
|
57
66
|
this.options.handlers.close(ws);
|
|
58
67
|
}
|
|
59
68
|
});
|
|
60
69
|
ws.on('error', (error) => {
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Base transport layer for Nostr connections
|
|
3
|
+
* @module transport/base
|
|
4
|
+
*/
|
|
5
|
+
/// <reference types="node" />
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import type { MetricsProvider, ScoringStrategy } from '../types/scoring';
|
|
8
|
+
export interface TransportOptions {
|
|
9
|
+
metricsProvider?: MetricsProvider;
|
|
10
|
+
scoringStrategy?: ScoringStrategy;
|
|
11
|
+
metricsEnabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare abstract class BaseTransport extends EventEmitter {
|
|
14
|
+
protected scoringStrategy: ScoringStrategy;
|
|
15
|
+
protected metricsProvider?: MetricsProvider;
|
|
16
|
+
protected metricsEnabled: boolean;
|
|
17
|
+
constructor(options?: TransportOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Create a default metrics provider if none is supplied
|
|
20
|
+
*/
|
|
21
|
+
protected createDefaultMetricsProvider(): MetricsProvider;
|
|
22
|
+
/**
|
|
23
|
+
* Track a metric update
|
|
24
|
+
*/
|
|
25
|
+
protected trackMetric(endpoint: string, metricType: string, value: any): void;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the score for an endpoint
|
|
28
|
+
* @param endpoint - Endpoint to get score for
|
|
29
|
+
* @returns {number} Score between 0 and 100
|
|
30
|
+
*/
|
|
31
|
+
getScore(endpoint: string): number;
|
|
32
|
+
/**
|
|
33
|
+
* Gets scores for all endpoints
|
|
34
|
+
* @returns {Map<string, number>} Map of endpoint scores
|
|
35
|
+
*/
|
|
36
|
+
getAllScores(): Map<string, number>;
|
|
37
|
+
/**
|
|
38
|
+
* Enable/disable metrics
|
|
39
|
+
*/
|
|
40
|
+
setMetricsEnabled(enabled: boolean): void;
|
|
41
|
+
/**
|
|
42
|
+
* Updates the scoring strategy
|
|
43
|
+
* @param strategy - New scoring strategy
|
|
44
|
+
*/
|
|
45
|
+
updateScoringStrategy(strategy: ScoringStrategy): void;
|
|
46
|
+
/**
|
|
47
|
+
* Updates the metrics provider
|
|
48
|
+
* @param provider - New metrics provider
|
|
49
|
+
*/
|
|
50
|
+
updateMetricsProvider(provider?: MetricsProvider): void;
|
|
51
|
+
abstract connect(endpoint: string): Promise<void>;
|
|
52
|
+
abstract disconnect(endpoint: string): Promise<void>;
|
|
53
|
+
abstract send(endpoint: string, data: any): Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Base transport layer for Nostr connections
|
|
3
|
+
* @module transport/base
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
import { getLogger } from '../utils/logger';
|
|
7
|
+
import { DefaultScoringStrategy } from '../types/scoring';
|
|
8
|
+
const logger = getLogger('BaseTransport');
|
|
9
|
+
export class BaseTransport extends EventEmitter {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
super();
|
|
12
|
+
this.scoringStrategy = options.scoringStrategy ?? new DefaultScoringStrategy();
|
|
13
|
+
this.metricsProvider = options.metricsProvider;
|
|
14
|
+
this.metricsEnabled = !!this.metricsProvider;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a default metrics provider if none is supplied
|
|
18
|
+
*/
|
|
19
|
+
createDefaultMetricsProvider() {
|
|
20
|
+
return {
|
|
21
|
+
getMetrics: () => ({
|
|
22
|
+
totalConnections: 0,
|
|
23
|
+
activeConnections: 0,
|
|
24
|
+
connectionErrors: 0,
|
|
25
|
+
messagesReceived: 0,
|
|
26
|
+
messagesSent: 0,
|
|
27
|
+
bytesReceived: 0,
|
|
28
|
+
bytesSent: 0,
|
|
29
|
+
averageLatency: 0,
|
|
30
|
+
lastSeen: Date.now()
|
|
31
|
+
}),
|
|
32
|
+
getAllMetrics: () => new Map(),
|
|
33
|
+
trackMetric: () => { }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Track a metric update
|
|
38
|
+
*/
|
|
39
|
+
trackMetric(endpoint, metricType, value) {
|
|
40
|
+
if (!this.metricsEnabled)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
this.metricsProvider?.trackMetric(endpoint, metricType, value);
|
|
44
|
+
const event = {
|
|
45
|
+
endpoint,
|
|
46
|
+
metricType,
|
|
47
|
+
value,
|
|
48
|
+
timestamp: Date.now()
|
|
49
|
+
};
|
|
50
|
+
this.emit('metric', event);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger.error({ error, endpoint, metricType }, 'Error tracking metric');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gets the score for an endpoint
|
|
58
|
+
* @param endpoint - Endpoint to get score for
|
|
59
|
+
* @returns {number} Score between 0 and 100
|
|
60
|
+
*/
|
|
61
|
+
getScore(endpoint) {
|
|
62
|
+
if (!this.metricsEnabled)
|
|
63
|
+
return 100;
|
|
64
|
+
const metrics = this.metricsProvider?.getMetrics(endpoint);
|
|
65
|
+
if (!metrics)
|
|
66
|
+
return 100;
|
|
67
|
+
return this.scoringStrategy.calculateScore(metrics);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Gets scores for all endpoints
|
|
71
|
+
* @returns {Map<string, number>} Map of endpoint scores
|
|
72
|
+
*/
|
|
73
|
+
getAllScores() {
|
|
74
|
+
if (!this.metricsEnabled || !this.metricsProvider)
|
|
75
|
+
return new Map();
|
|
76
|
+
const scores = new Map();
|
|
77
|
+
const allMetrics = this.metricsProvider.getAllMetrics();
|
|
78
|
+
for (const [endpoint, metrics] of allMetrics) {
|
|
79
|
+
scores.set(endpoint, this.scoringStrategy.calculateScore(metrics));
|
|
80
|
+
}
|
|
81
|
+
return scores;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Enable/disable metrics
|
|
85
|
+
*/
|
|
86
|
+
setMetricsEnabled(enabled) {
|
|
87
|
+
this.metricsEnabled = enabled;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Updates the scoring strategy
|
|
91
|
+
* @param strategy - New scoring strategy
|
|
92
|
+
*/
|
|
93
|
+
updateScoringStrategy(strategy) {
|
|
94
|
+
this.scoringStrategy = strategy;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Updates the metrics provider
|
|
98
|
+
* @param provider - New metrics provider
|
|
99
|
+
*/
|
|
100
|
+
updateMetricsProvider(provider) {
|
|
101
|
+
this.metricsProvider = provider;
|
|
102
|
+
this.metricsEnabled = !!provider;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocket transport implementation
|
|
3
|
+
* @module transport/websocket
|
|
4
|
+
*/
|
|
5
|
+
import { BaseTransport } from './base';
|
|
6
|
+
import type { TransportOptions } from './base';
|
|
7
|
+
export declare class WebSocketTransport extends BaseTransport {
|
|
8
|
+
private connections;
|
|
9
|
+
constructor(options?: TransportOptions);
|
|
10
|
+
connect(endpoint: string): Promise<void>;
|
|
11
|
+
disconnect(endpoint: string): Promise<void>;
|
|
12
|
+
send(endpoint: string, data: any): Promise<void>;
|
|
13
|
+
private checkEndpointHealth;
|
|
14
|
+
/**
|
|
15
|
+
* Get all active WebSocket connections
|
|
16
|
+
*/
|
|
17
|
+
getActiveConnections(): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Check if connected to endpoint
|
|
20
|
+
*/
|
|
21
|
+
isConnected(endpoint: string): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocket transport implementation
|
|
3
|
+
* @module transport/websocket
|
|
4
|
+
*/
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { BaseTransport } from './base';
|
|
7
|
+
import { getLogger } from '../utils/logger';
|
|
8
|
+
const logger = getLogger('WebSocketTransport');
|
|
9
|
+
export class WebSocketTransport extends BaseTransport {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.connections = new Map();
|
|
13
|
+
}
|
|
14
|
+
async connect(endpoint) {
|
|
15
|
+
if (this.connections.has(endpoint)) {
|
|
16
|
+
logger.warn({ endpoint }, 'Already connected to endpoint');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const ws = new WebSocket(endpoint);
|
|
21
|
+
ws.on('open', () => {
|
|
22
|
+
this.trackMetric(endpoint, 'connection', true);
|
|
23
|
+
this.trackMetric(endpoint, 'openHandshake', Date.now());
|
|
24
|
+
});
|
|
25
|
+
ws.on('close', () => {
|
|
26
|
+
this.trackMetric(endpoint, 'connection', false);
|
|
27
|
+
this.trackMetric(endpoint, 'closeHandshake', Date.now());
|
|
28
|
+
this.connections.delete(endpoint);
|
|
29
|
+
});
|
|
30
|
+
ws.on('message', (data) => {
|
|
31
|
+
const length = data instanceof Buffer ? data.length :
|
|
32
|
+
data instanceof ArrayBuffer ? data.byteLength :
|
|
33
|
+
data instanceof Array ? data.reduce((acc, buf) => acc + buf.length, 0) : 0;
|
|
34
|
+
this.trackMetric(endpoint, 'messageReceived', length);
|
|
35
|
+
});
|
|
36
|
+
ws.on('error', (error) => {
|
|
37
|
+
this.trackMetric(endpoint, 'error', error);
|
|
38
|
+
logger.error({ error, endpoint }, 'WebSocket error');
|
|
39
|
+
});
|
|
40
|
+
// Track ping/pong for latency
|
|
41
|
+
ws.on('ping', () => {
|
|
42
|
+
this.trackMetric(endpoint, 'ping', Date.now());
|
|
43
|
+
});
|
|
44
|
+
ws.on('pong', () => {
|
|
45
|
+
if (!this.metricsEnabled || !this.metricsProvider)
|
|
46
|
+
return;
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const metrics = this.metricsProvider.getMetrics(endpoint);
|
|
49
|
+
if (!metrics || !metrics.lastSeen)
|
|
50
|
+
return;
|
|
51
|
+
this.trackMetric(endpoint, 'latency', now - metrics.lastSeen);
|
|
52
|
+
});
|
|
53
|
+
this.connections.set(endpoint, ws);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.trackMetric(endpoint, 'connectionError', error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async disconnect(endpoint) {
|
|
61
|
+
const ws = this.connections.get(endpoint);
|
|
62
|
+
if (!ws)
|
|
63
|
+
return;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
ws.close();
|
|
66
|
+
ws.on('close', () => {
|
|
67
|
+
this.connections.delete(endpoint);
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
ws.on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async send(endpoint, data) {
|
|
74
|
+
const ws = this.connections.get(endpoint);
|
|
75
|
+
if (!ws)
|
|
76
|
+
throw new Error(`Not connected to ${endpoint}`);
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
ws.send(data, (error) => {
|
|
79
|
+
if (error) {
|
|
80
|
+
this.trackMetric(endpoint, 'sendError', error);
|
|
81
|
+
reject(error);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.trackMetric(endpoint, 'messageSent', data.length);
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async checkEndpointHealth(endpoint) {
|
|
91
|
+
try {
|
|
92
|
+
if (!this.metricsEnabled || !this.metricsProvider) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const metrics = this.metricsProvider.getMetrics(endpoint);
|
|
96
|
+
if (!metrics || !metrics.lastSeen) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const elapsed = now - metrics.lastSeen;
|
|
101
|
+
// If we haven't seen a ping response in 30 seconds, consider unhealthy
|
|
102
|
+
return elapsed < 30000;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.error('Error checking endpoint health:', error);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all active WebSocket connections
|
|
111
|
+
*/
|
|
112
|
+
getActiveConnections() {
|
|
113
|
+
return Array.from(this.connections.keys());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if connected to endpoint
|
|
117
|
+
*/
|
|
118
|
+
isConnected(endpoint) {
|
|
119
|
+
const ws = this.connections.get(endpoint);
|
|
120
|
+
return ws?.readyState === WebSocket.OPEN;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Event type definitions
|
|
3
|
+
* @module types/events
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Base Nostr event interface following NIP-01 specification
|
|
7
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/01.md
|
|
8
|
+
*/
|
|
9
|
+
export interface NostrEvent {
|
|
10
|
+
/** Event ID in hex format */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Public key of the event creator in hex format */
|
|
13
|
+
pubkey: string;
|
|
14
|
+
/** Unix timestamp in seconds */
|
|
15
|
+
created_at: number;
|
|
16
|
+
/** Event kind number */
|
|
17
|
+
kind: number;
|
|
18
|
+
/** Array of tags */
|
|
19
|
+
tags: string[][];
|
|
20
|
+
/** Event content */
|
|
21
|
+
content: string;
|
|
22
|
+
/** Signature of the event data in hex format */
|
|
23
|
+
sig: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Signed Nostr event with id and signature
|
|
27
|
+
* @extends NostrEvent
|
|
28
|
+
*/
|
|
29
|
+
export interface SignedNostrEvent extends NostrEvent {
|
|
30
|
+
/** Event ID in hex format */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Signature of the event data in hex format */
|
|
33
|
+
sig: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Event validation result
|
|
37
|
+
*/
|
|
38
|
+
export interface NostrEventValidationResult {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Event subscription filter
|
|
44
|
+
*/
|
|
45
|
+
export interface NostrEventFilter {
|
|
46
|
+
ids?: string[];
|
|
47
|
+
authors?: string[];
|
|
48
|
+
kinds?: number[];
|
|
49
|
+
since?: number;
|
|
50
|
+
until?: number;
|
|
51
|
+
limit?: number;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Nostr subscription event interface
|
|
56
|
+
* Used for creating subscription messages to relays
|
|
57
|
+
*/
|
|
58
|
+
export interface NostrSubscriptionEvent {
|
|
59
|
+
/** Subscription ID */
|
|
60
|
+
subscriptionId: string;
|
|
61
|
+
/** Array of filters */
|
|
62
|
+
filters: NostrEventFilter[];
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Filter type definitions
|
|
3
|
+
* @module types/filters
|
|
4
|
+
*/
|
|
5
|
+
import { NostrEventFilter } from './events';
|
|
6
|
+
/**
|
|
7
|
+
* Subscription request filter
|
|
8
|
+
*/
|
|
9
|
+
export interface NostrSubscriptionFilter extends NostrEventFilter {
|
|
10
|
+
subscriptionId?: string;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Filter validation result
|
|
15
|
+
*/
|
|
16
|
+
export interface NostrFilterValidationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Handler type definitions for WebSocket events
|
|
3
|
+
* @module types/handlers
|
|
4
|
+
*/
|
|
5
|
+
import type { WebSocket } from 'ws';
|
|
6
|
+
import type { NostrWSMessage } from './messages';
|
|
7
|
+
import type { ExtendedWebSocket } from './websocket';
|
|
8
|
+
/**
|
|
9
|
+
* Events emitted by the NostrWSClient
|
|
10
|
+
* @interface NostrWSClientEvents
|
|
11
|
+
*/
|
|
12
|
+
export interface NostrWSClientEvents {
|
|
13
|
+
/**
|
|
14
|
+
* Emitted when the client connects to the relay
|
|
15
|
+
*/
|
|
16
|
+
connect: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Emitted when the client disconnects from the relay
|
|
19
|
+
*/
|
|
20
|
+
disconnect: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Emitted when the client reconnects to the relay
|
|
23
|
+
*/
|
|
24
|
+
reconnect: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Emitted when a message is received from the relay
|
|
27
|
+
* @param message - The received message
|
|
28
|
+
*/
|
|
29
|
+
message: (message: NostrWSMessage) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Emitted when an error occurs
|
|
32
|
+
* @param error - The error object
|
|
33
|
+
*/
|
|
34
|
+
error: (error: Error) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Events emitted by the NostrWSServer
|
|
38
|
+
* @interface NostrWSServerEvents
|
|
39
|
+
*/
|
|
40
|
+
export interface NostrWSServerEvents {
|
|
41
|
+
/**
|
|
42
|
+
* Emitted when a client connects to the server
|
|
43
|
+
* @param client - The connected client
|
|
44
|
+
*/
|
|
45
|
+
connection: (client: ExtendedWebSocket) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Emitted when a message is received from a client
|
|
48
|
+
* @param message - The received message
|
|
49
|
+
* @param client - The client that sent the message
|
|
50
|
+
*/
|
|
51
|
+
message: (message: NostrWSMessage, client: ExtendedWebSocket) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Emitted when an error occurs
|
|
54
|
+
* @param error - The error object
|
|
55
|
+
*/
|
|
56
|
+
error: (error: Error) => void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Configuration options for WebSocket handlers
|
|
60
|
+
* @interface NostrWSHandlers
|
|
61
|
+
*/
|
|
62
|
+
export interface NostrWSHandlers {
|
|
63
|
+
/**
|
|
64
|
+
* Handler for incoming messages
|
|
65
|
+
* @param ws - The WebSocket instance
|
|
66
|
+
* @param message - The received message
|
|
67
|
+
*/
|
|
68
|
+
message: (ws: ExtendedWebSocket, message: NostrWSMessage) => Promise<void> | void;
|
|
69
|
+
/**
|
|
70
|
+
* Handler for WebSocket errors
|
|
71
|
+
* @param ws - The WebSocket instance
|
|
72
|
+
* @param error - The error object
|
|
73
|
+
*/
|
|
74
|
+
error?: (ws: WebSocket, error: Error) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Handler for WebSocket connection close
|
|
77
|
+
* @param ws - The WebSocket instance
|
|
78
|
+
*/
|
|
79
|
+
close?: (ws: WebSocket) => void;
|
|
80
|
+
}
|