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