@olane/o-node 0.7.50 → 0.7.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/connection/index.d.ts +6 -1
- package/dist/src/connection/index.d.ts.map +1 -1
- package/dist/src/connection/index.js +6 -1
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts +16 -0
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -0
- package/dist/src/connection/interfaces/stream-init-message.d.ts +29 -0
- package/dist/src/connection/interfaces/stream-init-message.d.ts.map +1 -0
- package/dist/src/connection/interfaces/stream-init-message.js +8 -0
- package/dist/src/connection/interfaces/stream-manager.config.d.ts +8 -0
- package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +1 -0
- package/dist/src/connection/interfaces/stream-manager.config.js +1 -0
- package/dist/src/connection/o-node-connection.d.ts +5 -7
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +26 -56
- package/dist/src/connection/o-node-connection.manager.d.ts +7 -0
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +23 -5
- package/dist/src/connection/{o-node-connection-stream.d.ts → o-node-stream.d.ts} +18 -6
- package/dist/src/connection/o-node-stream.d.ts.map +1 -0
- package/dist/src/connection/{o-node-connection-stream.js → o-node-stream.js} +20 -2
- package/dist/src/connection/o-node-stream.manager.d.ts +181 -0
- package/dist/src/connection/o-node-stream.manager.d.ts.map +1 -0
- package/dist/src/connection/o-node-stream.manager.js +526 -0
- package/dist/src/connection/stream-manager.events.d.ts +83 -0
- package/dist/src/connection/stream-manager.events.d.ts.map +1 -0
- package/dist/src/connection/stream-manager.events.js +18 -0
- package/dist/src/o-node.tool.d.ts +0 -1
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +30 -20
- package/dist/test/connection-management.spec.js +24 -24
- package/dist/test/helpers/stream-pool-test-helpers.d.ts +1 -0
- package/dist/test/helpers/stream-pool-test-helpers.d.ts.map +1 -0
- package/dist/test/helpers/stream-pool-test-helpers.js +262 -0
- package/dist/test/network-communication.spec.js +68 -66
- package/dist/test/parent-child-registration.spec.js +1 -0
- package/dist/test/stream-pool-manager.spec.d.ts +1 -0
- package/dist/test/stream-pool-manager.spec.d.ts.map +1 -0
- package/dist/test/stream-pool-manager.spec.js +424 -0
- package/package.json +7 -7
- package/dist/src/connection/interfaces/o-node-connection-stream.config.d.ts +0 -8
- package/dist/src/connection/interfaces/o-node-connection-stream.config.d.ts.map +0 -1
- package/dist/src/connection/o-node-connection-stream.d.ts.map +0 -1
- package/dist/src/connection/stream-handler.d.ts +0 -102
- package/dist/src/connection/stream-handler.d.ts.map +0 -1
- package/dist/src/connection/stream-handler.js +0 -357
- /package/dist/src/connection/interfaces/{o-node-connection-stream.config.js → o-node-stream.config.js} +0 -0
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
import { oRequest, oResponse, CoreUtils, oError, oErrorCodes, Logger, ResponseBuilder, } from '@olane/o-core';
|
|
2
|
-
import { lpStream } from '@olane/o-config';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
/**
|
|
5
|
-
* StreamHandler centralizes all stream-related functionality including:
|
|
6
|
-
* - Message type detection (request vs response)
|
|
7
|
-
* - Stream lifecycle management (create, reuse, close)
|
|
8
|
-
* - Backpressure handling
|
|
9
|
-
* - Request/response handling
|
|
10
|
-
* - Stream routing for middleware nodes
|
|
11
|
-
*/
|
|
12
|
-
export class StreamHandler {
|
|
13
|
-
constructor(logger) {
|
|
14
|
-
this.logger = logger ?? new Logger('StreamHandler');
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Detects if a decoded message is a request
|
|
18
|
-
* Requests have a 'method' field and no 'result' field
|
|
19
|
-
*/
|
|
20
|
-
isRequest(message) {
|
|
21
|
-
return typeof message?.method === 'string' && message.result === undefined;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Detects if a decoded message is a response
|
|
25
|
-
* Responses have a 'result' field and no 'method' field
|
|
26
|
-
*/
|
|
27
|
-
isResponse(message) {
|
|
28
|
-
return message?.result !== undefined && message.method === undefined;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Extracts and parses JSON from various formats including:
|
|
32
|
-
* - Already parsed objects
|
|
33
|
-
* - Plain JSON
|
|
34
|
-
* - Markdown code blocks (```json ... ``` or ``` ... ```)
|
|
35
|
-
* - Mixed content with explanatory text
|
|
36
|
-
* - JSON5 format (trailing commas, comments, unquoted keys, etc.)
|
|
37
|
-
*
|
|
38
|
-
* @param decoded - The decoded string that may contain JSON, or an already parsed object
|
|
39
|
-
* @returns Parsed JSON object
|
|
40
|
-
* @throws Error if JSON parsing fails even with JSON5 fallback
|
|
41
|
-
*/
|
|
42
|
-
extractAndParseJSON(decoded) {
|
|
43
|
-
// If already an object (not a string), return it directly
|
|
44
|
-
if (typeof decoded !== 'string') {
|
|
45
|
-
return decoded;
|
|
46
|
-
}
|
|
47
|
-
let jsonString = decoded.trim();
|
|
48
|
-
// Attempt standard JSON.parse first
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(jsonString);
|
|
51
|
-
}
|
|
52
|
-
catch (jsonError) {
|
|
53
|
-
this.logger.debug('Standard JSON parse failed, trying JSON5', {
|
|
54
|
-
error: jsonError.message,
|
|
55
|
-
position: jsonError.message.match(/position (\d+)/)?.[1],
|
|
56
|
-
preview: jsonString,
|
|
57
|
-
});
|
|
58
|
-
// Fallback to JSON5 for more relaxed parsing
|
|
59
|
-
try {
|
|
60
|
-
return JSON5.parse(jsonString);
|
|
61
|
-
}
|
|
62
|
-
catch (json5Error) {
|
|
63
|
-
// Enhanced error with context
|
|
64
|
-
this.logger.error('JSON5 parse also failed', {
|
|
65
|
-
originalError: jsonError.message,
|
|
66
|
-
json5Error: json5Error.message,
|
|
67
|
-
preview: jsonString.substring(0, 200),
|
|
68
|
-
length: jsonString.length,
|
|
69
|
-
});
|
|
70
|
-
throw new Error(`Failed to parse JSON: ${jsonError.message}\nJSON5 also failed: ${json5Error.message}\nPreview: ${jsonString.substring(0, 200)}${jsonString.length > 200 ? '...' : ''}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Builds a bidirectional cache key from caller and receiver addresses
|
|
76
|
-
* The key is symmetric: A↔B === B↔A
|
|
77
|
-
*
|
|
78
|
-
* @param callerAddress - The caller's address
|
|
79
|
-
* @param receiverAddress - The receiver's address
|
|
80
|
-
* @returns Cache key string
|
|
81
|
-
*/
|
|
82
|
-
buildCacheKey(callerAddress, receiverAddress) {
|
|
83
|
-
const addresses = [callerAddress.value, receiverAddress.value].sort();
|
|
84
|
-
return `${addresses[0]}↔${addresses[1]}`;
|
|
85
|
-
}
|
|
86
|
-
// /**
|
|
87
|
-
// * Gets an existing open stream or creates a new one based on reuse policy
|
|
88
|
-
// *
|
|
89
|
-
// * @param connection - The libp2p connection
|
|
90
|
-
// * @param protocol - The protocol to use for the stream
|
|
91
|
-
// * @param config - Stream handler configuration
|
|
92
|
-
// * @param streamAddresses - Optional addresses for address-based stream reuse
|
|
93
|
-
// */
|
|
94
|
-
// async getOrCreateStream(
|
|
95
|
-
// connection: Connection,
|
|
96
|
-
// protocol: string,
|
|
97
|
-
// config: StreamHandlerConfig = {},
|
|
98
|
-
// streamAddresses?: {
|
|
99
|
-
// callerAddress: oAddress;
|
|
100
|
-
// receiverAddress: oAddress;
|
|
101
|
-
// direction: 'inbound' | 'outbound';
|
|
102
|
-
// },
|
|
103
|
-
// ): Promise<Stream> {
|
|
104
|
-
// this.logger.debug(
|
|
105
|
-
// `Getting or creating stream for protocol: ${protocol}, connection`,
|
|
106
|
-
// {
|
|
107
|
-
// status: connection.status,
|
|
108
|
-
// remoteAddr: connection.remoteAddr.toString(),
|
|
109
|
-
// streamCount: connection.streams.length,
|
|
110
|
-
// reusePolicy: config.reusePolicy ?? 'none',
|
|
111
|
-
// hasAddresses: !!streamAddresses,
|
|
112
|
-
// },
|
|
113
|
-
// );
|
|
114
|
-
// if (connection.status !== 'open') {
|
|
115
|
-
// throw new oError(oErrorCodes.INVALID_STATE, 'Connection not open');
|
|
116
|
-
// }
|
|
117
|
-
// const reusePolicy = config.reusePolicy ?? 'none';
|
|
118
|
-
// // Check address-based cache if reuse is enabled and addresses provided
|
|
119
|
-
// if (reusePolicy === 'reuse' && streamAddresses) {
|
|
120
|
-
// const cacheKey = this.buildCacheKey(
|
|
121
|
-
// streamAddresses.callerAddress,
|
|
122
|
-
// streamAddresses.receiverAddress,
|
|
123
|
-
// );
|
|
124
|
-
// const cachedStream = this.streamCache.get(cacheKey);
|
|
125
|
-
// if (cachedStream?.isReusable) {
|
|
126
|
-
// this.logger.debug('Reusing cached stream by address', {
|
|
127
|
-
// cacheKey,
|
|
128
|
-
// streamId: cachedStream.stream.id,
|
|
129
|
-
// direction: cachedStream.direction,
|
|
130
|
-
// });
|
|
131
|
-
// cachedStream.updateLastUsed();
|
|
132
|
-
// return cachedStream.stream;
|
|
133
|
-
// } else if (cachedStream) {
|
|
134
|
-
// // Stream exists but not reusable, remove from cache
|
|
135
|
-
// this.logger.debug('Removing non-reusable stream from cache', {
|
|
136
|
-
// cacheKey,
|
|
137
|
-
// status: cachedStream.stream.status,
|
|
138
|
-
// });
|
|
139
|
-
// this.streamCache.delete(cacheKey);
|
|
140
|
-
// }
|
|
141
|
-
// }
|
|
142
|
-
// this.logger.debug(
|
|
143
|
-
// 'No reusable cached stream found, checking connection streams',
|
|
144
|
-
// connection.streams.map((s) => ({
|
|
145
|
-
// status: s.status,
|
|
146
|
-
// protocol: s.protocol,
|
|
147
|
-
// writeStatus: s.writeStatus,
|
|
148
|
-
// remoteReadStatus: s.remoteReadStatus,
|
|
149
|
-
// id: s.id,
|
|
150
|
-
// })),
|
|
151
|
-
// );
|
|
152
|
-
// // Fallback to protocol-based check (legacy behavior)
|
|
153
|
-
// if (reusePolicy === 'reuse' && !streamAddresses) {
|
|
154
|
-
// const existingStream = connection.streams.find(
|
|
155
|
-
// (stream) =>
|
|
156
|
-
// stream.status === 'open' &&
|
|
157
|
-
// stream.protocol === protocol &&
|
|
158
|
-
// stream.writeStatus === 'writable' &&
|
|
159
|
-
// stream.remoteReadStatus === 'readable',
|
|
160
|
-
// );
|
|
161
|
-
// if (existingStream) {
|
|
162
|
-
// this.logger.debug(
|
|
163
|
-
// 'Reusing existing stream by protocol (legacy)',
|
|
164
|
-
// existingStream.id,
|
|
165
|
-
// existingStream.direction,
|
|
166
|
-
// );
|
|
167
|
-
// return existingStream;
|
|
168
|
-
// }
|
|
169
|
-
// }
|
|
170
|
-
// // Create new stream
|
|
171
|
-
// this.logger.debug('Creating new stream', { protocol });
|
|
172
|
-
// const stream = await connection.newStream(protocol, {
|
|
173
|
-
// signal: config.signal,
|
|
174
|
-
// maxOutboundStreams: config.maxOutboundStreams ?? 1000,
|
|
175
|
-
// runOnLimitedConnection: config.runOnLimitedConnection ?? false,
|
|
176
|
-
// });
|
|
177
|
-
// // Cache the stream if reuse is enabled and addresses are provided
|
|
178
|
-
// if (reusePolicy === 'reuse' && streamAddresses) {
|
|
179
|
-
// const managedStream = new oNodeConnectionStream(
|
|
180
|
-
// stream,
|
|
181
|
-
// streamAddresses.callerAddress,
|
|
182
|
-
// streamAddresses.receiverAddress,
|
|
183
|
-
// streamAddresses.direction,
|
|
184
|
-
// );
|
|
185
|
-
// this.cacheStream(managedStream);
|
|
186
|
-
// this.setupStreamCleanup(stream);
|
|
187
|
-
// }
|
|
188
|
-
// return stream;
|
|
189
|
-
// }
|
|
190
|
-
/**
|
|
191
|
-
* Sends data through a stream using length-prefixed encoding (libp2p v3 best practice)
|
|
192
|
-
* Each message is automatically prefixed with a varint indicating the message length
|
|
193
|
-
* This ensures proper message boundaries and eliminates concatenation issues
|
|
194
|
-
*
|
|
195
|
-
* @param stream - The stream to send data through
|
|
196
|
-
* @param data - The data to send
|
|
197
|
-
* @param config - Configuration for timeout and other options
|
|
198
|
-
*/
|
|
199
|
-
async sendLengthPrefixed(stream, data, config = {}) {
|
|
200
|
-
const lp = lpStream(stream);
|
|
201
|
-
await lp.write(data, { signal: config.signal });
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Handles an incoming stream on the server side using length-prefixed protocol
|
|
205
|
-
* Uses async read loops instead of event listeners (libp2p v3 best practice)
|
|
206
|
-
* Processes complete messages with proper boundaries
|
|
207
|
-
*
|
|
208
|
-
* @param stream - The incoming stream
|
|
209
|
-
* @param connection - The connection the stream belongs to
|
|
210
|
-
* @param toolExecutor - Function to execute tools for requests
|
|
211
|
-
*/
|
|
212
|
-
async handleIncomingStream(stream, connection, toolExecutor) {
|
|
213
|
-
const lp = lpStream(stream);
|
|
214
|
-
try {
|
|
215
|
-
while (stream.status === 'open') {
|
|
216
|
-
// Read complete length-prefixed message
|
|
217
|
-
const messageBytes = await lp.read();
|
|
218
|
-
const decoded = new TextDecoder().decode(messageBytes.subarray());
|
|
219
|
-
// Parse JSON (handles markdown blocks, mixed content, and JSON5)
|
|
220
|
-
const message = this.extractAndParseJSON(decoded);
|
|
221
|
-
if (this.isRequest(message)) {
|
|
222
|
-
await this.handleRequestMessage(message, stream, toolExecutor);
|
|
223
|
-
}
|
|
224
|
-
else if (this.isResponse(message)) {
|
|
225
|
-
this.logger.warn('Received response message on server-side stream, ignoring', message);
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
this.logger.warn('Received unknown message type', message);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
// Stream closed or error occurred
|
|
234
|
-
if (stream.status === 'open') {
|
|
235
|
-
this.logger.error('Error in length-prefixed stream handler:', error);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Handles a request message by executing the tool and sending response
|
|
241
|
-
*
|
|
242
|
-
* @param message - The decoded request message
|
|
243
|
-
* @param stream - The stream to send the response on
|
|
244
|
-
* @param toolExecutor - Function to execute the tool
|
|
245
|
-
*/
|
|
246
|
-
async handleRequestMessage(message, stream, toolExecutor) {
|
|
247
|
-
const request = new oRequest(message);
|
|
248
|
-
const responseBuilder = ResponseBuilder.create();
|
|
249
|
-
try {
|
|
250
|
-
// this.logger.debug(
|
|
251
|
-
// `Processing request on stream: method=${request.method}, id=${request.id}`,
|
|
252
|
-
// );
|
|
253
|
-
const result = await toolExecutor(request, stream);
|
|
254
|
-
const response = await responseBuilder.build(request, result, null);
|
|
255
|
-
await CoreUtils.sendResponse(response, stream);
|
|
256
|
-
this.logger.debug(`Successfully processed request: method=${request.method}, id=${request.id}`);
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
this.logger.error(`Error processing request: method=${request.method}, id=${request.id}`, error);
|
|
260
|
-
const errorResponse = await responseBuilder.buildError(request, error);
|
|
261
|
-
await CoreUtils.sendResponse(errorResponse, stream);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Handles an outgoing stream on the client side using length-prefixed protocol
|
|
266
|
-
* Uses async read loops to process responses with proper message boundaries
|
|
267
|
-
*
|
|
268
|
-
* @param stream - The outgoing stream
|
|
269
|
-
* @param emitter - Event emitter for chunk events
|
|
270
|
-
* @param config - Configuration including abort signal
|
|
271
|
-
* @param requestHandler - Optional handler for processing router requests received on this stream
|
|
272
|
-
* @param requestId - Optional request ID to filter responses (for stream reuse scenarios)
|
|
273
|
-
* @returns Promise that resolves with the final response
|
|
274
|
-
*/
|
|
275
|
-
async handleOutgoingStream(stream, emitter, config = {}, requestHandler, requestId) {
|
|
276
|
-
const lp = lpStream(stream);
|
|
277
|
-
try {
|
|
278
|
-
while (stream.status === 'open') {
|
|
279
|
-
this.logger.debug('Waiting for response...');
|
|
280
|
-
// Read complete length-prefixed message
|
|
281
|
-
const messageBytes = await lp.read({ signal: config.signal });
|
|
282
|
-
const decoded = new TextDecoder().decode(messageBytes.subarray());
|
|
283
|
-
// Parse JSON (handles markdown blocks, mixed content, and JSON5)
|
|
284
|
-
const message = this.extractAndParseJSON(decoded);
|
|
285
|
-
if (this.isResponse(message)) {
|
|
286
|
-
const response = new oResponse({
|
|
287
|
-
...message.result,
|
|
288
|
-
id: message.id,
|
|
289
|
-
});
|
|
290
|
-
// Filter by request ID if provided
|
|
291
|
-
if (requestId !== undefined && response.id !== requestId) {
|
|
292
|
-
this.logger.debug(`Ignoring response for different request (expected: ${requestId}, received: ${response.id})`);
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
// Emit chunk for streaming responses
|
|
296
|
-
emitter.emit('chunk', response);
|
|
297
|
-
// Check if this is the last chunk
|
|
298
|
-
if (response.result._last || !response.result._isStreaming) {
|
|
299
|
-
return response;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else if (this.isRequest(message)) {
|
|
303
|
-
// Process incoming router requests if handler is provided
|
|
304
|
-
if (requestHandler) {
|
|
305
|
-
this.logger.debug('Received router request on client-side stream, processing...', message);
|
|
306
|
-
await this.handleRequestMessage(message, stream, requestHandler);
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
this.logger.warn('Received request message on client-side stream, ignoring (no handler)', message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
this.logger.warn('Received unknown message type', message);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
throw new oError(oErrorCodes.TIMEOUT, 'Stream closed before response received');
|
|
317
|
-
}
|
|
318
|
-
catch (error) {
|
|
319
|
-
if (config.signal?.aborted) {
|
|
320
|
-
throw new oError(oErrorCodes.TIMEOUT, 'Request aborted');
|
|
321
|
-
}
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Forwards a request to the next hop and relays response chunks back
|
|
327
|
-
* This implements the middleware/proxy pattern for intermediate nodes
|
|
328
|
-
*
|
|
329
|
-
* @param request - The router request to forward
|
|
330
|
-
* @param incomingStream - The stream to send responses back on
|
|
331
|
-
* @param dialFn - Function to dial the next hop connection
|
|
332
|
-
*/
|
|
333
|
-
async forwardRequest(request, incomingStream, dialFn) {
|
|
334
|
-
try {
|
|
335
|
-
// Connect to next hop
|
|
336
|
-
const nextHopConnection = await dialFn(request.params.address);
|
|
337
|
-
// Set up chunk relay - forward responses from next hop back to incoming stream
|
|
338
|
-
nextHopConnection.onChunk(async (response) => {
|
|
339
|
-
try {
|
|
340
|
-
await CoreUtils.sendResponse(response, incomingStream);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
this.logger.error('Error forwarding chunk:', error);
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
// Transmit the request to next hop
|
|
347
|
-
await nextHopConnection.transmit(request);
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
this.logger.error('Error forwarding request:', error);
|
|
351
|
-
// Send error response back on incoming stream using ResponseBuilder
|
|
352
|
-
const responseBuilder = ResponseBuilder.create();
|
|
353
|
-
const errorResponse = await responseBuilder.buildError(request, error);
|
|
354
|
-
await CoreUtils.sendResponse(errorResponse, incomingStream);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
File without changes
|