@olane/o-node 0.7.12-alpha.51 → 0.7.12-alpha.53

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.
@@ -1,9 +1,11 @@
1
1
  import { Connection, Stream } from '@olane/o-config';
2
2
  import { oConnection, oRequest, oResponse } from '@olane/o-core';
3
3
  import { oNodeConnectionConfig } from './interfaces/o-node-connection.config.js';
4
+ import { StreamHandler } from './stream-handler.js';
4
5
  export declare class oNodeConnection extends oConnection {
5
6
  protected readonly config: oNodeConnectionConfig;
6
7
  p2pConnection: Connection;
8
+ protected streamHandler: StreamHandler;
7
9
  constructor(config: oNodeConnectionConfig);
8
10
  setupConnectionListeners(): void;
9
11
  validate(stream?: Stream): void;
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF,qBAAa,eAAgB,SAAQ,WAAW;IAGlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAFrD,aAAa,EAAE,UAAU,CAAC;gBAEF,MAAM,EAAE,qBAAqB;IAM5D,wBAAwB;IAYxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAmBlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAcpC,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IA2E/C,YAAY,CAAC,MAAM,EAAE,MAAM;IAU3B,KAAK,CAAC,KAAK,EAAE,KAAK;IAMlB,KAAK;CAKZ"}
1
+ {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,qBAAa,eAAgB,SAAQ,WAAW;IAIlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAHrD,aAAa,EAAE,UAAU,CAAC;IACjC,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;gBAER,MAAM,EAAE,qBAAqB;IAO5D,wBAAwB;IAYxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAmBlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBpC,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IAsC/C,YAAY,CAAC,MAAM,EAAE,MAAM;IAQ3B,KAAK,CAAC,KAAK,EAAE,KAAK;IAMlB,KAAK;CAKZ"}
@@ -1,9 +1,11 @@
1
- import { CoreUtils, oConnection, oError, oErrorCodes, oResponse, } from '@olane/o-core';
1
+ import { oConnection, oError, oErrorCodes, } from '@olane/o-core';
2
+ import { StreamHandler } from './stream-handler.js';
2
3
  export class oNodeConnection extends oConnection {
3
4
  constructor(config) {
4
5
  super(config);
5
6
  this.config = config;
6
7
  this.p2pConnection = config.p2pConnection;
8
+ this.streamHandler = new StreamHandler(this.logger);
7
9
  this.setupConnectionListeners();
8
10
  }
9
11
  setupConnectionListeners() {
@@ -26,16 +28,16 @@ export class oNodeConnection extends oConnection {
26
28
  }
27
29
  }
28
30
  async getOrCreateStream() {
29
- if (this.p2pConnection.status !== 'open') {
30
- throw new oError(oErrorCodes.INVALID_STATE, 'Connection not open');
31
- }
32
- return this.p2pConnection.newStream(this.nextHopAddress.protocol, {
31
+ const streamConfig = {
33
32
  signal: this.abortSignal,
34
33
  maxOutboundStreams: process.env.MAX_OUTBOUND_STREAMS
35
34
  ? parseInt(process.env.MAX_OUTBOUND_STREAMS)
36
35
  : 1000,
37
36
  runOnLimitedConnection: this.config.runOnLimitedConnection ?? false,
38
- });
37
+ reusePolicy: 'none', // Default policy, can be overridden in subclasses
38
+ drainTimeoutMs: this.config.drainTimeoutMs,
39
+ };
40
+ return this.streamHandler.getOrCreateStream(this.p2pConnection, this.nextHopAddress.protocol, streamConfig);
39
41
  }
40
42
  async transmit(request) {
41
43
  try {
@@ -44,54 +46,18 @@ export class oNodeConnection extends oConnection {
44
46
  }
45
47
  const stream = await this.getOrCreateStream();
46
48
  this.validate(stream);
47
- // Send the data with backpressure handling (libp2p v3 best practice)
49
+ const streamConfig = {
50
+ signal: this.abortSignal,
51
+ drainTimeoutMs: this.config.drainTimeoutMs,
52
+ reusePolicy: 'none', // Default policy
53
+ };
54
+ // Send the request with backpressure handling
48
55
  const data = new TextEncoder().encode(request.toString());
49
- const sent = stream.send(data);
50
- let lastResponse;
51
- await new Promise((resolve, reject) => {
52
- const abortHandler = async () => {
53
- try {
54
- await stream.abort(new Error('Request aborted'));
55
- }
56
- catch (e) {
57
- // Stream may already be closed
58
- }
59
- reject(new Error('Request aborted'));
60
- };
61
- // Listen for abort signal
62
- if (this.abortSignal) {
63
- this.abortSignal.addEventListener('abort', abortHandler);
64
- }
65
- stream.addEventListener('message', async (event) => {
66
- const response = await CoreUtils.processStreamResponse(event);
67
- this.emitter.emit('chunk', response);
68
- // marked as the last chunk let's close
69
- if (response.result._last || !response.result._isStreaming) {
70
- lastResponse = response;
71
- // Clean up abort listener before closing
72
- if (this.abortSignal) {
73
- this.abortSignal.removeEventListener('abort', abortHandler);
74
- }
75
- resolve(true);
76
- }
77
- });
78
- });
79
- stream.addEventListener('close', async () => {
80
- this.logger.debug('Stream closed by remote peer, closing connection...');
81
- stream.close().catch((error) => {
82
- this.logger.error('Error closing stream: ', error);
83
- });
84
- });
85
- // If send() returns false, wait for the stream to drain before continuing
86
- if (!sent) {
87
- this.logger.debug('Stream buffer full, waiting for drain...');
88
- await stream.onDrain({
89
- signal: AbortSignal.timeout(this.config.drainTimeoutMs ?? 30000),
90
- }); // Default: 30 second timeout
91
- }
92
- // handle cleanup of the stream
56
+ await this.streamHandler.send(stream, data, streamConfig);
57
+ // Handle response using StreamHandler
58
+ const response = await this.streamHandler.handleOutgoingStream(stream, this.emitter, streamConfig);
59
+ // Handle cleanup of the stream
93
60
  await this.postTransmit(stream);
94
- const response = oResponse.fromJSON(lastResponse);
95
61
  return response;
96
62
  }
97
63
  catch (error) {
@@ -102,14 +68,10 @@ export class oNodeConnection extends oConnection {
102
68
  }
103
69
  }
104
70
  async postTransmit(stream) {
105
- if (stream.status === 'open') {
106
- stream.close().catch((error) => {
107
- this.logger.error('Error closing stream after transmission: ', error);
108
- });
109
- }
110
- else {
111
- this.logger.debug('Stream is not open, skipping cleanup');
112
- }
71
+ const streamConfig = {
72
+ reusePolicy: 'none', // Default policy, can be overridden in subclasses
73
+ };
74
+ await this.streamHandler.close(stream, streamConfig);
113
75
  }
114
76
  async abort(error) {
115
77
  this.logger.debug('Aborting connection');
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAU,UAAU,EAAU,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAKhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IAJzD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;gBAElB,MAAM,EAAE,4BAA4B;IAOnD,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;IA2BtB;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6BlE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IA4BpC;;;;OAIG;IACH,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI;CA0BhE"}
1
+ {"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAU,UAAU,EAAU,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAKhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IAJzD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;gBAElB,MAAM,EAAE,4BAA4B;IAOnD,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;IA4BtB;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6BlE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IA4BpC;;;;OAIG;IACH,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI;CA0BhE"}
@@ -13,7 +13,8 @@ export class oNodeConnectionManager extends oConnectionManager {
13
13
  const existingConnection = this.getCachedLibp2pConnection(nextHopAddress);
14
14
  let p2pConnection;
15
15
  if (existingConnection && existingConnection.status === 'open') {
16
- this.logger.debug('Reusing existing libp2p connection for address: ' + address.toString());
16
+ this.logger.debug('Reusing existing libp2p connection for address: ' +
17
+ nextHopAddress.toString());
17
18
  p2pConnection = existingConnection;
18
19
  }
19
20
  else {
@@ -0,0 +1,46 @@
1
+ /// <reference types="node" />
2
+ import type { Stream } from '@libp2p/interface';
3
+ /**
4
+ * Stream reuse policy determines how streams are managed across multiple requests
5
+ */
6
+ export type StreamReusePolicy = 'none' | 'reuse' | 'pool';
7
+ /**
8
+ * Configuration for StreamHandler behavior
9
+ */
10
+ export interface StreamHandlerConfig {
11
+ /**
12
+ * Stream reuse policy:
13
+ * - 'none': Create new stream for each request (default)
14
+ * - 'reuse': Reuse existing open streams
15
+ * - 'pool': (Future) Maintain a pool of streams
16
+ */
17
+ reusePolicy?: StreamReusePolicy;
18
+ /**
19
+ * Timeout in milliseconds to wait for stream drain when buffer is full
20
+ * @default 30000 (30 seconds)
21
+ */
22
+ drainTimeoutMs?: number;
23
+ /**
24
+ * Whether this connection can run on limited connections
25
+ * @default false
26
+ */
27
+ runOnLimitedConnection?: boolean;
28
+ /**
29
+ * Maximum number of outbound streams
30
+ * @default 1000
31
+ */
32
+ maxOutboundStreams?: number;
33
+ /**
34
+ * AbortSignal for cancellation
35
+ */
36
+ signal?: AbortSignal;
37
+ }
38
+ /**
39
+ * Context for stream lifecycle operations
40
+ */
41
+ export interface StreamContext {
42
+ stream: Stream;
43
+ protocol: string;
44
+ config: StreamHandlerConfig;
45
+ }
46
+ //# sourceMappingURL=stream-handler.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.config.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.config.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEhC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;CAC7B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ /// <reference types="node" />
2
+ import type { Connection, Stream } from '@libp2p/interface';
3
+ import { EventEmitter } from 'events';
4
+ import { oRequest, oResponse, Logger } from '@olane/o-core';
5
+ import type { oRouterRequest } from '@olane/o-core';
6
+ import type { oConnection } from '@olane/o-core';
7
+ import type { RunResult } from '@olane/o-tool';
8
+ import type { StreamHandlerConfig } from './stream-handler.config.js';
9
+ /**
10
+ * StreamHandler centralizes all stream-related functionality including:
11
+ * - Message type detection (request vs response)
12
+ * - Stream lifecycle management (create, reuse, close)
13
+ * - Backpressure handling
14
+ * - Request/response handling
15
+ * - Stream routing for middleware nodes
16
+ */
17
+ export declare class StreamHandler {
18
+ private logger;
19
+ constructor(logger?: Logger);
20
+ /**
21
+ * Detects if a decoded message is a request
22
+ * Requests have a 'method' field and no 'result' field
23
+ */
24
+ isRequest(message: any): boolean;
25
+ /**
26
+ * Detects if a decoded message is a response
27
+ * Responses have a 'result' field and no 'method' field
28
+ */
29
+ isResponse(message: any): boolean;
30
+ /**
31
+ * Decodes a stream message event into a JSON object
32
+ */
33
+ decodeMessage(event: any): Promise<any>;
34
+ /**
35
+ * Gets an existing open stream or creates a new one based on reuse policy
36
+ *
37
+ * @param connection - The libp2p connection
38
+ * @param protocol - The protocol to use for the stream
39
+ * @param config - Stream handler configuration
40
+ */
41
+ getOrCreateStream(connection: Connection, protocol: string, config?: StreamHandlerConfig): Promise<Stream>;
42
+ /**
43
+ * Sends data through a stream with backpressure handling
44
+ *
45
+ * @param stream - The stream to send data through
46
+ * @param data - The data to send
47
+ * @param config - Configuration for timeout and other options
48
+ */
49
+ send(stream: Stream, data: Uint8Array, config?: StreamHandlerConfig): Promise<void>;
50
+ /**
51
+ * Closes a stream safely with error handling
52
+ *
53
+ * @param stream - The stream to close
54
+ * @param config - Configuration including reuse policy
55
+ */
56
+ close(stream: Stream, config?: StreamHandlerConfig): Promise<void>;
57
+ /**
58
+ * Handles an incoming stream on the server side
59
+ * Attaches message listener immediately (libp2p v3 best practice)
60
+ * Routes requests or executes tools based on the message
61
+ *
62
+ * @param stream - The incoming stream
63
+ * @param connection - The connection the stream belongs to
64
+ * @param toolExecutor - Function to execute tools for requests
65
+ */
66
+ handleIncomingStream(stream: Stream, connection: Connection, toolExecutor: (request: oRequest, stream: Stream) => Promise<RunResult>): Promise<void>;
67
+ /**
68
+ * Handles a request message by executing the tool and sending response
69
+ *
70
+ * @param message - The decoded request message
71
+ * @param stream - The stream to send the response on
72
+ * @param toolExecutor - Function to execute the tool
73
+ */
74
+ private handleRequestMessage;
75
+ /**
76
+ * Handles an outgoing stream on the client side
77
+ * Listens for response messages and emits them via the event emitter
78
+ *
79
+ * @param stream - The outgoing stream
80
+ * @param emitter - Event emitter for chunk events
81
+ * @param config - Configuration including abort signal
82
+ * @returns Promise that resolves with the final response
83
+ */
84
+ handleOutgoingStream(stream: Stream, emitter: EventEmitter, config?: StreamHandlerConfig): Promise<oResponse>;
85
+ /**
86
+ * Forwards a request to the next hop and relays response chunks back
87
+ * This implements the middleware/proxy pattern for intermediate nodes
88
+ *
89
+ * @param request - The router request to forward
90
+ * @param incomingStream - The stream to send responses back on
91
+ * @param dialFn - Function to dial the next hop connection
92
+ */
93
+ forwardRequest(request: oRouterRequest, incomingStream: Stream, dialFn: (address: string) => Promise<oConnection>): Promise<void>;
94
+ }
95
+ //# sourceMappingURL=stream-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,QAAQ,EACR,SAAS,EAIT,MAAM,EAEP,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,CAAC,EAAE,MAAM;IAI3B;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO;IAIhC;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO;IAIjC;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IAI7C;;;;;;OAMG;IACG,iBAAiB,CACrB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,mBAAwB,GAC/B,OAAO,CAAC,MAAM,CAAC;IA2ClB;;;;;;OAMG;IACG,IAAI,CACR,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,MAAM,GAAE,mBAAwB,GAC/B,OAAO,CAAC,IAAI,CAAC;IAiBhB;;;;;OAKG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB5E;;;;;;;;OAQG;IACG,oBAAoB,CACxB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,GACtE,OAAO,CAAC,IAAI,CAAC;IA0ChB;;;;;;OAMG;YACW,oBAAoB;IAkBlC;;;;;;;;OAQG;IACG,oBAAoB,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,mBAAwB,GAC/B,OAAO,CAAC,SAAS,CAAC;IA4FrB;;;;;;;OAOG;IACG,cAAc,CAClB,OAAO,EAAE,cAAc,EACvB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,GAChD,OAAO,CAAC,IAAI,CAAC;CAyBjB"}
@@ -0,0 +1,297 @@
1
+ import { oRequest, CoreUtils, oError, oErrorCodes, Logger, ResponseBuilder, } from '@olane/o-core';
2
+ /**
3
+ * StreamHandler centralizes all stream-related functionality including:
4
+ * - Message type detection (request vs response)
5
+ * - Stream lifecycle management (create, reuse, close)
6
+ * - Backpressure handling
7
+ * - Request/response handling
8
+ * - Stream routing for middleware nodes
9
+ */
10
+ export class StreamHandler {
11
+ constructor(logger) {
12
+ this.logger = logger ?? new Logger('StreamHandler');
13
+ }
14
+ /**
15
+ * Detects if a decoded message is a request
16
+ * Requests have a 'method' field and no 'result' field
17
+ */
18
+ isRequest(message) {
19
+ return typeof message?.method === 'string' && message.result === undefined;
20
+ }
21
+ /**
22
+ * Detects if a decoded message is a response
23
+ * Responses have a 'result' field and no 'method' field
24
+ */
25
+ isResponse(message) {
26
+ return message?.result !== undefined && message.method === undefined;
27
+ }
28
+ /**
29
+ * Decodes a stream message event into a JSON object
30
+ */
31
+ async decodeMessage(event) {
32
+ return CoreUtils.processStream(event);
33
+ }
34
+ /**
35
+ * Gets an existing open stream or creates a new one based on reuse policy
36
+ *
37
+ * @param connection - The libp2p connection
38
+ * @param protocol - The protocol to use for the stream
39
+ * @param config - Stream handler configuration
40
+ */
41
+ async getOrCreateStream(connection, protocol, config = {}) {
42
+ if (connection.status !== 'open') {
43
+ throw new oError(oErrorCodes.INVALID_STATE, 'Connection not open');
44
+ }
45
+ const reusePolicy = config.reusePolicy ?? 'none';
46
+ this.logger.debug('Reuse policy:', reusePolicy);
47
+ // Check for existing stream if reuse is enabled
48
+ if (reusePolicy === 'reuse') {
49
+ this.logger.debug('Reusing existing stream if we can find one. Stream insights:', JSON.stringify({
50
+ streamCount: connection.streams.length,
51
+ protocol: protocol,
52
+ }));
53
+ connection.streams.forEach((stream) => {
54
+ this.logger.debug('Stream re-use option:', stream.protocol, stream.status, stream.direction);
55
+ });
56
+ const existingStream = connection.streams.find((stream) => stream.status === 'open' && stream.protocol?.length > 0);
57
+ if (existingStream) {
58
+ this.logger.debug('Reusing existing stream');
59
+ return existingStream;
60
+ }
61
+ }
62
+ // Create new stream
63
+ this.logger.debug('Creating new stream');
64
+ return connection.newStream(protocol, {
65
+ signal: config.signal,
66
+ maxOutboundStreams: config.maxOutboundStreams ?? 1000,
67
+ runOnLimitedConnection: config.runOnLimitedConnection ?? false,
68
+ });
69
+ }
70
+ /**
71
+ * Sends data through a stream with backpressure handling
72
+ *
73
+ * @param stream - The stream to send data through
74
+ * @param data - The data to send
75
+ * @param config - Configuration for timeout and other options
76
+ */
77
+ async send(stream, data, config = {}) {
78
+ // Send the data with backpressure handling (libp2p v3 best practice)
79
+ const sent = stream.send(data);
80
+ // If send() returns false, buffer is full - wait for drain
81
+ if (!sent) {
82
+ this.logger.debug('Stream buffer full, waiting for drain...');
83
+ const drainTimeout = config.drainTimeoutMs ?? 30000;
84
+ await stream.onDrain({
85
+ signal: AbortSignal.timeout(drainTimeout),
86
+ });
87
+ this.logger.debug('Stream drained successfully');
88
+ }
89
+ }
90
+ /**
91
+ * Closes a stream safely with error handling
92
+ *
93
+ * @param stream - The stream to close
94
+ * @param config - Configuration including reuse policy
95
+ */
96
+ async close(stream, config = {}) {
97
+ // Don't close if reuse policy is enabled
98
+ if (config.reusePolicy === 'reuse') {
99
+ this.logger.debug('Stream reuse enabled, not closing stream');
100
+ return;
101
+ }
102
+ if (stream.status === 'open') {
103
+ try {
104
+ await stream.close();
105
+ this.logger.debug('Stream closed successfully');
106
+ }
107
+ catch (error) {
108
+ this.logger.debug('Error closing stream:', error.message);
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Handles an incoming stream on the server side
114
+ * Attaches message listener immediately (libp2p v3 best practice)
115
+ * Routes requests or executes tools based on the message
116
+ *
117
+ * @param stream - The incoming stream
118
+ * @param connection - The connection the stream belongs to
119
+ * @param toolExecutor - Function to execute tools for requests
120
+ */
121
+ async handleIncomingStream(stream, connection, toolExecutor) {
122
+ // CRITICAL: Attach message listener immediately to prevent buffer overflow (libp2p v3)
123
+ const messageHandler = async (event) => {
124
+ try {
125
+ // avoid processing non-olane messages
126
+ if (!event.data) {
127
+ return;
128
+ }
129
+ const message = await this.decodeMessage(event);
130
+ if (typeof message === 'string') {
131
+ // this.logger.warn(
132
+ // 'Received string message on server-side stream, ignoring',
133
+ // message,
134
+ // );
135
+ return;
136
+ }
137
+ if (this.isRequest(message)) {
138
+ await this.handleRequestMessage(message, stream, toolExecutor);
139
+ }
140
+ else if (this.isResponse(message)) {
141
+ this.logger.warn('Received response message on server-side stream, ignoring');
142
+ }
143
+ else {
144
+ this.logger.warn('Received unknown message type', message);
145
+ }
146
+ }
147
+ catch (error) {
148
+ this.logger.error('Error handling stream message:', error);
149
+ // Error already logged, stream will be closed by remote peer or timeout
150
+ }
151
+ };
152
+ const closeHandler = () => {
153
+ this.logger.debug('Stream closed by remote peer');
154
+ stream.removeEventListener('message', messageHandler);
155
+ // stream.removeEventListener('close', closeHandler);
156
+ };
157
+ stream.addEventListener('message', messageHandler);
158
+ // stream.addEventListener('close', closeHandler);
159
+ }
160
+ /**
161
+ * Handles a request message by executing the tool and sending response
162
+ *
163
+ * @param message - The decoded request message
164
+ * @param stream - The stream to send the response on
165
+ * @param toolExecutor - Function to execute the tool
166
+ */
167
+ async handleRequestMessage(message, stream, toolExecutor) {
168
+ const request = new oRequest(message);
169
+ const responseBuilder = ResponseBuilder.create();
170
+ try {
171
+ const result = await toolExecutor(request, stream);
172
+ const response = await responseBuilder.build(request, result, null);
173
+ await CoreUtils.sendResponse(response, stream);
174
+ }
175
+ catch (error) {
176
+ const errorResponse = await responseBuilder.buildError(request, error);
177
+ await CoreUtils.sendResponse(errorResponse, stream);
178
+ }
179
+ }
180
+ /**
181
+ * Handles an outgoing stream on the client side
182
+ * Listens for response messages and emits them via the event emitter
183
+ *
184
+ * @param stream - The outgoing stream
185
+ * @param emitter - Event emitter for chunk events
186
+ * @param config - Configuration including abort signal
187
+ * @returns Promise that resolves with the final response
188
+ */
189
+ async handleOutgoingStream(stream, emitter, config = {}) {
190
+ return new Promise((resolve, reject) => {
191
+ let lastResponse;
192
+ const messageHandler = async (event) => {
193
+ // avoid processing non-olane messages
194
+ if (!event.data) {
195
+ return;
196
+ }
197
+ try {
198
+ const message = await this.decodeMessage(event);
199
+ if (typeof message === 'string') {
200
+ // this.logger.warn(
201
+ // 'Received string message on server-side stream, ignoring',
202
+ // message,
203
+ // );
204
+ return;
205
+ }
206
+ if (this.isResponse(message)) {
207
+ const response = await CoreUtils.processStreamResponse(event);
208
+ // Emit chunk for streaming responses
209
+ emitter.emit('chunk', response);
210
+ // Check if this is the last chunk
211
+ if (response.result._last || !response.result._isStreaming) {
212
+ lastResponse = response;
213
+ cleanup();
214
+ resolve(response);
215
+ }
216
+ }
217
+ else if (this.isRequest(message)) {
218
+ this.logger.warn('Received request message on client-side stream, ignoring');
219
+ }
220
+ else {
221
+ this.logger.warn('Received unknown message type', message);
222
+ }
223
+ }
224
+ catch (error) {
225
+ this.logger.error('Error handling response message:', error);
226
+ cleanup();
227
+ reject(error);
228
+ }
229
+ };
230
+ const closeHandler = () => {
231
+ this.logger.debug('Stream closed by remote peer');
232
+ cleanup();
233
+ if (lastResponse) {
234
+ resolve(lastResponse);
235
+ }
236
+ else {
237
+ reject(new oError(oErrorCodes.TIMEOUT, 'Stream closed before response received'));
238
+ }
239
+ };
240
+ const abortHandler = () => {
241
+ this.logger.debug('Request aborted');
242
+ cleanup();
243
+ try {
244
+ stream.abort(new Error('Request aborted'));
245
+ }
246
+ catch (error) {
247
+ this.logger.debug('Error aborting stream:', error.message);
248
+ }
249
+ reject(new oError(oErrorCodes.TIMEOUT, 'Request aborted'));
250
+ };
251
+ const cleanup = () => {
252
+ stream.removeEventListener('message', messageHandler);
253
+ // stream.removeEventListener('close', closeHandler);
254
+ if (config.signal) {
255
+ config.signal.removeEventListener('abort', abortHandler);
256
+ }
257
+ };
258
+ stream.addEventListener('message', messageHandler);
259
+ // stream.addEventListener('close', closeHandler);
260
+ if (config.signal) {
261
+ config.signal.addEventListener('abort', abortHandler);
262
+ }
263
+ });
264
+ }
265
+ /**
266
+ * Forwards a request to the next hop and relays response chunks back
267
+ * This implements the middleware/proxy pattern for intermediate nodes
268
+ *
269
+ * @param request - The router request to forward
270
+ * @param incomingStream - The stream to send responses back on
271
+ * @param dialFn - Function to dial the next hop connection
272
+ */
273
+ async forwardRequest(request, incomingStream, dialFn) {
274
+ try {
275
+ // Connect to next hop
276
+ const nextHopConnection = await dialFn(request.params.address);
277
+ // Set up chunk relay - forward responses from next hop back to incoming stream
278
+ nextHopConnection.onChunk(async (response) => {
279
+ try {
280
+ await CoreUtils.sendStreamResponse(response, incomingStream);
281
+ }
282
+ catch (error) {
283
+ this.logger.error('Error forwarding chunk:', error);
284
+ }
285
+ });
286
+ // Transmit the request to next hop
287
+ await nextHopConnection.transmit(request);
288
+ }
289
+ catch (error) {
290
+ this.logger.error('Error forwarding request:', error);
291
+ // Send error response back on incoming stream using ResponseBuilder
292
+ const responseBuilder = ResponseBuilder.create();
293
+ const errorResponse = await responseBuilder.buildError(request, error);
294
+ await CoreUtils.sendResponse(errorResponse, incomingStream);
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stream-handler.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.spec.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,309 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { StreamHandler } from './stream-handler.js';
3
+ import { EventEmitter } from 'events';
4
+ import { oError, oErrorCodes } from '@o/o-core/src/error/o-error';
5
+ // Mock Stream interface
6
+ const createMockStream = (status = 'open') => {
7
+ const listeners = new Map();
8
+ return {
9
+ status,
10
+ send: vi.fn().mockReturnValue(true),
11
+ close: vi.fn().mockResolvedValue(undefined),
12
+ abort: vi.fn().mockResolvedValue(undefined),
13
+ onDrain: vi.fn().mockResolvedValue(undefined),
14
+ addEventListener: vi.fn((event, handler) => {
15
+ if (!listeners.has(event)) {
16
+ listeners.set(event, []);
17
+ }
18
+ listeners.get(event)?.push(handler);
19
+ }),
20
+ removeEventListener: vi.fn((event, handler) => {
21
+ const handlers = listeners.get(event);
22
+ if (handlers) {
23
+ const index = handlers.indexOf(handler);
24
+ if (index > -1) {
25
+ handlers.splice(index, 1);
26
+ }
27
+ }
28
+ }),
29
+ // Helper to trigger events in tests
30
+ _triggerEvent: (event, data) => {
31
+ listeners.get(event)?.forEach((handler) => handler(data));
32
+ },
33
+ };
34
+ };
35
+ // Mock Connection interface
36
+ const createMockConnection = (status = 'open') => {
37
+ return {
38
+ status,
39
+ streams: [],
40
+ newStream: vi.fn().mockImplementation(() => createMockStream()),
41
+ };
42
+ };
43
+ describe('StreamHandler', () => {
44
+ let streamHandler;
45
+ beforeEach(() => {
46
+ streamHandler = new StreamHandler();
47
+ });
48
+ describe('Message Type Detection', () => {
49
+ it('should identify a request message', () => {
50
+ const message = {
51
+ jsonrpc: '2.0',
52
+ id: '1',
53
+ method: 'test_method',
54
+ params: {},
55
+ };
56
+ expect(streamHandler.isRequest(message)).toBe(true);
57
+ expect(streamHandler.isResponse(message)).toBe(false);
58
+ });
59
+ it('should identify a response message', () => {
60
+ const message = {
61
+ jsonrpc: '2.0',
62
+ id: '1',
63
+ result: { success: true },
64
+ };
65
+ expect(streamHandler.isResponse(message)).toBe(true);
66
+ expect(streamHandler.isRequest(message)).toBe(false);
67
+ });
68
+ it('should not identify malformed message as request or response', () => {
69
+ const message = {
70
+ jsonrpc: '2.0',
71
+ id: '1',
72
+ };
73
+ expect(streamHandler.isRequest(message)).toBe(false);
74
+ expect(streamHandler.isResponse(message)).toBe(false);
75
+ });
76
+ it('should handle message with both method and result', () => {
77
+ const message = {
78
+ jsonrpc: '2.0',
79
+ id: '1',
80
+ method: 'test',
81
+ result: {},
82
+ };
83
+ // Should identify as request since method is present
84
+ expect(streamHandler.isRequest(message)).toBe(true);
85
+ expect(streamHandler.isResponse(message)).toBe(false);
86
+ });
87
+ });
88
+ describe('Stream Lifecycle - getOrCreateStream', () => {
89
+ it('should create a new stream with none reuse policy', async () => {
90
+ const connection = createMockConnection();
91
+ const protocol = '/test/1.0.0';
92
+ const stream = await streamHandler.getOrCreateStream(connection, protocol, {
93
+ reusePolicy: 'none',
94
+ });
95
+ expect(stream).toBeDefined();
96
+ expect(connection.newStream).toHaveBeenCalledWith(protocol, expect.any(Object));
97
+ });
98
+ it('should reuse existing stream with reuse policy', async () => {
99
+ const existingStream = createMockStream();
100
+ const connection = createMockConnection();
101
+ connection.streams = [existingStream];
102
+ const stream = await streamHandler.getOrCreateStream(connection, '/test/1.0.0', { reusePolicy: 'reuse' });
103
+ expect(stream).toBe(existingStream);
104
+ expect(connection.newStream).not.toHaveBeenCalled();
105
+ });
106
+ it('should create new stream when no open stream exists with reuse policy', async () => {
107
+ const closedStream = createMockStream('closed');
108
+ const connection = createMockConnection();
109
+ connection.streams = [closedStream];
110
+ const stream = await streamHandler.getOrCreateStream(connection, '/test/1.0.0', { reusePolicy: 'reuse' });
111
+ expect(stream).not.toBe(closedStream);
112
+ expect(connection.newStream).toHaveBeenCalled();
113
+ });
114
+ it('should throw error when connection is not open', async () => {
115
+ const connection = createMockConnection('closed');
116
+ await expect(streamHandler.getOrCreateStream(connection, '/test/1.0.0')).rejects.toThrow();
117
+ });
118
+ it('should pass configuration options to newStream', async () => {
119
+ const connection = createMockConnection();
120
+ const signal = new AbortController().signal;
121
+ await streamHandler.getOrCreateStream(connection, '/test/1.0.0', {
122
+ signal,
123
+ maxOutboundStreams: 500,
124
+ runOnLimitedConnection: true,
125
+ });
126
+ expect(connection.newStream).toHaveBeenCalledWith('/test/1.0.0', {
127
+ signal,
128
+ maxOutboundStreams: 500,
129
+ runOnLimitedConnection: true,
130
+ });
131
+ });
132
+ });
133
+ describe('Stream Lifecycle - send', () => {
134
+ it('should send data without backpressure', async () => {
135
+ const stream = createMockStream();
136
+ stream.send = vi.fn().mockReturnValue(true);
137
+ const data = new TextEncoder().encode('test data');
138
+ await streamHandler.send(stream, data);
139
+ expect(stream.send).toHaveBeenCalledWith(data);
140
+ expect(stream.onDrain).not.toHaveBeenCalled();
141
+ });
142
+ it('should wait for drain when backpressure occurs', async () => {
143
+ const stream = createMockStream();
144
+ stream.send = vi.fn().mockReturnValue(false);
145
+ const data = new TextEncoder().encode('test data');
146
+ await streamHandler.send(stream, data, { drainTimeoutMs: 5000 });
147
+ expect(stream.send).toHaveBeenCalledWith(data);
148
+ expect(stream.onDrain).toHaveBeenCalledWith({
149
+ signal: expect.any(AbortSignal),
150
+ });
151
+ });
152
+ });
153
+ describe('Stream Lifecycle - close', () => {
154
+ it('should close stream when reuse policy is none', async () => {
155
+ const stream = createMockStream();
156
+ await streamHandler.close(stream, { reusePolicy: 'none' });
157
+ expect(stream.close).toHaveBeenCalled();
158
+ });
159
+ it('should not close stream when reuse policy is reuse', async () => {
160
+ const stream = createMockStream();
161
+ await streamHandler.close(stream, { reusePolicy: 'reuse' });
162
+ expect(stream.close).not.toHaveBeenCalled();
163
+ });
164
+ it('should not close stream when status is not open', async () => {
165
+ const stream = createMockStream('closed');
166
+ await streamHandler.close(stream, { reusePolicy: 'none' });
167
+ expect(stream.close).not.toHaveBeenCalled();
168
+ });
169
+ it('should handle errors during close gracefully', async () => {
170
+ const stream = createMockStream();
171
+ stream.close = vi.fn().mockRejectedValue(new Error('Close failed'));
172
+ // Should not throw
173
+ await expect(streamHandler.close(stream)).resolves.toBeUndefined();
174
+ });
175
+ });
176
+ describe('Server-side - handleIncomingStream', () => {
177
+ it('should attach message listener immediately', async () => {
178
+ const stream = createMockStream();
179
+ const connection = createMockConnection();
180
+ const toolExecutor = vi.fn().mockResolvedValue({ success: true });
181
+ await streamHandler.handleIncomingStream(stream, connection, toolExecutor);
182
+ expect(stream.addEventListener).toHaveBeenCalledWith('message', expect.any(Function));
183
+ expect(stream.addEventListener).toHaveBeenCalledWith('close', expect.any(Function));
184
+ });
185
+ it('should execute tool when request message is received', async () => {
186
+ const stream = createMockStream();
187
+ const connection = createMockConnection();
188
+ const toolExecutor = vi.fn().mockResolvedValue({ success: true });
189
+ await streamHandler.handleIncomingStream(stream, connection, toolExecutor);
190
+ // Simulate receiving a request
191
+ const requestData = {
192
+ data: new TextEncoder().encode(JSON.stringify({
193
+ jsonrpc: '2.0',
194
+ id: '1',
195
+ method: 'test_method',
196
+ params: {},
197
+ })),
198
+ };
199
+ stream._triggerEvent('message', requestData);
200
+ // Wait for async handling
201
+ await new Promise(resolve => setTimeout(resolve, 10));
202
+ expect(toolExecutor).toHaveBeenCalled();
203
+ });
204
+ it('should send error response when tool execution fails', async () => {
205
+ const stream = createMockStream();
206
+ const connection = createMockConnection();
207
+ const toolExecutor = vi.fn().mockRejectedValue(new oError(oErrorCodes.INTERNAL_ERROR, 'Tool execution failed'));
208
+ await streamHandler.handleIncomingStream(stream, connection, toolExecutor);
209
+ const requestData = {
210
+ data: new TextEncoder().encode(JSON.stringify({
211
+ jsonrpc: '2.0',
212
+ id: '1',
213
+ method: 'test_method',
214
+ params: {},
215
+ })),
216
+ };
217
+ stream._triggerEvent('message', requestData);
218
+ // Wait for async handling
219
+ await new Promise(resolve => setTimeout(resolve, 10));
220
+ expect(stream.send).toHaveBeenCalled();
221
+ });
222
+ it('should clean up listeners on stream close', async () => {
223
+ const stream = createMockStream();
224
+ const connection = createMockConnection();
225
+ const toolExecutor = vi.fn();
226
+ await streamHandler.handleIncomingStream(stream, connection, toolExecutor);
227
+ stream._triggerEvent('close', {});
228
+ expect(stream.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
229
+ expect(stream.removeEventListener).toHaveBeenCalledWith('close', expect.any(Function));
230
+ });
231
+ });
232
+ describe('Client-side - handleOutgoingStream', () => {
233
+ it('should resolve with response when final chunk received', async () => {
234
+ const stream = createMockStream();
235
+ const emitter = new EventEmitter();
236
+ const responsePromise = streamHandler.handleOutgoingStream(stream, emitter);
237
+ // Simulate receiving a final response
238
+ const responseData = {
239
+ data: new TextEncoder().encode(JSON.stringify({
240
+ jsonrpc: '2.0',
241
+ id: '1',
242
+ result: { _last: true, success: true },
243
+ })),
244
+ };
245
+ stream._triggerEvent('message', responseData);
246
+ const response = await responsePromise;
247
+ expect(response).toBeDefined();
248
+ expect(response.result._last).toBe(true);
249
+ });
250
+ it('should emit chunks for streaming responses', async () => {
251
+ const stream = createMockStream();
252
+ const emitter = new EventEmitter();
253
+ const chunkListener = vi.fn();
254
+ emitter.on('chunk', chunkListener);
255
+ const responsePromise = streamHandler.handleOutgoingStream(stream, emitter);
256
+ // Send a chunk
257
+ const chunkData = {
258
+ data: new TextEncoder().encode(JSON.stringify({
259
+ jsonrpc: '2.0',
260
+ id: '1',
261
+ result: { _isStreaming: true, _last: false, data: 'chunk 1' },
262
+ })),
263
+ };
264
+ stream._triggerEvent('message', chunkData);
265
+ // Wait for async handling
266
+ await new Promise(resolve => setTimeout(resolve, 10));
267
+ expect(chunkListener).toHaveBeenCalled();
268
+ // Send final chunk
269
+ const finalData = {
270
+ data: new TextEncoder().encode(JSON.stringify({
271
+ jsonrpc: '2.0',
272
+ id: '1',
273
+ result: { _isStreaming: true, _last: true, data: 'final chunk' },
274
+ })),
275
+ };
276
+ stream._triggerEvent('message', finalData);
277
+ await responsePromise;
278
+ });
279
+ it('should reject when stream closes before response', async () => {
280
+ const stream = createMockStream();
281
+ const emitter = new EventEmitter();
282
+ const responsePromise = streamHandler.handleOutgoingStream(stream, emitter);
283
+ stream._triggerEvent('close', {});
284
+ await expect(responsePromise).rejects.toThrow();
285
+ });
286
+ it('should handle abort signal', async () => {
287
+ const stream = createMockStream();
288
+ const emitter = new EventEmitter();
289
+ const abortController = new AbortController();
290
+ const responsePromise = streamHandler.handleOutgoingStream(stream, emitter, {
291
+ signal: abortController.signal,
292
+ });
293
+ abortController.abort();
294
+ // Wait for abort to be processed
295
+ await new Promise(resolve => setTimeout(resolve, 10));
296
+ await expect(responsePromise).rejects.toThrow();
297
+ expect(stream.abort).toHaveBeenCalled();
298
+ });
299
+ });
300
+ describe('Message Decoding', () => {
301
+ it('should decode Uint8Array message', async () => {
302
+ const testData = { test: 'data' };
303
+ const encoded = new TextEncoder().encode(JSON.stringify(testData));
304
+ const event = { data: encoded };
305
+ const decoded = await streamHandler.decodeMessage(event);
306
+ expect(decoded).toEqual(testData);
307
+ });
308
+ });
309
+ });
@@ -8,6 +8,7 @@ declare const oNodeTool_base: typeof oServerNode;
8
8
  * @returns A new class that extends the base class and implements the oTool interface
9
9
  */
10
10
  export declare class oNodeTool extends oNodeTool_base {
11
+ private streamHandler;
11
12
  handleProtocol(address: oAddress): Promise<void>;
12
13
  initialize(): Promise<void>;
13
14
  handleStream(stream: Stream, connection: Connection): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,QAAQ,EAIT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAIrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAUhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CA2B5D"}
1
+ {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAKrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAUhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CA2B5D"}
@@ -1,8 +1,9 @@
1
- import { CoreUtils, oRequest, ResponseBuilder, ChildJoinedEvent, } from '@olane/o-core';
1
+ import { ChildJoinedEvent, } from '@olane/o-core';
2
2
  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 { StreamHandler } from './connection/stream-handler.js';
6
7
  /**
7
8
  * oTool is a mixin that extends the base class and implements the oTool interface
8
9
  * @param Base - The base class to extend
@@ -20,6 +21,7 @@ export class oNodeTool extends oTool(oServerNode) {
20
21
  }
21
22
  async initialize() {
22
23
  await super.initialize();
24
+ this.streamHandler = new StreamHandler(this.logger);
23
25
  await this.handleProtocol(this.address);
24
26
  if (this.staticAddress &&
25
27
  this.staticAddress?.toString() !== this.address.toString()) {
@@ -28,37 +30,18 @@ export class oNodeTool extends oTool(oServerNode) {
28
30
  }
29
31
  async handleStream(stream, connection) {
30
32
  this.logger.debug('Handling connection: ', connection.id);
31
- // CRITICAL: Attach message listener immediately to prevent buffer overflow (libp2p v3)
32
- // Per libp2p migration guide: "If no message event handler is added, streams will
33
- // buffer incoming data until a pre-configured limit is reached, after which the stream will be reset."
34
- const messageHandler = async (event) => {
35
- if (!event.data) {
36
- this.logger.warn('Malformed event data');
37
- return;
38
- }
39
- const requestConfig = await CoreUtils.processStream(event);
40
- const request = new oRequest(requestConfig);
41
- // Use ResponseBuilder with automatic error handling and metrics tracking
42
- const responseBuilder = ResponseBuilder.create().withMetrics(this.metrics);
43
- let response;
33
+ // Use StreamHandler for consistent stream handling
34
+ // This follows libp2p v3 best practices for immediate message listener attachment
35
+ await this.streamHandler.handleIncomingStream(stream, connection, async (request, stream) => {
44
36
  try {
45
37
  const result = await this.execute(request, stream);
46
- response = await responseBuilder.build(request, result, null);
38
+ // Return the raw result - StreamHandler will build and send the response
39
+ return result;
47
40
  }
48
41
  catch (error) {
49
42
  this.logger.error('Error executing tool: ', request.toString(), error, typeof error);
50
- response = await responseBuilder.buildError(request, error);
43
+ throw error; // StreamHandler will handle error response building
51
44
  }
52
- // Send the response
53
- await CoreUtils.sendResponse(response, stream);
54
- };
55
- // Attach listener synchronously before any async operations
56
- stream.addEventListener('message', messageHandler);
57
- stream.addEventListener('close', () => {
58
- this.logger.debug('Stream closed by remote peer');
59
- stream.close().catch((error) => {
60
- this.logger.error('Error closing stream: ', error);
61
- });
62
45
  });
63
46
  }
64
47
  async _tool_identify() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olane/o-node",
3
- "version": "0.7.12-alpha.51",
3
+ "version": "0.7.12-alpha.53",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -54,12 +54,12 @@
54
54
  "typescript": "5.4.5"
55
55
  },
56
56
  "dependencies": {
57
- "@olane/o-config": "0.7.12-alpha.51",
58
- "@olane/o-core": "0.7.12-alpha.51",
59
- "@olane/o-protocol": "0.7.12-alpha.51",
60
- "@olane/o-tool": "0.7.12-alpha.51",
57
+ "@olane/o-config": "0.7.12-alpha.53",
58
+ "@olane/o-core": "0.7.12-alpha.53",
59
+ "@olane/o-protocol": "0.7.12-alpha.53",
60
+ "@olane/o-tool": "0.7.12-alpha.53",
61
61
  "debug": "^4.4.1",
62
62
  "dotenv": "^16.5.0"
63
63
  },
64
- "gitHead": "b877e1e95a2bf32845ec30072eb72422fd25aba7"
64
+ "gitHead": "f6cd4162b80dd265aba3a8bf322a009fa0ff81e4"
65
65
  }