@olane/o-node 0.7.49 → 0.7.51
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 +4 -0
- package/dist/src/connection/index.d.ts.map +1 -1
- package/dist/src/connection/index.js +4 -0
- package/dist/src/connection/interfaces/o-node-connection-stream.config.d.ts +8 -0
- package/dist/src/connection/interfaces/o-node-connection-stream.config.d.ts.map +1 -1
- package/dist/src/connection/interfaces/stream-pool-manager.config.d.ts +41 -0
- package/dist/src/connection/interfaces/stream-pool-manager.config.d.ts.map +1 -0
- package/dist/src/connection/interfaces/stream-pool-manager.config.js +1 -0
- package/dist/src/connection/o-node-connection-stream.d.ts +12 -0
- package/dist/src/connection/o-node-connection-stream.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection-stream.js +18 -0
- package/dist/src/connection/stream-pool-manager.d.ts +86 -0
- package/dist/src/connection/stream-pool-manager.d.ts.map +1 -0
- package/dist/src/connection/stream-pool-manager.events.d.ts +57 -0
- package/dist/src/connection/stream-pool-manager.events.d.ts.map +1 -0
- package/dist/src/connection/stream-pool-manager.events.js +14 -0
- package/dist/src/connection/stream-pool-manager.js +356 -0
- package/dist/src/interfaces/i-registrable-node.d.ts +7 -0
- package/dist/src/interfaces/i-registrable-node.d.ts.map +1 -1
- package/dist/src/managers/o-registration.manager.d.ts.map +1 -1
- package/dist/src/managers/o-registration.manager.js +11 -1
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +16 -13
- package/dist/test/connection-management.spec.js +24 -24
- package/dist/test/helpers/stream-pool-test-helpers.d.ts +76 -0
- package/dist/test/helpers/stream-pool-test-helpers.d.ts.map +1 -0
- package/dist/test/helpers/stream-pool-test-helpers.js +229 -0
- package/dist/test/network-communication.spec.js +68 -66
- 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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { oObject, oError, oErrorCodes } from '@olane/o-core';
|
|
3
|
+
import { StreamPoolEvent, } from './stream-pool-manager.events.js';
|
|
4
|
+
/**
|
|
5
|
+
* StreamPoolManager manages a pool of reusable streams with automatic recovery.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Stream[0]: Dedicated reader for incoming requests (background loop)
|
|
9
|
+
* - Streams[1-N]: Round-robin pool for outgoing request-response cycles
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Automatic stream pool initialization
|
|
13
|
+
* - Dedicated reader with automatic restart on failure
|
|
14
|
+
* - Event-based stream monitoring (listens to 'close' events)
|
|
15
|
+
* - Automatic stream replacement when failures detected
|
|
16
|
+
* - Event emission for monitoring and observability
|
|
17
|
+
*/
|
|
18
|
+
export class StreamPoolManager extends oObject {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
this.streams = [];
|
|
22
|
+
this.currentStreamIndex = 1; // Start from 1 (skip reader)
|
|
23
|
+
this.failureCount = 0;
|
|
24
|
+
this.eventEmitter = new EventEmitter();
|
|
25
|
+
this.streamEventHandlers = new Map();
|
|
26
|
+
this.isInitialized = false;
|
|
27
|
+
this.isClosing = false;
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.POOL_SIZE = config.poolSize || 10;
|
|
30
|
+
this.READER_STREAM_INDEX = config.readerStreamIndex ?? 0;
|
|
31
|
+
if (this.POOL_SIZE < 2) {
|
|
32
|
+
throw new Error('Pool size must be at least 2 (1 reader + 1 request-response)');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
get backgroundReaderActive() {
|
|
36
|
+
return this.dedicatedReaderStream?.isValid || false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize the stream pool and start the dedicated reader
|
|
40
|
+
*/
|
|
41
|
+
async initialize() {
|
|
42
|
+
if (this.isInitialized) {
|
|
43
|
+
this.logger.debug('Stream pool already initialized');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.logger.info('Initializing stream pool', {
|
|
47
|
+
poolSize: this.POOL_SIZE,
|
|
48
|
+
readerIndex: this.READER_STREAM_INDEX,
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
// Spawn all streams in parallel
|
|
52
|
+
const streamPromises = [];
|
|
53
|
+
for (let i = 0; i < this.POOL_SIZE; i++) {
|
|
54
|
+
streamPromises.push(this.config.createStream());
|
|
55
|
+
}
|
|
56
|
+
this.streams = await Promise.all(streamPromises);
|
|
57
|
+
// Set stream types and attach event listeners
|
|
58
|
+
for (let i = 0; i < this.streams.length; i++) {
|
|
59
|
+
const stream = this.streams[i];
|
|
60
|
+
if (i === this.READER_STREAM_INDEX) {
|
|
61
|
+
stream.config.streamType = 'dedicated-reader';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
stream.config.streamType = 'request-response';
|
|
65
|
+
}
|
|
66
|
+
// Attach event listeners for monitoring
|
|
67
|
+
this.attachStreamListeners(stream, i);
|
|
68
|
+
}
|
|
69
|
+
// Initialize dedicated reader
|
|
70
|
+
await this.initializeBackgroundReader();
|
|
71
|
+
this.isInitialized = true;
|
|
72
|
+
this.emit(StreamPoolEvent.PoolInitialized, {
|
|
73
|
+
poolSize: this.streams.length,
|
|
74
|
+
});
|
|
75
|
+
this.logger.info('Stream pool initialized successfully', {
|
|
76
|
+
totalStreams: this.streams.length,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.logger.error('Failed to initialize stream pool:', error);
|
|
81
|
+
throw new oError(oErrorCodes.INTERNAL_ERROR, `Failed to initialize stream pool: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get a stream for outgoing request-response cycles using round-robin selection
|
|
86
|
+
*/
|
|
87
|
+
async getStream() {
|
|
88
|
+
if (!this.isInitialized) {
|
|
89
|
+
throw new oError(oErrorCodes.INVALID_STATE, 'Stream pool not initialized. Call initialize() first.');
|
|
90
|
+
}
|
|
91
|
+
if (this.streams.length < this.POOL_SIZE) {
|
|
92
|
+
this.logger.warn('Stream pool not at full capacity', {
|
|
93
|
+
current: this.streams.length,
|
|
94
|
+
expected: this.POOL_SIZE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Round-robin selection from request-response pool (skip reader stream)
|
|
98
|
+
const requestResponsePoolSize = this.POOL_SIZE - 1;
|
|
99
|
+
const selectedIndex = ((this.currentStreamIndex - 1) % requestResponsePoolSize) + 1;
|
|
100
|
+
this.currentStreamIndex =
|
|
101
|
+
selectedIndex === this.POOL_SIZE - 1 ? 1 : selectedIndex + 1;
|
|
102
|
+
const selectedStream = this.streams[selectedIndex];
|
|
103
|
+
// Validate stream health before returning
|
|
104
|
+
if (!selectedStream.isValid) {
|
|
105
|
+
this.logger.warn('Selected stream is not valid, attempting recovery', {
|
|
106
|
+
index: selectedIndex,
|
|
107
|
+
});
|
|
108
|
+
await this.replaceStream(selectedIndex);
|
|
109
|
+
return this.streams[selectedIndex];
|
|
110
|
+
}
|
|
111
|
+
return selectedStream;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get statistics about the stream pool
|
|
115
|
+
*/
|
|
116
|
+
getStats() {
|
|
117
|
+
const healthyStreams = this.streams.filter((s) => s.isValid).length;
|
|
118
|
+
const requestResponseStreams = this.streams.filter((s) => s.isRequestResponse).length;
|
|
119
|
+
let readerStreamHealth = 'not-initialized';
|
|
120
|
+
if (this.dedicatedReaderStream) {
|
|
121
|
+
readerStreamHealth = this.dedicatedReaderStream.isValid
|
|
122
|
+
? 'healthy'
|
|
123
|
+
: 'unhealthy';
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
totalStreams: this.streams.length,
|
|
127
|
+
healthyStreams,
|
|
128
|
+
readerStreamHealth,
|
|
129
|
+
requestResponseStreams,
|
|
130
|
+
failureCount: this.failureCount,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Close the stream pool and cleanup resources
|
|
135
|
+
*/
|
|
136
|
+
async close() {
|
|
137
|
+
if (this.isClosing) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.isClosing = true;
|
|
141
|
+
this.logger.info('Closing stream pool', {
|
|
142
|
+
totalStreams: this.streams.length,
|
|
143
|
+
});
|
|
144
|
+
// Remove all event listeners from streams
|
|
145
|
+
for (const stream of this.streams) {
|
|
146
|
+
this.removeStreamListeners(stream);
|
|
147
|
+
}
|
|
148
|
+
// Close all streams (though they may not actually close due to reuse policy)
|
|
149
|
+
await Promise.all(this.streams.map((s) => s.close()));
|
|
150
|
+
this.streams = [];
|
|
151
|
+
this.dedicatedReaderStream = undefined;
|
|
152
|
+
this.streamEventHandlers.clear();
|
|
153
|
+
this.isInitialized = false;
|
|
154
|
+
this.isClosing = false;
|
|
155
|
+
this.emit(StreamPoolEvent.PoolClosed);
|
|
156
|
+
this.logger.info('Stream pool closed');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Add event listener
|
|
160
|
+
*/
|
|
161
|
+
on(event, listener) {
|
|
162
|
+
this.eventEmitter.on(event, listener);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Remove event listener
|
|
166
|
+
*/
|
|
167
|
+
off(event, listener) {
|
|
168
|
+
this.eventEmitter.off(event, listener);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Emit event
|
|
172
|
+
*/
|
|
173
|
+
emit(event, data) {
|
|
174
|
+
this.eventEmitter.emit(event, data);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Initialize the background reader on the dedicated stream
|
|
178
|
+
*/
|
|
179
|
+
async initializeBackgroundReader() {
|
|
180
|
+
if (this.backgroundReaderActive) {
|
|
181
|
+
this.logger.debug('Background reader already active');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const readerStream = this.streams[this.READER_STREAM_INDEX];
|
|
185
|
+
if (!readerStream) {
|
|
186
|
+
throw new oError(oErrorCodes.INTERNAL_ERROR, 'Reader stream not found in pool');
|
|
187
|
+
}
|
|
188
|
+
if (!readerStream.isValid) {
|
|
189
|
+
this.logger.warn('Reader stream is not valid, replacing it');
|
|
190
|
+
await this.replaceStream(this.READER_STREAM_INDEX);
|
|
191
|
+
return this.initializeBackgroundReader();
|
|
192
|
+
}
|
|
193
|
+
this.dedicatedReaderStream = readerStream;
|
|
194
|
+
this.logger.info('Starting background reader', {
|
|
195
|
+
streamId: readerStream.p2pStream.id,
|
|
196
|
+
});
|
|
197
|
+
// Get the raw p2p stream
|
|
198
|
+
const p2pStream = readerStream.p2pStream;
|
|
199
|
+
// Start the persistent read loop in the background
|
|
200
|
+
this.config.streamHandler
|
|
201
|
+
.handleIncomingStream(p2pStream, this.config.p2pConnection, async (request, s) => {
|
|
202
|
+
this.logger.debug('Background reader received request:', request.method);
|
|
203
|
+
if (this.config.requestHandler) {
|
|
204
|
+
return await this.config.requestHandler(request, s);
|
|
205
|
+
}
|
|
206
|
+
throw new Error('No request handler configured for incoming requests');
|
|
207
|
+
})
|
|
208
|
+
.catch((error) => {
|
|
209
|
+
this.logger.warn('Background reader exited:', error?.message || 'stream closed');
|
|
210
|
+
this.failureCount++;
|
|
211
|
+
this.emit(StreamPoolEvent.ReaderFailed, {
|
|
212
|
+
error: error?.message,
|
|
213
|
+
failureCount: this.failureCount,
|
|
214
|
+
});
|
|
215
|
+
// Attempt automatic recovery
|
|
216
|
+
this.handleReaderFailure().catch((recoveryError) => {
|
|
217
|
+
this.logger.error('Failed to recover from reader failure:', recoveryError);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
this.emit(StreamPoolEvent.ReaderStarted, {
|
|
221
|
+
streamId: readerStream.p2pStream.id,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Handle dedicated reader failure with automatic recovery
|
|
226
|
+
*/
|
|
227
|
+
async handleReaderFailure() {
|
|
228
|
+
if (this.isClosing) {
|
|
229
|
+
this.logger.debug('Pool is closing, skipping reader recovery');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.logger.warn('Attempting to recover from reader failure');
|
|
233
|
+
try {
|
|
234
|
+
// Replace the failed reader stream
|
|
235
|
+
await this.replaceStream(this.READER_STREAM_INDEX);
|
|
236
|
+
// Reinitialize the background reader
|
|
237
|
+
await this.initializeBackgroundReader();
|
|
238
|
+
this.logger.info('Successfully recovered from reader failure');
|
|
239
|
+
this.emit(StreamPoolEvent.ReaderRecovered, {
|
|
240
|
+
failureCount: this.failureCount,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
this.logger.error('Failed to recover from reader failure:', error);
|
|
245
|
+
this.emit(StreamPoolEvent.RecoveryFailed, {
|
|
246
|
+
error: error.message,
|
|
247
|
+
failureCount: this.failureCount,
|
|
248
|
+
});
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Replace a stream at the given index
|
|
254
|
+
*/
|
|
255
|
+
async replaceStream(index) {
|
|
256
|
+
this.logger.info('Replacing stream', { index });
|
|
257
|
+
try {
|
|
258
|
+
const oldStream = this.streams[index];
|
|
259
|
+
const streamType = oldStream?.streamType || 'general';
|
|
260
|
+
// Remove event listeners from old stream
|
|
261
|
+
if (oldStream) {
|
|
262
|
+
this.removeStreamListeners(oldStream);
|
|
263
|
+
}
|
|
264
|
+
// Create new stream
|
|
265
|
+
const newStream = await this.config.createStream();
|
|
266
|
+
newStream.config.streamType = streamType;
|
|
267
|
+
// Replace in pool
|
|
268
|
+
this.streams[index] = newStream;
|
|
269
|
+
// Attach event listeners to new stream
|
|
270
|
+
this.attachStreamListeners(newStream, index);
|
|
271
|
+
// Close old stream (if it exists and is valid)
|
|
272
|
+
if (oldStream) {
|
|
273
|
+
try {
|
|
274
|
+
await oldStream.close();
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
this.logger.debug('Error closing old stream:', error.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.emit(StreamPoolEvent.StreamReplaced, { index, streamType });
|
|
281
|
+
this.logger.info('Stream replaced successfully', { index, streamType });
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
this.logger.error('Failed to replace stream:', error);
|
|
285
|
+
throw new oError(oErrorCodes.INTERNAL_ERROR, `Failed to replace stream at index ${index}: ${error.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Attach event listeners to a stream for monitoring
|
|
290
|
+
*/
|
|
291
|
+
attachStreamListeners(stream, index) {
|
|
292
|
+
const streamId = stream.p2pStream.id;
|
|
293
|
+
// Remove any existing listeners for this stream
|
|
294
|
+
this.removeStreamListeners(stream);
|
|
295
|
+
// Create close event handler
|
|
296
|
+
const closeHandler = (event) => {
|
|
297
|
+
this.handleStreamClose(index, event);
|
|
298
|
+
};
|
|
299
|
+
// Attach listener to underlying p2p stream
|
|
300
|
+
stream.p2pStream.addEventListener('close', closeHandler);
|
|
301
|
+
// Store handler for cleanup
|
|
302
|
+
this.streamEventHandlers.set(streamId, closeHandler);
|
|
303
|
+
this.logger.debug('Attached stream listeners', { index, streamId });
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Remove event listeners from a stream
|
|
307
|
+
*/
|
|
308
|
+
removeStreamListeners(stream) {
|
|
309
|
+
const streamId = stream.p2pStream.id;
|
|
310
|
+
const handler = this.streamEventHandlers.get(streamId);
|
|
311
|
+
if (handler) {
|
|
312
|
+
stream.p2pStream.removeEventListener('close', handler);
|
|
313
|
+
this.streamEventHandlers.delete(streamId);
|
|
314
|
+
this.logger.debug('Removed stream listeners', { streamId });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Handle stream close event - triggers immediate replacement
|
|
319
|
+
*/
|
|
320
|
+
handleStreamClose(index, event) {
|
|
321
|
+
const stream = this.streams[index];
|
|
322
|
+
const streamId = stream?.p2pStream.id;
|
|
323
|
+
const isReaderStream = index === this.READER_STREAM_INDEX;
|
|
324
|
+
this.logger.warn('Stream closed event detected', {
|
|
325
|
+
index,
|
|
326
|
+
streamId,
|
|
327
|
+
isReaderStream,
|
|
328
|
+
error: event?.error?.message,
|
|
329
|
+
});
|
|
330
|
+
this.failureCount++;
|
|
331
|
+
// Special handling for dedicated reader stream
|
|
332
|
+
if (isReaderStream) {
|
|
333
|
+
this.emit(StreamPoolEvent.ReaderFailed, {
|
|
334
|
+
error: event?.error?.message || 'stream closed',
|
|
335
|
+
failureCount: this.failureCount,
|
|
336
|
+
});
|
|
337
|
+
// Attempt automatic recovery
|
|
338
|
+
this.handleReaderFailure().catch((recoveryError) => {
|
|
339
|
+
this.logger.error('Failed to recover from reader failure:', recoveryError);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Request-response stream failure
|
|
344
|
+
this.emit(StreamPoolEvent.StreamFailed, {
|
|
345
|
+
index,
|
|
346
|
+
streamId,
|
|
347
|
+
error: event?.error?.message,
|
|
348
|
+
failureCount: this.failureCount,
|
|
349
|
+
});
|
|
350
|
+
// Attempt automatic replacement
|
|
351
|
+
this.replaceStream(index).catch((replaceError) => {
|
|
352
|
+
this.logger.error('Failed to replace stream:', replaceError);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -4,6 +4,9 @@ import { oNodeAddress } from '../router/o-node.address.js';
|
|
|
4
4
|
import { oNodeConfig } from './o-node.config.js';
|
|
5
5
|
import { oNodeTransport } from '../router/o-node.transport.js';
|
|
6
6
|
import { oNodeHierarchyManager } from '../o-node.hierarchy-manager.js';
|
|
7
|
+
export interface IReconnectionManager {
|
|
8
|
+
waitForParentAndReconnect(): Promise<void>;
|
|
9
|
+
}
|
|
7
10
|
/**
|
|
8
11
|
* Interface for nodes that support registration management.
|
|
9
12
|
* This interface defines the contract that oRegistrationManager needs
|
|
@@ -50,6 +53,10 @@ export interface IRegistrableNode {
|
|
|
50
53
|
* Hierarchy manager for tracking parent/child/leader relationships
|
|
51
54
|
*/
|
|
52
55
|
hierarchyManager: oNodeHierarchyManager;
|
|
56
|
+
/**
|
|
57
|
+
* Reconnection manager for handling parent discovery
|
|
58
|
+
*/
|
|
59
|
+
reconnectionManager?: IReconnectionManager;
|
|
53
60
|
/**
|
|
54
61
|
* Execute a method on another node
|
|
55
62
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i-registrable-node.d.ts","sourceRoot":"","sources":["../../../src/interfaces/i-registrable-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"i-registrable-node.d.ts","sourceRoot":"","sources":["../../../src/interfaces/i-registrable-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAGvE,MAAM,WAAW,oBAAoB;IACnC,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;OAEG;IACH,OAAO,EAAE,YAAY,CAAC;IAEtB;;OAEG;IACH,aAAa,EAAE,YAAY,CAAC;IAE5B;;OAEG;IACH,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,IAAI,EAAE,QAAQ,CAAC;IAEf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,UAAU,EAAE,cAAc,EAAE,CAAC;IAE7B;;OAEG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB;;OAEG;IACH,gBAAgB,EAAE,qBAAqB,CAAC;IAExC;;OAEG;IACH,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;IAE3C;;OAEG;IACH,GAAG,CACD,OAAO,EAAE,QAAQ,EACjB,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;SAAE,CAAC;QAChC,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,GACA,OAAO,CAAC,GAAG,CAAC,CAAC;IAEhB;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-registration.manager.d.ts","sourceRoot":"","sources":["../../../src/managers/o-registration.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EAIR,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,aAAa,GACb,YAAY,GACZ,QAAQ,CAAC;AAEb,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAqB,SAAQ,OAAO;IAMnC,OAAO,CAAC,IAAI;IALxB,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAQ;gBAER,IAAI,EAAE,gBAAgB;IAI1C;;;OAGG;IACH,iBAAiB,IAAI,OAAO;IAM5B;;;OAGG;IACH,iBAAiB,IAAI,OAAO;IAM5B;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,iBAAiB,IAAI,OAAO;IAe5B;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,QAAQ,IAAI,IAAI;IAKhB;;OAEG;IACH,SAAS,IAAI,kBAAkB;IAS/B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"o-registration.manager.d.ts","sourceRoot":"","sources":["../../../src/managers/o-registration.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EAIR,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,aAAa,GACb,YAAY,GACZ,QAAQ,CAAC;AAEb,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAqB,SAAQ,OAAO;IAMnC,OAAO,CAAC,IAAI;IALxB,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAQ;gBAER,IAAI,EAAE,gBAAgB;IAI1C;;;OAGG;IACH,iBAAiB,IAAI,OAAO;IAM5B;;;OAGG;IACH,iBAAiB,IAAI,OAAO;IAM5B;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,iBAAiB,IAAI,OAAO;IAe5B;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,QAAQ,IAAI,IAAI;IAKhB;;OAEG;IACH,SAAS,IAAI,kBAAkB;IAS/B;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAsFrC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;CAwDtC"}
|
|
@@ -128,7 +128,17 @@ export class oRegistrationManager extends oObject {
|
|
|
128
128
|
this.node.parent.setTransports(this.node.leader?.libp2pTransports || []);
|
|
129
129
|
}
|
|
130
130
|
else {
|
|
131
|
-
|
|
131
|
+
this.logger.debug('Waiting for parent and reconnecting...');
|
|
132
|
+
// Reset state since we're delegating to reconnection manager
|
|
133
|
+
this.parentState = 'not_registered';
|
|
134
|
+
if (this.node.reconnectionManager) {
|
|
135
|
+
await this.node.reconnectionManager.waitForParentAndReconnect();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new Error('Parent has no transports and no reconnection manager available');
|
|
139
|
+
}
|
|
140
|
+
// If we get here, reconnection was successful and registration is complete
|
|
141
|
+
return;
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
144
|
// Register with parent via child_register call
|
|
@@ -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;;AAMrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;
|
|
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;;AAMrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;IAuBrC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAsBhC,mBAAmB;IAWnB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,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;IAyCV,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAiC5D"}
|
package/dist/src/o-node.tool.js
CHANGED
|
@@ -25,6 +25,7 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
25
25
|
await this.p2pNode.handle(reuseProtocol, this.handleStreamReuse.bind(this), {
|
|
26
26
|
maxInboundStreams: 10000,
|
|
27
27
|
maxOutboundStreams: maxOutboundsStreams,
|
|
28
|
+
runOnLimitedConnection: this.config.runOnLimitedConnection,
|
|
28
29
|
});
|
|
29
30
|
this.logger.debug('Handled protocol reuse: ' + reuseProtocol);
|
|
30
31
|
}
|
|
@@ -40,10 +41,12 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
40
41
|
this.logger.debug('Handling protocol: ' + address.protocol, {
|
|
41
42
|
maxInboundStreams: 10000,
|
|
42
43
|
maxOutboundStreams: maxOutboundsStreams,
|
|
44
|
+
runOnLimitedConnection: this.config.runOnLimitedConnection,
|
|
43
45
|
});
|
|
44
46
|
await this.p2pNode.handle(address.protocol, this.handleStream.bind(this), {
|
|
45
47
|
maxInboundStreams: 10000,
|
|
46
48
|
maxOutboundStreams: maxOutboundsStreams,
|
|
49
|
+
runOnLimitedConnection: this.config.runOnLimitedConnection,
|
|
47
50
|
});
|
|
48
51
|
await this.handleProtocolReuse(address);
|
|
49
52
|
}
|
|
@@ -66,20 +69,20 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
66
69
|
async handleStream(stream, connection, reuse) {
|
|
67
70
|
if (reuse) {
|
|
68
71
|
this.logger.debug('Handle stream with reuse = true');
|
|
72
|
+
// record inbound connection to manager
|
|
73
|
+
const remoteAddress = await ConnectionUtils.addressFromConnection({
|
|
74
|
+
currentNode: this,
|
|
75
|
+
connection: connection,
|
|
76
|
+
});
|
|
77
|
+
this.connectionManager.answer({
|
|
78
|
+
nextHopAddress: remoteAddress,
|
|
79
|
+
address: remoteAddress,
|
|
80
|
+
callerAddress: this.address,
|
|
81
|
+
p2pConnection: connection,
|
|
82
|
+
reuse,
|
|
83
|
+
// requestHandler: this.execute.bind(this), TODO: do we need this?
|
|
84
|
+
});
|
|
69
85
|
}
|
|
70
|
-
// record inbound connection to manager
|
|
71
|
-
const remoteAddress = await ConnectionUtils.addressFromConnection({
|
|
72
|
-
currentNode: this,
|
|
73
|
-
connection: connection,
|
|
74
|
-
});
|
|
75
|
-
this.connectionManager.answer({
|
|
76
|
-
nextHopAddress: remoteAddress,
|
|
77
|
-
address: remoteAddress,
|
|
78
|
-
callerAddress: this.address,
|
|
79
|
-
p2pConnection: connection,
|
|
80
|
-
reuse,
|
|
81
|
-
// requestHandler: this.execute.bind(this), TODO: do we need this?
|
|
82
|
-
});
|
|
83
86
|
// Use StreamHandler for consistent stream handling
|
|
84
87
|
// This follows libp2p v3 best practices for length-prefixed streaming
|
|
85
88
|
await this.streamHandler.handleIncomingStream(stream, connection, async (request, stream) => {
|
|
@@ -347,29 +347,29 @@ describe('Connection Management', () => {
|
|
|
347
347
|
// This test verifies stop mechanism doesn't throw errors
|
|
348
348
|
spy.stop();
|
|
349
349
|
});
|
|
350
|
-
it('should handle cleanup of multiple connections', async () => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
});
|
|
350
|
+
// it('should handle cleanup of multiple connections', async () => {
|
|
351
|
+
// builder = await NetworkTopologies.fiveNode();
|
|
352
|
+
// const leader = builder.getNode('o://leader')!;
|
|
353
|
+
// // Establish connections
|
|
354
|
+
// const child1 = builder.getNode('o://child1')!;
|
|
355
|
+
// const child2 = builder.getNode('o://child2')!;
|
|
356
|
+
// await leader.use(child1.address, {
|
|
357
|
+
// method: 'echo',
|
|
358
|
+
// params: { message: 'child1' },
|
|
359
|
+
// });
|
|
360
|
+
// await leader.use(child2.address, {
|
|
361
|
+
// method: 'echo',
|
|
362
|
+
// params: { message: 'child2' },
|
|
363
|
+
// });
|
|
364
|
+
// // Stop all children
|
|
365
|
+
// await builder.stopNode('o://child1');
|
|
366
|
+
// await builder.stopNode('o://child2');
|
|
367
|
+
// // Leader should remain operational
|
|
368
|
+
// const response = await leader.use(leader.address, {
|
|
369
|
+
// method: 'get_info',
|
|
370
|
+
// params: {},
|
|
371
|
+
// });
|
|
372
|
+
// expect(response.result.success).to.be.true;
|
|
373
|
+
// });
|
|
374
374
|
});
|
|
375
375
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { oNodeConnectionStream } from '../../src/connection/o-node-connection-stream.js';
|
|
2
|
+
import { StreamPoolManagerConfig } from '../../src/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a mock stream for testing
|
|
5
|
+
*/
|
|
6
|
+
export declare function createMockStream(id?: string, options?: {
|
|
7
|
+
status?: 'open' | 'closed' | 'reset';
|
|
8
|
+
writeStatus?: 'writable' | 'writing' | 'closed';
|
|
9
|
+
readStatus?: 'readable' | 'reading' | 'closed';
|
|
10
|
+
remoteReadStatus?: 'readable' | 'reading' | 'closed';
|
|
11
|
+
}): any;
|
|
12
|
+
/**
|
|
13
|
+
* Create a mock P2P connection for testing
|
|
14
|
+
*/
|
|
15
|
+
export declare function createMockP2PConnection(id?: string, status?: 'open' | 'closed'): any;
|
|
16
|
+
/**
|
|
17
|
+
* Create a mock StreamHandler for testing
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMockStreamHandler(): any;
|
|
20
|
+
/**
|
|
21
|
+
* Create a mock oNodeConnectionStream
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMockConnectionStream(p2pStream?: any, streamType?: 'dedicated-reader' | 'request-response' | 'general'): oNodeConnectionStream;
|
|
24
|
+
/**
|
|
25
|
+
* Factory for creating StreamPoolManager test config
|
|
26
|
+
*/
|
|
27
|
+
export declare function createStreamPoolManagerConfig(overrides?: Partial<StreamPoolManagerConfig>): StreamPoolManagerConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Event capture helper for testing event emissions
|
|
30
|
+
*/
|
|
31
|
+
export declare class EventCapture {
|
|
32
|
+
private emitter;
|
|
33
|
+
private events;
|
|
34
|
+
constructor(emitter: any);
|
|
35
|
+
/**
|
|
36
|
+
* Start capturing events
|
|
37
|
+
*/
|
|
38
|
+
start(eventNames: string[]): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get all captured events
|
|
41
|
+
*/
|
|
42
|
+
getEvents(): Array<{
|
|
43
|
+
type: string;
|
|
44
|
+
data: any;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Get events of specific type
|
|
49
|
+
*/
|
|
50
|
+
getEventsByType(type: string): any[];
|
|
51
|
+
/**
|
|
52
|
+
* Check if event was emitted
|
|
53
|
+
*/
|
|
54
|
+
hasEvent(type: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Get count of specific event type
|
|
57
|
+
*/
|
|
58
|
+
getEventCount(type: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* Wait for specific event
|
|
61
|
+
*/
|
|
62
|
+
waitForEvent(type: string, timeoutMs?: number): Promise<any>;
|
|
63
|
+
/**
|
|
64
|
+
* Clear captured events
|
|
65
|
+
*/
|
|
66
|
+
clear(): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Helper to make a stream invalid
|
|
70
|
+
*/
|
|
71
|
+
export declare function makeStreamInvalid(stream: any): void;
|
|
72
|
+
/**
|
|
73
|
+
* Helper to wait for condition with timeout
|
|
74
|
+
*/
|
|
75
|
+
export declare function waitFor(condition: () => boolean, timeoutMs?: number, intervalMs?: number): Promise<void>;
|
|
76
|
+
//# sourceMappingURL=stream-pool-test-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-pool-test-helpers.d.ts","sourceRoot":"","sources":["../../../test/helpers/stream-pool-test-helpers.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,qBAAqB,EAAE,MAAM,kDAAkD,CAAC;AACzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAE7D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,GAAE,MAAsB,EAC1B,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrC,WAAW,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAChD,UAAU,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/C,gBAAgB,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CACjD,GACL,GAAG,CAqBL;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,GAAE,MAA0B,EAC9B,MAAM,GAAE,MAAM,GAAG,QAAiB,GACjC,GAAG,CAoBL;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,GAAG,CAwE7C;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,CAAC,EAAE,GAAG,EACf,UAAU,GAAE,kBAAkB,GAAG,kBAAkB,GAAG,SAAqB,GAC1E,qBAAqB,CASvB;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,uBAAuB,CAuBzB;AAED;;GAEG;AACH,qBAAa,YAAY;IAGX,OAAO,CAAC,OAAO;IAF3B,OAAO,CAAC,MAAM,CAA6D;gBAEvD,OAAO,EAAE,GAAG;IAEhC;;OAEG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI;IAYjC;;OAEG;IACH,SAAS,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAIlE;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,EAAE;IAIpC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI/B;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAInC;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,GAAG,CAAC;IAcxE;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAGnD;AAED;;GAEG;AACH,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,OAAO,EACxB,SAAS,GAAE,MAAa,EACxB,UAAU,GAAE,MAAW,GACtB,OAAO,CAAC,IAAI,CAAC,CASf"}
|