@olane/o-node 0.7.54 → 0.7.56

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