@olane/o-node 0.7.55 → 0.7.57
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/dist/src/connection/enums/o-node-message-event.d.ts +14 -0
- package/dist/src/connection/enums/o-node-message-event.d.ts.map +1 -0
- package/dist/src/connection/enums/o-node-message-event.js +5 -0
- package/dist/src/connection/index.d.ts +0 -3
- package/dist/src/connection/index.d.ts.map +1 -1
- package/dist/src/connection/index.js +0 -3
- package/dist/src/connection/interfaces/abort-signal.config.d.ts +5 -0
- package/dist/src/connection/interfaces/abort-signal.config.d.ts.map +1 -0
- package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts +13 -1
- package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
- package/dist/src/connection/interfaces/o-node-connection.config.d.ts +18 -5
- package/dist/src/connection/interfaces/o-node-connection.config.d.ts.map +1 -1
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts +3 -11
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.d.ts +29 -53
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +102 -149
- package/dist/src/connection/o-node-connection.manager.d.ts +12 -8
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +49 -35
- package/dist/src/connection/o-node-stream.d.ts +48 -15
- package/dist/src/connection/o-node-stream.d.ts.map +1 -1
- package/dist/src/connection/o-node-stream.js +143 -31
- package/dist/src/connection/stream-handler.config.d.ts +0 -11
- package/dist/src/connection/stream-handler.config.d.ts.map +1 -1
- package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts +9 -0
- package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts.map +1 -0
- package/dist/src/lib/interfaces/o-node-request-manager.config.js +1 -0
- package/dist/src/lib/o-node-request-manager.d.ts +46 -0
- package/dist/src/lib/o-node-request-manager.d.ts.map +1 -0
- package/dist/src/lib/o-node-request-manager.js +173 -0
- package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
- package/dist/src/managers/o-reconnection.manager.js +4 -0
- package/dist/src/managers/o-registration.manager.d.ts.map +1 -1
- package/dist/src/managers/o-registration.manager.js +9 -4
- package/dist/src/o-node.d.ts +6 -7
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +22 -37
- package/dist/src/o-node.tool.d.ts +3 -3
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +28 -56
- package/dist/src/router/o-node.router.d.ts.map +1 -1
- package/dist/src/router/o-node.router.js +4 -2
- package/dist/test/connection-management.spec.js +3 -0
- package/package.json +7 -7
- package/dist/src/connection/interfaces/stream-init-message.d.ts +0 -64
- package/dist/src/connection/interfaces/stream-init-message.d.ts.map +0 -1
- package/dist/src/connection/interfaces/stream-init-message.js +0 -18
- package/dist/src/connection/interfaces/stream-manager.config.d.ts +0 -8
- package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +0 -1
- package/dist/src/connection/o-node-stream.manager.d.ts +0 -210
- package/dist/src/connection/o-node-stream.manager.d.ts.map +0 -1
- package/dist/src/connection/o-node-stream.manager.js +0 -696
- /package/dist/src/connection/interfaces/{stream-manager.config.js → abort-signal.config.js} +0 -0
|
@@ -1,696 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { oObject, oError, oErrorCodes, oRequest, oResponse, CoreUtils, ResponseBuilder, } from '@olane/o-core';
|
|
3
|
-
import { oNodeStream } from './o-node-stream.js';
|
|
4
|
-
import { StreamManagerEvent, } from './stream-manager.events.js';
|
|
5
|
-
import { isStreamInitMessage, } from './interfaces/stream-init-message.js';
|
|
6
|
-
import { lpStream } from '@olane/o-config';
|
|
7
|
-
import JSON5 from 'json5';
|
|
8
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
-
/**
|
|
10
|
-
* oNodeStreamManager handles the lifecycle and tracking of streams for a single connection.
|
|
11
|
-
* Features:
|
|
12
|
-
* - Tracks all streams by ID
|
|
13
|
-
* - Creates new streams on demand
|
|
14
|
-
* - Manages stream lifecycle (cleanup after use)
|
|
15
|
-
* - Provides events for monitoring
|
|
16
|
-
* - No reuse at base layer (extensible in subclasses)
|
|
17
|
-
*/
|
|
18
|
-
export class oNodeStreamManager extends oObject {
|
|
19
|
-
constructor(config) {
|
|
20
|
-
const id = uuidv4();
|
|
21
|
-
super('id:' + id);
|
|
22
|
-
this.config = config;
|
|
23
|
-
this.streams = new Map();
|
|
24
|
-
this.eventEmitter = new EventEmitter();
|
|
25
|
-
this.isInitialized = false;
|
|
26
|
-
this.activeStreamHandlers = new Map();
|
|
27
|
-
this.streamMonitoringIntervals = new Map(); // Track monitoring intervals
|
|
28
|
-
this.id = id;
|
|
29
|
-
this.p2pConnection = config.p2pConnection;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Initialize the stream manager
|
|
33
|
-
*/
|
|
34
|
-
async initialize() {
|
|
35
|
-
if (this.isInitialized) {
|
|
36
|
-
this.logger.debug('Stream manager already initialized');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
// Stream manager is now initialized and ready to handle streams
|
|
41
|
-
this.isInitialized = true;
|
|
42
|
-
this.emit(StreamManagerEvent.ManagerInitialized, {});
|
|
43
|
-
this.logger.info('Stream manager initialized', {
|
|
44
|
-
remotePeer: this.p2pConnection.remotePeer.toString(),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
this.logger.error('Failed to initialize stream manager:', error);
|
|
49
|
-
throw new oError(oErrorCodes.INTERNAL_ERROR, `Failed to initialize stream manager: ${error.message}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Gets or creates a stream for the connection
|
|
54
|
-
* Always creates a new stream (no reuse at base layer)
|
|
55
|
-
*
|
|
56
|
-
* For limited connections (where caller has identified a reader stream):
|
|
57
|
-
* - If callerReaderStream exists, wrap and return it for sending requests
|
|
58
|
-
* - Otherwise, create new stream as normal
|
|
59
|
-
*
|
|
60
|
-
* @param protocol - The protocol string for the stream
|
|
61
|
-
* @param remoteAddress - The remote address for the stream
|
|
62
|
-
* @param config - Stream handler configuration
|
|
63
|
-
* @returns Wrapped stream
|
|
64
|
-
*/
|
|
65
|
-
async getOrCreateStream(protocol, remoteAddress, config = {}) {
|
|
66
|
-
this.logger.debug('Getting or creating stream', this.callerReaderStream?.protocol, 'and status', this.callerReaderStream?.status, 'json:', JSON.stringify(this.callerReaderStream));
|
|
67
|
-
// If we have a caller's reader stream (from limited connection), use it for sending requests
|
|
68
|
-
if (this.callerReaderStream && this.callerReaderStream.status === 'open') {
|
|
69
|
-
this.logger.debug('Using caller reader stream for limited connection', {
|
|
70
|
-
streamId: this.callerReaderStream.id,
|
|
71
|
-
});
|
|
72
|
-
// TODO: figure out why this would cause the node stream to be closed?
|
|
73
|
-
// Wrap the reader stream for use (if not already wrapped)
|
|
74
|
-
// const existingWrapped = Array.from(this.streams.values()).find(
|
|
75
|
-
// (s) => s.p2pStream.id === this.callerReaderStream!.id,
|
|
76
|
-
// );
|
|
77
|
-
// if (existingWrapped) {
|
|
78
|
-
// return existingWrapped;
|
|
79
|
-
// }
|
|
80
|
-
// Wrap the reader stream
|
|
81
|
-
const wrappedStream = new oNodeStream(this.callerReaderStream, {
|
|
82
|
-
direction: 'inbound', // It's inbound to us, we write to it
|
|
83
|
-
reusePolicy: 'reuse',
|
|
84
|
-
remoteAddress: remoteAddress,
|
|
85
|
-
streamType: 'request-response',
|
|
86
|
-
});
|
|
87
|
-
// this.streams.set(this.callerReaderStream.id, wrappedStream);
|
|
88
|
-
return wrappedStream;
|
|
89
|
-
}
|
|
90
|
-
// Always create new stream (no reuse at base layer)
|
|
91
|
-
return await this.createStream(protocol, remoteAddress, config);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Creates a new stream from the connection
|
|
95
|
-
*
|
|
96
|
-
* @param protocol - The protocol string for the stream
|
|
97
|
-
* @param remoteAddress - The remote address for the stream
|
|
98
|
-
* @param config - Stream handler configuration
|
|
99
|
-
* @returns Wrapped stream
|
|
100
|
-
*/
|
|
101
|
-
async createStream(protocol, remoteAddress, config = {}) {
|
|
102
|
-
this.logger.debug('Creating new stream', {
|
|
103
|
-
protocol,
|
|
104
|
-
currentStreamCount: this.streams.size,
|
|
105
|
-
});
|
|
106
|
-
// Create stream from libp2p connection
|
|
107
|
-
const stream = await this.p2pConnection.newStream(protocol, {
|
|
108
|
-
signal: config.signal,
|
|
109
|
-
maxOutboundStreams: config.maxOutboundStreams ?? 1000,
|
|
110
|
-
runOnLimitedConnection: config.runOnLimitedConnection ?? true,
|
|
111
|
-
});
|
|
112
|
-
// Wrap in oNodeStream with metadata
|
|
113
|
-
const wrappedStream = new oNodeStream(stream, {
|
|
114
|
-
direction: 'outbound',
|
|
115
|
-
reusePolicy: 'none', // Always none at base layer
|
|
116
|
-
remoteAddress: remoteAddress,
|
|
117
|
-
streamType: 'request-response',
|
|
118
|
-
});
|
|
119
|
-
// Track the stream
|
|
120
|
-
this.streams.set(stream.id, wrappedStream);
|
|
121
|
-
this.logger.debug('Stream created', {
|
|
122
|
-
streamId: stream.id,
|
|
123
|
-
protocol,
|
|
124
|
-
totalStreams: this.streams.size,
|
|
125
|
-
});
|
|
126
|
-
return wrappedStream;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Releases and cleans up a stream
|
|
130
|
-
*
|
|
131
|
-
* @param streamId - The ID of the stream to release
|
|
132
|
-
*/
|
|
133
|
-
async releaseStream(streamId) {
|
|
134
|
-
const wrappedStream = this.streams.get(streamId);
|
|
135
|
-
if (!wrappedStream) {
|
|
136
|
-
this.logger.debug('Stream not found for release', { streamId });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
this.logger.debug('Releasing stream', { streamId });
|
|
140
|
-
// Remove from tracking
|
|
141
|
-
this.streams.delete(streamId);
|
|
142
|
-
// Close the stream if still open
|
|
143
|
-
if (wrappedStream.p2pStream.status === 'open') {
|
|
144
|
-
try {
|
|
145
|
-
await wrappedStream.p2pStream.close();
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
this.logger.debug('Error closing stream during release', {
|
|
149
|
-
streamId,
|
|
150
|
-
error: error.message,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
this.logger.debug('Stream released', {
|
|
155
|
-
streamId,
|
|
156
|
-
remainingStreams: this.streams.size,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Gets all tracked streams
|
|
161
|
-
*
|
|
162
|
-
* @returns Array of wrapped streams
|
|
163
|
-
*/
|
|
164
|
-
getAllStreams() {
|
|
165
|
-
return Array.from(this.streams.values());
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Gets a stream by its ID
|
|
169
|
-
* Checks persistent caller streams (reader/writer) and tracked streams
|
|
170
|
-
*
|
|
171
|
-
* @param streamId - The ID of the stream to retrieve
|
|
172
|
-
* @returns The libp2p Stream or undefined if not found
|
|
173
|
-
*/
|
|
174
|
-
getStreamById(streamId) {
|
|
175
|
-
// Check caller writer stream
|
|
176
|
-
if (this.callerWriterStream?.id === streamId) {
|
|
177
|
-
return this.callerWriterStream;
|
|
178
|
-
}
|
|
179
|
-
// Check caller reader stream
|
|
180
|
-
if (this.callerReaderStream?.id === streamId) {
|
|
181
|
-
return this.callerReaderStream;
|
|
182
|
-
}
|
|
183
|
-
// Check tracked streams
|
|
184
|
-
const wrappedStream = this.streams.get(streamId);
|
|
185
|
-
return wrappedStream?.p2pStream;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Sets up monitoring for stream closure and emits events when detected
|
|
189
|
-
* Periodically checks stream status and cleans up when stream closes
|
|
190
|
-
*
|
|
191
|
-
* @param stream - The stream to monitor
|
|
192
|
-
* @param role - The role of the stream ('reader' or 'writer')
|
|
193
|
-
*/
|
|
194
|
-
setupStreamCloseMonitoring(stream, role) {
|
|
195
|
-
const streamId = stream.id;
|
|
196
|
-
// Clear any existing monitoring for this stream
|
|
197
|
-
const existingInterval = this.streamMonitoringIntervals.get(streamId);
|
|
198
|
-
if (existingInterval) {
|
|
199
|
-
clearInterval(existingInterval);
|
|
200
|
-
}
|
|
201
|
-
// Check stream status every 5 seconds
|
|
202
|
-
const interval = setInterval(() => {
|
|
203
|
-
if (stream.status !== 'open') {
|
|
204
|
-
this.logger.info(`Caller ${role} stream closed`, {
|
|
205
|
-
streamId,
|
|
206
|
-
status: stream.status,
|
|
207
|
-
role,
|
|
208
|
-
});
|
|
209
|
-
// Emit stream closed event
|
|
210
|
-
this.emit(StreamManagerEvent.StreamClosed, {
|
|
211
|
-
streamId,
|
|
212
|
-
role,
|
|
213
|
-
status: stream.status,
|
|
214
|
-
});
|
|
215
|
-
// Clear the stream reference
|
|
216
|
-
if (role === 'reader') {
|
|
217
|
-
this.callerReaderStream = undefined;
|
|
218
|
-
this.logger.info('Limited connection reader stream closed, will create new streams for requests');
|
|
219
|
-
}
|
|
220
|
-
else if (role === 'writer') {
|
|
221
|
-
this.callerWriterStream = undefined;
|
|
222
|
-
this.logger.info('Limited connection writer stream closed, responses may be affected');
|
|
223
|
-
}
|
|
224
|
-
// Stop monitoring this stream
|
|
225
|
-
clearInterval(interval);
|
|
226
|
-
this.streamMonitoringIntervals.delete(streamId);
|
|
227
|
-
}
|
|
228
|
-
}, 5000);
|
|
229
|
-
// Track the interval for cleanup
|
|
230
|
-
this.streamMonitoringIntervals.set(streamId, interval);
|
|
231
|
-
this.logger.debug(`Started monitoring ${role} stream`, { streamId });
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Emits an async event and waits for the first listener to return a result
|
|
235
|
-
* This enables event-based request handling with async responses
|
|
236
|
-
*/
|
|
237
|
-
async emitAsync(event, data) {
|
|
238
|
-
const listeners = this.eventEmitter.listeners(event);
|
|
239
|
-
if (listeners.length === 0) {
|
|
240
|
-
throw new oError(oErrorCodes.INTERNAL_ERROR, `No listener registered for event: ${event}`);
|
|
241
|
-
}
|
|
242
|
-
// Call the first listener and await its response
|
|
243
|
-
const listener = listeners[0];
|
|
244
|
-
return await listener(data);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Detects if a decoded message is a request
|
|
248
|
-
* Requests have a 'method' field and no 'result' field
|
|
249
|
-
*/
|
|
250
|
-
isRequest(message) {
|
|
251
|
-
return typeof message?.method === 'string' && message.result === undefined;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Detects if a decoded message is a response
|
|
255
|
-
* Responses have a 'result' field and no 'method' field
|
|
256
|
-
*/
|
|
257
|
-
isResponse(message) {
|
|
258
|
-
return message?.result !== undefined && message.method === undefined;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Detects if a decoded message is a stream initialization message
|
|
262
|
-
* Uses the imported type guard from stream-init-message.ts
|
|
263
|
-
*/
|
|
264
|
-
isStreamInit(message) {
|
|
265
|
-
return isStreamInitMessage(message);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Handles a stream initialization message
|
|
269
|
-
* Stores reference to caller's reader stream for bidirectional communication
|
|
270
|
-
* Sends acknowledgment back to confirm stream registration
|
|
271
|
-
*
|
|
272
|
-
* @param message - The decoded stream init message
|
|
273
|
-
* @param stream - The stream that sent the message
|
|
274
|
-
*/
|
|
275
|
-
async handleStreamInitMessage(message, stream) {
|
|
276
|
-
try {
|
|
277
|
-
if (message.role === 'reader') {
|
|
278
|
-
this.callerReaderStream = stream;
|
|
279
|
-
this.logger.info('Identified caller reader stream', {
|
|
280
|
-
streamId: stream.id,
|
|
281
|
-
connectionId: message.connectionId,
|
|
282
|
-
});
|
|
283
|
-
this.emit(StreamManagerEvent.StreamIdentified, {
|
|
284
|
-
streamId: stream.id,
|
|
285
|
-
role: message.role,
|
|
286
|
-
connectionId: message.connectionId,
|
|
287
|
-
});
|
|
288
|
-
// Set up monitoring for reader stream closure
|
|
289
|
-
this.setupStreamCloseMonitoring(stream, 'reader');
|
|
290
|
-
}
|
|
291
|
-
else if (message.role === 'writer') {
|
|
292
|
-
this.callerWriterStream = stream;
|
|
293
|
-
this.logger.info('Identified caller writer stream', {
|
|
294
|
-
streamId: stream.id,
|
|
295
|
-
connectionId: message.connectionId,
|
|
296
|
-
});
|
|
297
|
-
this.emit(StreamManagerEvent.StreamIdentified, {
|
|
298
|
-
streamId: stream.id,
|
|
299
|
-
role: message.role,
|
|
300
|
-
connectionId: message.connectionId,
|
|
301
|
-
});
|
|
302
|
-
// Set up monitoring for writer stream closure
|
|
303
|
-
this.setupStreamCloseMonitoring(stream, 'writer');
|
|
304
|
-
}
|
|
305
|
-
// Send acknowledgment back to caller
|
|
306
|
-
const ackMessage = {
|
|
307
|
-
type: 'stream-init-ack',
|
|
308
|
-
status: 'success',
|
|
309
|
-
streamId: stream.id,
|
|
310
|
-
role: message.role,
|
|
311
|
-
timestamp: Date.now(),
|
|
312
|
-
};
|
|
313
|
-
const ackBytes = new TextEncoder().encode(JSON.stringify(ackMessage));
|
|
314
|
-
await this.sendLengthPrefixed(stream, ackBytes, {});
|
|
315
|
-
this.logger.debug('Sent stream-init-ack', {
|
|
316
|
-
streamId: stream.id,
|
|
317
|
-
role: message.role,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
this.logger.error('Failed to process stream-init message', error);
|
|
322
|
-
// Try to send error acknowledgment
|
|
323
|
-
try {
|
|
324
|
-
const errorAck = {
|
|
325
|
-
type: 'stream-init-ack',
|
|
326
|
-
status: 'error',
|
|
327
|
-
streamId: stream.id,
|
|
328
|
-
role: message.role,
|
|
329
|
-
error: error.message,
|
|
330
|
-
timestamp: Date.now(),
|
|
331
|
-
};
|
|
332
|
-
const errorAckBytes = new TextEncoder().encode(JSON.stringify(errorAck));
|
|
333
|
-
await this.sendLengthPrefixed(stream, errorAckBytes, {});
|
|
334
|
-
}
|
|
335
|
-
catch (ackError) {
|
|
336
|
-
this.logger.error('Failed to send error acknowledgment', ackError);
|
|
337
|
-
}
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Extracts and parses JSON from various formats including:
|
|
343
|
-
* - Already parsed objects
|
|
344
|
-
* - Plain JSON
|
|
345
|
-
* - Markdown code blocks (```json ... ``` or ``` ... ```)
|
|
346
|
-
* - Mixed content with explanatory text
|
|
347
|
-
* - JSON5 format (trailing commas, comments, unquoted keys, etc.)
|
|
348
|
-
*
|
|
349
|
-
* @param decoded - The decoded string that may contain JSON, or an already parsed object
|
|
350
|
-
* @returns Parsed JSON object
|
|
351
|
-
* @throws Error if JSON parsing fails even with JSON5 fallback
|
|
352
|
-
*/
|
|
353
|
-
extractAndParseJSON(decoded) {
|
|
354
|
-
// If already an object (not a string), return it directly
|
|
355
|
-
if (typeof decoded !== 'string') {
|
|
356
|
-
return decoded;
|
|
357
|
-
}
|
|
358
|
-
let jsonString = decoded.trim();
|
|
359
|
-
// Attempt standard JSON.parse first
|
|
360
|
-
try {
|
|
361
|
-
return JSON.parse(jsonString);
|
|
362
|
-
}
|
|
363
|
-
catch (jsonError) {
|
|
364
|
-
this.logger.debug('Standard JSON parse failed, trying JSON5', {
|
|
365
|
-
error: jsonError.message,
|
|
366
|
-
position: jsonError.message.match(/position (\d+)/)?.[1],
|
|
367
|
-
preview: jsonString,
|
|
368
|
-
});
|
|
369
|
-
// Fallback to JSON5 for more relaxed parsing
|
|
370
|
-
try {
|
|
371
|
-
return JSON5.parse(jsonString);
|
|
372
|
-
}
|
|
373
|
-
catch (json5Error) {
|
|
374
|
-
// Enhanced error with context
|
|
375
|
-
this.logger.error('JSON5 parse also failed', {
|
|
376
|
-
originalError: jsonError.message,
|
|
377
|
-
json5Error: json5Error.message,
|
|
378
|
-
preview: jsonString.substring(0, 200),
|
|
379
|
-
length: jsonString.length,
|
|
380
|
-
});
|
|
381
|
-
throw new Error(`Failed to parse JSON: ${jsonError.message}\nJSON5 also failed: ${json5Error.message}\nPreview: ${jsonString.substring(0, 200)}${jsonString.length > 200 ? '...' : ''}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Sends data through a stream using length-prefixed encoding (libp2p v3 best practice)
|
|
387
|
-
* Each message is automatically prefixed with a varint indicating the message length
|
|
388
|
-
* This ensures proper message boundaries and eliminates concatenation issues
|
|
389
|
-
*
|
|
390
|
-
* @param stream - The stream to send data through
|
|
391
|
-
* @param data - The data to send
|
|
392
|
-
* @param config - Configuration for timeout and other options
|
|
393
|
-
*/
|
|
394
|
-
async sendLengthPrefixed(stream, data, config = {}) {
|
|
395
|
-
const lp = lpStream(stream);
|
|
396
|
-
await lp.write(data, { signal: config.signal });
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Tracks an active stream handler
|
|
400
|
-
*/
|
|
401
|
-
trackStreamHandler(stream, abortController) {
|
|
402
|
-
this.activeStreamHandlers.set(stream.id, { stream, abortController });
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Untracks a stream handler
|
|
406
|
-
*/
|
|
407
|
-
untrackStreamHandler(streamId) {
|
|
408
|
-
this.activeStreamHandlers.delete(streamId);
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Handles an incoming stream on the server side using length-prefixed protocol
|
|
412
|
-
* Uses async read loops instead of event listeners (libp2p v3 best practice)
|
|
413
|
-
* Processes complete messages with proper boundaries
|
|
414
|
-
*
|
|
415
|
-
* @param stream - The incoming stream
|
|
416
|
-
* @param connection - The connection the stream belongs to
|
|
417
|
-
*/
|
|
418
|
-
async handleIncomingStream(stream, connection) {
|
|
419
|
-
const lp = lpStream(stream);
|
|
420
|
-
const abortController = new AbortController();
|
|
421
|
-
this.trackStreamHandler(stream, abortController);
|
|
422
|
-
try {
|
|
423
|
-
while (stream.status === 'open' && !abortController.signal.aborted) {
|
|
424
|
-
// Read complete length-prefixed message
|
|
425
|
-
const messageBytes = await lp.read();
|
|
426
|
-
const decoded = new TextDecoder().decode(messageBytes.subarray());
|
|
427
|
-
// Parse JSON (handles markdown blocks, mixed content, and JSON5)
|
|
428
|
-
const message = this.extractAndParseJSON(decoded);
|
|
429
|
-
if (this.isStreamInit(message)) {
|
|
430
|
-
await this.handleStreamInitMessage(message, stream);
|
|
431
|
-
// Continue reading for subsequent messages on this stream
|
|
432
|
-
}
|
|
433
|
-
else if (this.isRequest(message)) {
|
|
434
|
-
await this.handleRequestMessage(message, stream, connection);
|
|
435
|
-
}
|
|
436
|
-
else if (this.isResponse(message)) {
|
|
437
|
-
this.logger.warn('Received response message on server-side stream, ignoring', message);
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
this.logger.warn('Received unknown message type', message);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
// Stream closed or error occurred
|
|
446
|
-
if (stream.status === 'open') {
|
|
447
|
-
this.emit(StreamManagerEvent.StreamError, {
|
|
448
|
-
streamId: stream.id,
|
|
449
|
-
error,
|
|
450
|
-
context: 'incoming',
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
finally {
|
|
455
|
-
this.untrackStreamHandler(stream.id);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Determines which stream to use for sending the response
|
|
460
|
-
* Checks for _streamId in request params and routes accordingly
|
|
461
|
-
*
|
|
462
|
-
* @param request - The incoming request
|
|
463
|
-
* @param defaultStream - The stream the request came on (fallback)
|
|
464
|
-
* @returns The stream to use for the response
|
|
465
|
-
*/
|
|
466
|
-
getResponseStream(request, defaultStream) {
|
|
467
|
-
const streamId = request.params._streamId;
|
|
468
|
-
// If no explicit response stream specified, use the request stream (backward compatibility)
|
|
469
|
-
if (!streamId) {
|
|
470
|
-
return defaultStream;
|
|
471
|
-
}
|
|
472
|
-
// Check if the response stream is the identified caller writer stream
|
|
473
|
-
if (this.callerWriterStream && this.callerWriterStream.id === streamId) {
|
|
474
|
-
this.logger.debug('Routing response to caller writer stream', {
|
|
475
|
-
requestId: request.id,
|
|
476
|
-
streamId,
|
|
477
|
-
});
|
|
478
|
-
return this.callerWriterStream;
|
|
479
|
-
}
|
|
480
|
-
if (this.callerReaderStream && this.callerReaderStream.id === streamId) {
|
|
481
|
-
this.logger.debug('Routing response to caller reader stream', {
|
|
482
|
-
requestId: request.id,
|
|
483
|
-
streamId,
|
|
484
|
-
});
|
|
485
|
-
return this.callerReaderStream;
|
|
486
|
-
}
|
|
487
|
-
// If specified stream not found, warn and fall back to request stream
|
|
488
|
-
this.logger.warn('Specified response stream not found, using request stream', {
|
|
489
|
-
requestId: request.id,
|
|
490
|
-
streamId,
|
|
491
|
-
});
|
|
492
|
-
return defaultStream;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Handles a request message by emitting an event and sending response
|
|
496
|
-
*
|
|
497
|
-
* @param message - The decoded request message
|
|
498
|
-
* @param stream - The stream to send the response on
|
|
499
|
-
* @param connection - The connection the stream belongs to
|
|
500
|
-
*/
|
|
501
|
-
async handleRequestMessage(message, stream, connection) {
|
|
502
|
-
const request = new oRequest(message);
|
|
503
|
-
const responseBuilder = ResponseBuilder.create();
|
|
504
|
-
// Determine which stream to use for the response
|
|
505
|
-
const responseStream = this.getResponseStream(request, stream);
|
|
506
|
-
try {
|
|
507
|
-
// Emit InboundRequest event and wait for handler to process
|
|
508
|
-
const result = await this.emitAsync(StreamManagerEvent.InboundRequest, {
|
|
509
|
-
request,
|
|
510
|
-
stream,
|
|
511
|
-
connection,
|
|
512
|
-
});
|
|
513
|
-
const response = await responseBuilder.build(request, result, null);
|
|
514
|
-
await CoreUtils.sendResponse(response, responseStream);
|
|
515
|
-
this.logger.debug(`Successfully processed request: method=${request.method}, id=${request.id}`);
|
|
516
|
-
}
|
|
517
|
-
catch (error) {
|
|
518
|
-
this.logger.error(`Error processing request: method=${request.method}, id=${request.id}`, error);
|
|
519
|
-
const errorResponse = await responseBuilder.buildError(request, error);
|
|
520
|
-
await CoreUtils.sendResponse(errorResponse, responseStream);
|
|
521
|
-
this.emit(StreamManagerEvent.StreamError, {
|
|
522
|
-
streamId: stream.id,
|
|
523
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
524
|
-
context: 'incoming',
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Handles an outgoing stream on the client side using length-prefixed protocol
|
|
530
|
-
* Uses async read loops to process responses with proper message boundaries
|
|
531
|
-
*
|
|
532
|
-
* @param stream - The outgoing stream
|
|
533
|
-
* @param emitter - Event emitter for chunk events
|
|
534
|
-
* @param config - Configuration including abort signal
|
|
535
|
-
* @param requestId - Optional request ID to filter responses (for stream reuse scenarios)
|
|
536
|
-
* @returns Promise that resolves with the final response
|
|
537
|
-
*/
|
|
538
|
-
async handleOutgoingStream(stream, emitter, config = {}, requestId) {
|
|
539
|
-
const lp = lpStream(stream);
|
|
540
|
-
const abortController = new AbortController();
|
|
541
|
-
this.trackStreamHandler(stream, abortController);
|
|
542
|
-
// Combine external signal with our internal abort controller
|
|
543
|
-
const combinedSignal = config.signal
|
|
544
|
-
? AbortSignal.any([config.signal, abortController.signal])
|
|
545
|
-
: abortController.signal;
|
|
546
|
-
try {
|
|
547
|
-
while (stream.status === 'open' && !combinedSignal.aborted) {
|
|
548
|
-
this.logger.debug('Waiting for response...');
|
|
549
|
-
// Read complete length-prefixed message
|
|
550
|
-
const messageBytes = await lp.read({ signal: combinedSignal });
|
|
551
|
-
const decoded = new TextDecoder().decode(messageBytes.subarray());
|
|
552
|
-
// Parse JSON (handles markdown blocks, mixed content, and JSON5)
|
|
553
|
-
const message = this.extractAndParseJSON(decoded);
|
|
554
|
-
if (this.isResponse(message)) {
|
|
555
|
-
const response = new oResponse({
|
|
556
|
-
...message.result,
|
|
557
|
-
id: message.id,
|
|
558
|
-
});
|
|
559
|
-
// Filter by request ID if provided
|
|
560
|
-
if (requestId !== undefined && response.id !== requestId) {
|
|
561
|
-
this.logger.debug(`Ignoring response for different request (expected: ${requestId}, received: ${response.id})`);
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
// Emit chunk for streaming responses
|
|
565
|
-
emitter.emit('chunk', response);
|
|
566
|
-
// Check if this is the last chunk
|
|
567
|
-
if (response.result._last || !response.result._isStreaming) {
|
|
568
|
-
return response;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else if (this.isRequest(message)) {
|
|
572
|
-
// Process incoming router requests via event emission
|
|
573
|
-
const hasListeners = this.eventEmitter.listenerCount(StreamManagerEvent.InboundRequest) >
|
|
574
|
-
0;
|
|
575
|
-
if (hasListeners) {
|
|
576
|
-
this.logger.debug('Received router request on client-side stream, processing...', message);
|
|
577
|
-
// Use handleRequestMessage which emits the InboundRequest event
|
|
578
|
-
await this.handleRequestMessage(message, stream, this.p2pConnection);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
this.logger.warn('Received request message on client-side stream, ignoring (no handler)', message);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
this.logger.warn('Received unknown message type', message);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
throw new oError(oErrorCodes.TIMEOUT, 'Stream closed before response received');
|
|
589
|
-
}
|
|
590
|
-
catch (error) {
|
|
591
|
-
this.emit(StreamManagerEvent.StreamError, {
|
|
592
|
-
streamId: stream.id,
|
|
593
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
594
|
-
context: 'outgoing',
|
|
595
|
-
});
|
|
596
|
-
if (combinedSignal.aborted) {
|
|
597
|
-
throw new oError(oErrorCodes.TIMEOUT, 'Request aborted');
|
|
598
|
-
}
|
|
599
|
-
throw error;
|
|
600
|
-
}
|
|
601
|
-
finally {
|
|
602
|
-
this.untrackStreamHandler(stream.id);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Forwards a request to the next hop and relays response chunks back
|
|
607
|
-
* This implements the middleware/proxy pattern for intermediate nodes
|
|
608
|
-
*
|
|
609
|
-
* @param request - The router request to forward
|
|
610
|
-
* @param incomingStream - The stream to send responses back on
|
|
611
|
-
* @param dialFn - Function to dial the next hop connection
|
|
612
|
-
*/
|
|
613
|
-
async forwardRequest(request, incomingStream, dialFn) {
|
|
614
|
-
try {
|
|
615
|
-
// Connect to next hop
|
|
616
|
-
const nextHopConnection = await dialFn(request.params.address);
|
|
617
|
-
// Set up chunk relay - forward responses from next hop back to incoming stream
|
|
618
|
-
nextHopConnection.onChunk(async (response) => {
|
|
619
|
-
try {
|
|
620
|
-
await CoreUtils.sendResponse(response, incomingStream);
|
|
621
|
-
}
|
|
622
|
-
catch (error) {
|
|
623
|
-
this.logger.error('Error forwarding chunk:', error);
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
// Transmit the request to next hop
|
|
627
|
-
await nextHopConnection.transmit(request);
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
this.logger.error('Error forwarding request:', error);
|
|
631
|
-
// Send error response back on incoming stream using ResponseBuilder
|
|
632
|
-
const responseBuilder = ResponseBuilder.create();
|
|
633
|
-
const errorResponse = await responseBuilder.buildError(request, error);
|
|
634
|
-
await CoreUtils.sendResponse(errorResponse, incomingStream);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Close the stream manager and cleanup resources
|
|
639
|
-
*/
|
|
640
|
-
async close() {
|
|
641
|
-
this.logger.info('Closing stream manager', {
|
|
642
|
-
activeStreams: this.streams.size,
|
|
643
|
-
activeHandlers: this.activeStreamHandlers.size,
|
|
644
|
-
monitoringIntervals: this.streamMonitoringIntervals.size,
|
|
645
|
-
});
|
|
646
|
-
// Clear all stream monitoring intervals
|
|
647
|
-
for (const [streamId, interval,] of this.streamMonitoringIntervals.entries()) {
|
|
648
|
-
clearInterval(interval);
|
|
649
|
-
this.logger.debug('Cleared monitoring interval', { streamId });
|
|
650
|
-
}
|
|
651
|
-
this.streamMonitoringIntervals.clear();
|
|
652
|
-
// Abort all active stream handlers
|
|
653
|
-
for (const [streamId, { abortController },] of this.activeStreamHandlers.entries()) {
|
|
654
|
-
abortController.abort();
|
|
655
|
-
}
|
|
656
|
-
this.activeStreamHandlers.clear();
|
|
657
|
-
// Close all tracked streams
|
|
658
|
-
const closePromises = Array.from(this.streams.values()).map(async (wrappedStream) => {
|
|
659
|
-
try {
|
|
660
|
-
if (wrappedStream.p2pStream.status === 'open') {
|
|
661
|
-
await wrappedStream.p2pStream.close();
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
catch (error) {
|
|
665
|
-
this.logger.debug('Error closing stream during manager close', {
|
|
666
|
-
streamId: wrappedStream.p2pStream.id,
|
|
667
|
-
error: error.message,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
await Promise.all(closePromises);
|
|
672
|
-
// Clear tracking
|
|
673
|
-
this.streams.clear();
|
|
674
|
-
this.isInitialized = false;
|
|
675
|
-
this.emit(StreamManagerEvent.ManagerClosed, undefined);
|
|
676
|
-
this.logger.info('Stream manager closed');
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Add event listener
|
|
680
|
-
*/
|
|
681
|
-
on(event, listener) {
|
|
682
|
-
this.eventEmitter.on(event, listener);
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Remove event listener
|
|
686
|
-
*/
|
|
687
|
-
off(event, listener) {
|
|
688
|
-
this.eventEmitter.off(event, listener);
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Emit event
|
|
692
|
-
*/
|
|
693
|
-
emit(event, data) {
|
|
694
|
-
this.eventEmitter.emit(event, data);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
File without changes
|