@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.
Files changed (54) hide show
  1. package/dist/src/connection/enums/o-node-message-event.d.ts +14 -0
  2. package/dist/src/connection/enums/o-node-message-event.d.ts.map +1 -0
  3. package/dist/src/connection/enums/o-node-message-event.js +5 -0
  4. package/dist/src/connection/index.d.ts +0 -3
  5. package/dist/src/connection/index.d.ts.map +1 -1
  6. package/dist/src/connection/index.js +0 -3
  7. package/dist/src/connection/interfaces/abort-signal.config.d.ts +5 -0
  8. package/dist/src/connection/interfaces/abort-signal.config.d.ts.map +1 -0
  9. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts +13 -1
  10. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
  11. package/dist/src/connection/interfaces/o-node-connection.config.d.ts +18 -5
  12. package/dist/src/connection/interfaces/o-node-connection.config.d.ts.map +1 -1
  13. package/dist/src/connection/interfaces/o-node-stream.config.d.ts +3 -11
  14. package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -1
  15. package/dist/src/connection/o-node-connection.d.ts +29 -53
  16. package/dist/src/connection/o-node-connection.d.ts.map +1 -1
  17. package/dist/src/connection/o-node-connection.js +102 -149
  18. package/dist/src/connection/o-node-connection.manager.d.ts +12 -8
  19. package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
  20. package/dist/src/connection/o-node-connection.manager.js +49 -35
  21. package/dist/src/connection/o-node-stream.d.ts +48 -15
  22. package/dist/src/connection/o-node-stream.d.ts.map +1 -1
  23. package/dist/src/connection/o-node-stream.js +143 -31
  24. package/dist/src/connection/stream-handler.config.d.ts +0 -11
  25. package/dist/src/connection/stream-handler.config.d.ts.map +1 -1
  26. package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts +9 -0
  27. package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts.map +1 -0
  28. package/dist/src/lib/interfaces/o-node-request-manager.config.js +1 -0
  29. package/dist/src/lib/o-node-request-manager.d.ts +46 -0
  30. package/dist/src/lib/o-node-request-manager.d.ts.map +1 -0
  31. package/dist/src/lib/o-node-request-manager.js +173 -0
  32. package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
  33. package/dist/src/managers/o-reconnection.manager.js +4 -0
  34. package/dist/src/managers/o-registration.manager.d.ts.map +1 -1
  35. package/dist/src/managers/o-registration.manager.js +9 -4
  36. package/dist/src/o-node.d.ts +6 -7
  37. package/dist/src/o-node.d.ts.map +1 -1
  38. package/dist/src/o-node.js +22 -37
  39. package/dist/src/o-node.tool.d.ts +3 -3
  40. package/dist/src/o-node.tool.d.ts.map +1 -1
  41. package/dist/src/o-node.tool.js +28 -56
  42. package/dist/src/router/o-node.router.d.ts.map +1 -1
  43. package/dist/src/router/o-node.router.js +4 -2
  44. package/dist/test/connection-management.spec.js +3 -0
  45. package/package.json +7 -7
  46. package/dist/src/connection/interfaces/stream-init-message.d.ts +0 -64
  47. package/dist/src/connection/interfaces/stream-init-message.d.ts.map +0 -1
  48. package/dist/src/connection/interfaces/stream-init-message.js +0 -18
  49. package/dist/src/connection/interfaces/stream-manager.config.d.ts +0 -8
  50. package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +0 -1
  51. package/dist/src/connection/o-node-stream.manager.d.ts +0 -210
  52. package/dist/src/connection/o-node-stream.manager.d.ts.map +0 -1
  53. package/dist/src/connection/o-node-stream.manager.js +0 -696
  54. /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
- }