@olane/o-node 0.7.53 → 0.7.55

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 (25) hide show
  1. package/dist/src/connection/interfaces/o-node-connection.config.d.ts +2 -1
  2. package/dist/src/connection/interfaces/o-node-connection.config.d.ts.map +1 -1
  3. package/dist/src/connection/interfaces/stream-init-message.d.ts +39 -4
  4. package/dist/src/connection/interfaces/stream-init-message.d.ts.map +1 -1
  5. package/dist/src/connection/interfaces/stream-init-message.js +11 -1
  6. package/dist/src/connection/o-node-connection.d.ts +49 -3
  7. package/dist/src/connection/o-node-connection.d.ts.map +1 -1
  8. package/dist/src/connection/o-node-connection.js +120 -2
  9. package/dist/src/connection/o-node-connection.manager.d.ts +6 -55
  10. package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
  11. package/dist/src/connection/o-node-connection.manager.js +47 -183
  12. package/dist/src/connection/o-node-stream.d.ts.map +1 -1
  13. package/dist/src/connection/o-node-stream.js +1 -0
  14. package/dist/src/connection/o-node-stream.manager.d.ts +34 -5
  15. package/dist/src/connection/o-node-stream.manager.d.ts.map +1 -1
  16. package/dist/src/connection/o-node-stream.manager.js +190 -20
  17. package/dist/src/connection/stream-manager.events.d.ts +13 -1
  18. package/dist/src/connection/stream-manager.events.d.ts.map +1 -1
  19. package/dist/src/connection/stream-manager.events.js +2 -0
  20. package/dist/src/o-node.tool.d.ts.map +1 -1
  21. package/dist/src/o-node.tool.js +14 -19
  22. package/dist/src/utils/connection.utils.d.ts +3 -3
  23. package/dist/src/utils/connection.utils.d.ts.map +1 -1
  24. package/dist/src/utils/connection.utils.js +46 -19
  25. package/package.json +7 -7
@@ -5,6 +5,7 @@ import { StreamManagerEvent, } from './stream-manager.events.js';
5
5
  import { isStreamInitMessage, } from './interfaces/stream-init-message.js';
6
6
  import { lpStream } from '@olane/o-config';
7
7
  import JSON5 from 'json5';
8
+ import { v4 as uuidv4 } from 'uuid';
8
9
  /**
9
10
  * oNodeStreamManager handles the lifecycle and tracking of streams for a single connection.
10
11
  * Features:
@@ -16,12 +17,15 @@ import JSON5 from 'json5';
16
17
  */
17
18
  export class oNodeStreamManager extends oObject {
18
19
  constructor(config) {
19
- super();
20
+ const id = uuidv4();
21
+ super('id:' + id);
20
22
  this.config = config;
21
23
  this.streams = new Map();
22
24
  this.eventEmitter = new EventEmitter();
23
25
  this.isInitialized = false;
24
26
  this.activeStreamHandlers = new Map();
27
+ this.streamMonitoringIntervals = new Map(); // Track monitoring intervals
28
+ this.id = id;
25
29
  this.p2pConnection = config.p2pConnection;
26
30
  }
27
31
  /**
@@ -59,17 +63,20 @@ export class oNodeStreamManager extends oObject {
59
63
  * @returns Wrapped stream
60
64
  */
61
65
  async getOrCreateStream(protocol, remoteAddress, config = {}) {
62
- this.logger.debug('Getting or creating stream', this.callerReaderStream?.protocol, 'and status', this.callerReaderStream?.status);
66
+ this.logger.debug('Getting or creating stream', this.callerReaderStream?.protocol, 'and status', this.callerReaderStream?.status, 'json:', JSON.stringify(this.callerReaderStream));
63
67
  // If we have a caller's reader stream (from limited connection), use it for sending requests
64
68
  if (this.callerReaderStream && this.callerReaderStream.status === 'open') {
65
69
  this.logger.debug('Using caller reader stream for limited connection', {
66
70
  streamId: this.callerReaderStream.id,
67
71
  });
72
+ // TODO: figure out why this would cause the node stream to be closed?
68
73
  // 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
- }
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
+ // }
73
80
  // Wrap the reader stream
74
81
  const wrappedStream = new oNodeStream(this.callerReaderStream, {
75
82
  direction: 'inbound', // It's inbound to us, we write to it
@@ -77,7 +84,7 @@ export class oNodeStreamManager extends oObject {
77
84
  remoteAddress: remoteAddress,
78
85
  streamType: 'request-response',
79
86
  });
80
- this.streams.set(this.callerReaderStream.id, wrappedStream);
87
+ // this.streams.set(this.callerReaderStream.id, wrappedStream);
81
88
  return wrappedStream;
82
89
  }
83
90
  // Always create new stream (no reuse at base layer)
@@ -100,7 +107,7 @@ export class oNodeStreamManager extends oObject {
100
107
  const stream = await this.p2pConnection.newStream(protocol, {
101
108
  signal: config.signal,
102
109
  maxOutboundStreams: config.maxOutboundStreams ?? 1000,
103
- runOnLimitedConnection: config.runOnLimitedConnection ?? false,
110
+ runOnLimitedConnection: config.runOnLimitedConnection ?? true,
104
111
  });
105
112
  // Wrap in oNodeStream with metadata
106
113
  const wrappedStream = new oNodeStream(stream, {
@@ -157,6 +164,72 @@ export class oNodeStreamManager extends oObject {
157
164
  getAllStreams() {
158
165
  return Array.from(this.streams.values());
159
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
+ }
160
233
  /**
161
234
  * Emits an async event and waits for the first listener to return a result
162
235
  * This enables event-based request handling with async responses
@@ -194,23 +267,76 @@ export class oNodeStreamManager extends oObject {
194
267
  /**
195
268
  * Handles a stream initialization message
196
269
  * Stores reference to caller's reader stream for bidirectional communication
270
+ * Sends acknowledgment back to confirm stream registration
197
271
  *
198
272
  * @param message - The decoded stream init message
199
273
  * @param stream - The stream that sent the message
200
274
  */
201
- handleStreamInitMessage(message, stream) {
202
- if (message.role === 'reader') {
203
- this.callerReaderStream = stream;
204
- this.logger.info('Identified caller reader stream', {
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',
205
309
  streamId: stream.id,
206
- connectionId: message.connectionId,
207
- });
208
- this.emit(StreamManagerEvent.StreamIdentified, {
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', {
209
316
  streamId: stream.id,
210
317
  role: message.role,
211
- connectionId: message.connectionId,
212
318
  });
213
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
+ }
214
340
  }
215
341
  /**
216
342
  * Extracts and parses JSON from various formats including:
@@ -301,7 +427,7 @@ export class oNodeStreamManager extends oObject {
301
427
  // Parse JSON (handles markdown blocks, mixed content, and JSON5)
302
428
  const message = this.extractAndParseJSON(decoded);
303
429
  if (this.isStreamInit(message)) {
304
- this.handleStreamInitMessage(message, stream);
430
+ await this.handleStreamInitMessage(message, stream);
305
431
  // Continue reading for subsequent messages on this stream
306
432
  }
307
433
  else if (this.isRequest(message)) {
@@ -318,7 +444,6 @@ export class oNodeStreamManager extends oObject {
318
444
  catch (error) {
319
445
  // Stream closed or error occurred
320
446
  if (stream.status === 'open') {
321
- this.logger.error('Error in length-prefixed stream handler:', error);
322
447
  this.emit(StreamManagerEvent.StreamError, {
323
448
  streamId: stream.id,
324
449
  error,
@@ -330,6 +455,42 @@ export class oNodeStreamManager extends oObject {
330
455
  this.untrackStreamHandler(stream.id);
331
456
  }
332
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
+ }
333
494
  /**
334
495
  * Handles a request message by emitting an event and sending response
335
496
  *
@@ -340,6 +501,8 @@ export class oNodeStreamManager extends oObject {
340
501
  async handleRequestMessage(message, stream, connection) {
341
502
  const request = new oRequest(message);
342
503
  const responseBuilder = ResponseBuilder.create();
504
+ // Determine which stream to use for the response
505
+ const responseStream = this.getResponseStream(request, stream);
343
506
  try {
344
507
  // Emit InboundRequest event and wait for handler to process
345
508
  const result = await this.emitAsync(StreamManagerEvent.InboundRequest, {
@@ -348,13 +511,13 @@ export class oNodeStreamManager extends oObject {
348
511
  connection,
349
512
  });
350
513
  const response = await responseBuilder.build(request, result, null);
351
- await CoreUtils.sendResponse(response, stream);
514
+ await CoreUtils.sendResponse(response, responseStream);
352
515
  this.logger.debug(`Successfully processed request: method=${request.method}, id=${request.id}`);
353
516
  }
354
517
  catch (error) {
355
518
  this.logger.error(`Error processing request: method=${request.method}, id=${request.id}`, error);
356
519
  const errorResponse = await responseBuilder.buildError(request, error);
357
- await CoreUtils.sendResponse(errorResponse, stream);
520
+ await CoreUtils.sendResponse(errorResponse, responseStream);
358
521
  this.emit(StreamManagerEvent.StreamError, {
359
522
  streamId: stream.id,
360
523
  error: error instanceof Error ? error : new Error(String(error)),
@@ -478,7 +641,14 @@ export class oNodeStreamManager extends oObject {
478
641
  this.logger.info('Closing stream manager', {
479
642
  activeStreams: this.streams.size,
480
643
  activeHandlers: this.activeStreamHandlers.size,
644
+ monitoringIntervals: this.streamMonitoringIntervals.size,
481
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();
482
652
  // Abort all active stream handlers
483
653
  for (const [streamId, { abortController },] of this.activeStreamHandlers.entries()) {
484
654
  abortController.abort();
@@ -5,12 +5,14 @@ export declare enum StreamManagerEvent {
5
5
  ManagerInitialized = "manager-initialized",
6
6
  ManagerClosed = "manager-closed",
7
7
  ReaderStarted = "reader-started",
8
+ WriterStarted = "writer-started",
8
9
  ReaderFailed = "reader-failed",
9
10
  ReaderRecovered = "reader-recovered",
10
11
  RecoveryFailed = "recovery-failed",
11
12
  StreamReplaced = "stream-replaced",
12
13
  StreamFailed = "stream-failed",
13
14
  StreamIdentified = "stream-identified",
15
+ StreamClosed = "stream-closed",
14
16
  InboundRequest = "inbound-request",
15
17
  InboundResponse = "inbound-response",
16
18
  StreamError = "stream-error"
@@ -23,6 +25,9 @@ export interface InitializedData {
23
25
  export interface ReaderStartedData {
24
26
  streamId: string;
25
27
  }
28
+ export interface WriterStartedData {
29
+ streamId: string;
30
+ }
26
31
  export interface ReaderFailedData {
27
32
  error?: string;
28
33
  failureCount: number;
@@ -46,9 +51,14 @@ export interface StreamFailedData {
46
51
  }
47
52
  export interface StreamIdentifiedData {
48
53
  streamId: string;
49
- role: 'reader' | 'standard';
54
+ role: 'reader' | 'writer' | 'standard';
50
55
  connectionId?: string;
51
56
  }
57
+ export interface StreamClosedData {
58
+ streamId: string;
59
+ role: 'reader' | 'writer';
60
+ status: string;
61
+ }
52
62
  export interface InboundRequestData {
53
63
  request: any;
54
64
  stream: any;
@@ -70,12 +80,14 @@ export type StreamManagerEventData = {
70
80
  [StreamManagerEvent.ManagerInitialized]: InitializedData;
71
81
  [StreamManagerEvent.ManagerClosed]: void;
72
82
  [StreamManagerEvent.ReaderStarted]: ReaderStartedData;
83
+ [StreamManagerEvent.WriterStarted]: WriterStartedData;
73
84
  [StreamManagerEvent.ReaderFailed]: ReaderFailedData;
74
85
  [StreamManagerEvent.ReaderRecovered]: ReaderRecoveredData;
75
86
  [StreamManagerEvent.RecoveryFailed]: RecoveryFailedData;
76
87
  [StreamManagerEvent.StreamReplaced]: StreamReplacedData;
77
88
  [StreamManagerEvent.StreamFailed]: StreamFailedData;
78
89
  [StreamManagerEvent.StreamIdentified]: StreamIdentifiedData;
90
+ [StreamManagerEvent.StreamClosed]: StreamClosedData;
79
91
  [StreamManagerEvent.InboundRequest]: InboundRequestData;
80
92
  [StreamManagerEvent.InboundResponse]: InboundResponseData;
81
93
  [StreamManagerEvent.StreamError]: StreamErrorData;
@@ -1 +1 @@
1
- {"version":3,"file":"stream-manager.events.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-manager.events.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,oBAAY,kBAAkB;IAC5B,kBAAkB,wBAAwB;IAC1C,aAAa,mBAAmB;IAChC,aAAa,mBAAmB;IAChC,YAAY,kBAAkB;IAC9B,eAAe,qBAAqB;IACpC,cAAc,oBAAoB;IAClC,cAAc,oBAAoB;IAClC,YAAY,kBAAkB;IAC9B,gBAAgB,sBAAsB;IACtC,cAAc,oBAAoB;IAClC,eAAe,qBAAqB;IACpC,WAAW,iBAAiB;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;CAAG;AAEnC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,EAAE,GAAG,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,GAAG,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,eAAe,CAAC;IACzD,CAAC,kBAAkB,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC;IACzC,CAAC,kBAAkB,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IACtD,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAAC;IACpD,CAAC,kBAAkB,CAAC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IAC1D,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACxD,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACxD,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAAC;IACpD,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IAC5D,CAAC,kBAAkB,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACxD,CAAC,kBAAkB,CAAC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IAC1D,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;CACnD,CAAC"}
1
+ {"version":3,"file":"stream-manager.events.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-manager.events.ts"],"names":[],"mappings":"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"}
@@ -6,12 +6,14 @@ export var StreamManagerEvent;
6
6
  StreamManagerEvent["ManagerInitialized"] = "manager-initialized";
7
7
  StreamManagerEvent["ManagerClosed"] = "manager-closed";
8
8
  StreamManagerEvent["ReaderStarted"] = "reader-started";
9
+ StreamManagerEvent["WriterStarted"] = "writer-started";
9
10
  StreamManagerEvent["ReaderFailed"] = "reader-failed";
10
11
  StreamManagerEvent["ReaderRecovered"] = "reader-recovered";
11
12
  StreamManagerEvent["RecoveryFailed"] = "recovery-failed";
12
13
  StreamManagerEvent["StreamReplaced"] = "stream-replaced";
13
14
  StreamManagerEvent["StreamFailed"] = "stream-failed";
14
15
  StreamManagerEvent["StreamIdentified"] = "stream-identified";
16
+ StreamManagerEvent["StreamClosed"] = "stream-closed";
15
17
  StreamManagerEvent["InboundRequest"] = "inbound-request";
16
18
  StreamManagerEvent["InboundResponse"] = "inbound-response";
17
19
  StreamManagerEvent["StreamError"] = "stream-error";
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAOrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;IAuBrC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAsBhC,mBAAmB;IAWnB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC;IAwDV,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAiC5D"}
1
+ {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAOrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;IAuBrC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAyBhC,mBAAmB;IAWnB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC;IA+CV,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAiC5D"}
@@ -3,7 +3,6 @@ import { oTool } from '@olane/o-tool';
3
3
  import { oServerNode } from './nodes/server.node.js';
4
4
  import { oNodeTransport } from './router/o-node.transport.js';
5
5
  import { oNodeAddress } from './router/o-node.address.js';
6
- import { ConnectionUtils } from './utils/connection.utils.js';
7
6
  import { StreamManagerEvent } from './connection/stream-manager.events.js';
8
7
  /**
9
8
  * oTool is a mixin that extends the base class and implements the oTool interface
@@ -25,13 +24,16 @@ export class oNodeTool extends oTool(oServerNode) {
25
24
  await this.p2pNode.handle(reuseProtocol, this.handleStreamReuse.bind(this), {
26
25
  maxInboundStreams: 10000,
27
26
  maxOutboundStreams: maxOutboundsStreams,
28
- runOnLimitedConnection: this.config.runOnLimitedConnection,
27
+ runOnLimitedConnection: true, // reuse is always on limited connections
29
28
  });
30
29
  this.logger.debug('Handled protocol reuse: ' + reuseProtocol);
31
30
  }
32
31
  async handleProtocol(address) {
32
+ if (!address || !address.protocol) {
33
+ throw new Error('Invalid address passed: ' + address);
34
+ }
33
35
  const protocols = this.p2pNode.getProtocols();
34
- if (protocols.find((p) => p === address.protocol)) {
36
+ if (protocols.find((p) => p === address?.protocol)) {
35
37
  // already handling
36
38
  return;
37
39
  }
@@ -66,29 +68,22 @@ export class oNodeTool extends oTool(oServerNode) {
66
68
  return this.handleStream(stream, connection, true);
67
69
  }
68
70
  async handleStream(stream, connection, reuse) {
69
- if (reuse) {
70
- this.logger.debug('Handle stream with reuse = true');
71
- // record inbound connection to manager
72
- }
73
- const remoteAddress = await ConnectionUtils.addressFromConnection({
74
- currentNode: this,
75
- connection: connection,
71
+ this.logger.debug('Handling incoming stream on connection:', {
72
+ connectionId: connection.id,
73
+ direction: connection.direction,
76
74
  });
77
- await this.connectionManager.answer({
78
- nextHopAddress: remoteAddress,
79
- address: remoteAddress,
75
+ const unknown = new oNodeAddress('o://unknown', []);
76
+ const oConnection = await this.connectionManager.answer({
77
+ nextHopAddress: unknown,
78
+ address: unknown,
80
79
  callerAddress: this.address,
81
80
  p2pConnection: connection,
82
81
  reuse,
83
82
  });
84
83
  // Get the oNodeConnection for this libp2p connection
85
- const oConnection = this.connectionManager.getConnectionByP2pConnection(connection);
86
84
  if (!oConnection) {
87
- this.logger.error('No oNodeConnection found for incoming stream', {
88
- remotePeer: connection.remotePeer.toString(),
89
- connectionId: connection.id,
90
- });
91
- return;
85
+ this.logger.error('Failed to process inbound connection');
86
+ throw new Error('Failed to process inbound connection');
92
87
  }
93
88
  // Subscribe to InboundRequest events from the stream manager
94
89
  // This follows an event-driven pattern for handling incoming requests
@@ -2,10 +2,10 @@ import { Connection } from '@olane/o-config';
2
2
  import { oObject } from '@olane/o-core';
3
3
  export declare class ConnectionUtils extends oObject {
4
4
  /**
5
- * Waits for a peer to appear in the peer store with sufficient protocol information.
6
- * Implements retry logic to handle race conditions where peer store is not immediately populated.
5
+ * Waits for a peer to be identified via the identify protocol.
6
+ * Uses event-driven approach listening to peer store protocol updates.
7
7
  */
8
- private static waitForPeerInStore;
8
+ private static waitForPeerIdentify;
9
9
  static addressFromConnection(options: {
10
10
  currentNode: any;
11
11
  connection: Connection;
@@ -1 +1 @@
1
- {"version":3,"file":"connection.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/connection.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2B,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAuB,OAAO,EAAE,MAAM,eAAe,CAAC;AAI7D,qBAAa,eAAgB,SAAQ,OAAO;IAC1C;;;OAGG;mBACkB,kBAAkB;WAgCnB,qBAAqB,CAAC,OAAO,EAAE;QACjD,WAAW,EAAE,GAAG,CAAC;QACjB,UAAU,EAAE,UAAU,CAAC;KACxB;CA0DF"}
1
+ {"version":3,"file":"connection.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/connection.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAKX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAuB,OAAO,EAAE,MAAM,eAAe,CAAC;AAI7D,qBAAa,eAAgB,SAAQ,OAAO;IAC1C;;;OAGG;mBACkB,mBAAmB;WA0EpB,qBAAqB,CAAC,OAAO,EAAE;QACjD,WAAW,EAAE,GAAG,CAAC;QACjB,UAAU,EAAE,UAAU,CAAC;KACxB;CA4DF"}
@@ -3,38 +3,64 @@ import { oNodeAddress } from '../router/o-node.address.js';
3
3
  import { oNodeTransport } from '../router/o-node.transport.js';
4
4
  export class ConnectionUtils extends oObject {
5
5
  /**
6
- * Waits for a peer to appear in the peer store with sufficient protocol information.
7
- * Implements retry logic to handle race conditions where peer store is not immediately populated.
6
+ * Waits for a peer to be identified via the identify protocol.
7
+ * Uses event-driven approach listening to peer store protocol updates.
8
8
  */
9
- static async waitForPeerInStore(p2pNode, remotePeerId) {
10
- const MAX_RETRIES = 100; // 5 seconds total (100 * 50ms)
11
- const RETRY_DELAY_MS = 50;
12
- const MIN_PROTOCOLS = 2;
13
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
9
+ static async waitForPeerIdentify(p2pNode, remotePeerId, nodeProtocol) {
10
+ const TIMEOUT_MS = 5000; // 5 seconds timeout
11
+ // Helper to check if peer has sufficient protocols
12
+ const checkPeerProtocols = async () => {
14
13
  const peers = await p2pNode.peerStore.all();
15
14
  const remotePeer = peers.find((peer) => {
16
15
  return peer.id.toString() === remotePeerId.toString();
17
16
  });
18
- // Check if peer exists and has sufficient protocol information
19
- if (remotePeer && remotePeer.protocols.length > MIN_PROTOCOLS) {
20
- return remotePeer;
21
- }
22
- // Wait before next retry
23
- if (attempt < MAX_RETRIES - 1) {
24
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
17
+ if (remotePeer) {
18
+ const oProtocols = remotePeer.protocols.filter((p) => p.startsWith('/o/') && p.startsWith(nodeProtocol) === false) || [];
19
+ console.log('Found o-protocols for peer:', oProtocols, 'with node address:', nodeProtocol);
20
+ if (oProtocols.length > 0) {
21
+ return remotePeer;
22
+ }
25
23
  }
24
+ return null;
25
+ };
26
+ // Check if peer already has sufficient protocols
27
+ const existingPeer = await checkPeerProtocols();
28
+ if (existingPeer) {
29
+ return existingPeer;
26
30
  }
27
- // Timeout exceeded
28
- throw new Error(`Timeout waiting for peer ${remotePeerId.toString()} to appear in peer store with sufficient protocols (waited ${MAX_RETRIES * RETRY_DELAY_MS}ms)`);
31
+ // Wait for peer store protocol update event
32
+ return new Promise((resolve, reject) => {
33
+ const timeoutId = setTimeout(() => {
34
+ // TypeScript doesn't have types for peerStore events, but they exist at runtime
35
+ p2pNode.removeEventListener('peer:identify', protocolChangeHandler);
36
+ reject(new Error(`Timeout waiting for peer ${remotePeerId.toString()} to be identified (waited ${TIMEOUT_MS}ms)`));
37
+ }, TIMEOUT_MS);
38
+ const protocolChangeHandler = async (evt) => {
39
+ const { peerId } = evt.detail;
40
+ console.log('evt.detail:', evt.detail);
41
+ // Check if this is the peer we're waiting for
42
+ if (peerId?.toString() === remotePeerId.toString()) {
43
+ const peer = await checkPeerProtocols();
44
+ if (peer) {
45
+ clearTimeout(timeoutId);
46
+ // TypeScript doesn't have types for peerStore events, but they exist at runtime
47
+ p2pNode.removeEventListener('peer:identify', protocolChangeHandler);
48
+ resolve(peer);
49
+ }
50
+ }
51
+ };
52
+ // TypeScript doesn't have types for peerStore events, but they exist at runtime
53
+ p2pNode.addEventListener('peer:identify', protocolChangeHandler);
54
+ });
29
55
  }
30
56
  // TODO: improve this logic (poor implementation for now)
31
57
  static async addressFromConnection(options) {
32
58
  try {
33
59
  const { currentNode, connection } = options;
34
60
  const p2pNode = currentNode.p2pNode;
35
- // Wait for peer to appear in peer store with sufficient protocol information
36
- // This handles race conditions where peer store is not immediately populated
37
- const remotePeer = await this.waitForPeerInStore(p2pNode, connection.remotePeer);
61
+ // Wait for peer to be identified via the identify protocol
62
+ // This uses an event-driven approach to detect when the peer store is updated
63
+ const remotePeer = await this.waitForPeerIdentify(p2pNode, connection.remotePeer, currentNode.address.protocol);
38
64
  // Get origin address for comparison
39
65
  const originAddress = currentNode.address?.value;
40
66
  if (!originAddress) {
@@ -43,6 +69,7 @@ export class ConnectionUtils extends oObject {
43
69
  const oProtocol = remotePeer.protocols.find((p) => p.startsWith('/o/') &&
44
70
  p.includes(currentNode?.address?.protocol) === false);
45
71
  if (!oProtocol) {
72
+ console.log('Remote peer protocols:', remotePeer.protocols);
46
73
  throw new Error('Failed to extract remote address, could not find o-protocol in peer protocols.');
47
74
  }
48
75
  const address = oNodeAddress.fromProtocol(oProtocol);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olane/o-node",
3
- "version": "0.7.53",
3
+ "version": "0.7.55",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  "devDependencies": {
41
41
  "@eslint/eslintrc": "^3.3.1",
42
42
  "@eslint/js": "^9.29.0",
43
- "@olane/o-test": "0.7.53",
43
+ "@olane/o-test": "0.7.55",
44
44
  "@tsconfig/node20": "^20.1.6",
45
45
  "@types/jest": "^30.0.0",
46
46
  "@types/json5": "^2.2.0",
@@ -60,13 +60,13 @@
60
60
  "typescript": "5.4.5"
61
61
  },
62
62
  "dependencies": {
63
- "@olane/o-config": "0.7.53",
64
- "@olane/o-core": "0.7.53",
65
- "@olane/o-protocol": "0.7.53",
66
- "@olane/o-tool": "0.7.53",
63
+ "@olane/o-config": "0.7.55",
64
+ "@olane/o-core": "0.7.55",
65
+ "@olane/o-protocol": "0.7.55",
66
+ "@olane/o-tool": "0.7.55",
67
67
  "debug": "^4.4.1",
68
68
  "dotenv": "^16.5.0",
69
69
  "json5": "^2.2.3"
70
70
  },
71
- "gitHead": "cc41cbe72abffd5f2720aec92516aafea50ee9c3"
71
+ "gitHead": "f7d02a1ecfb0d28b7b1009a7c3eb6d0d4863adc2"
72
72
  }