@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.
Files changed (48) hide show
  1. package/dist/src/connection/index.d.ts +6 -5
  2. package/dist/src/connection/index.d.ts.map +1 -1
  3. package/dist/src/connection/index.js +6 -5
  4. package/dist/src/connection/interfaces/{o-node-connection-stream.config.d.ts → o-node-stream.config.d.ts} +2 -2
  5. package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -0
  6. package/dist/src/connection/interfaces/stream-init-message.d.ts +29 -0
  7. package/dist/src/connection/interfaces/stream-init-message.d.ts.map +1 -0
  8. package/dist/src/connection/interfaces/stream-init-message.js +8 -0
  9. package/dist/src/connection/interfaces/stream-manager.config.d.ts +8 -0
  10. package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +1 -0
  11. package/dist/src/connection/o-node-connection.d.ts +5 -7
  12. package/dist/src/connection/o-node-connection.d.ts.map +1 -1
  13. package/dist/src/connection/o-node-connection.js +26 -56
  14. package/dist/src/connection/o-node-connection.manager.d.ts +7 -0
  15. package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
  16. package/dist/src/connection/o-node-connection.manager.js +23 -5
  17. package/dist/src/connection/{o-node-connection-stream.d.ts → o-node-stream.d.ts} +6 -6
  18. package/dist/src/connection/o-node-stream.d.ts.map +1 -0
  19. package/dist/src/connection/{o-node-connection-stream.js → o-node-stream.js} +2 -2
  20. package/dist/src/connection/o-node-stream.manager.d.ts +181 -0
  21. package/dist/src/connection/o-node-stream.manager.d.ts.map +1 -0
  22. package/dist/src/connection/o-node-stream.manager.js +526 -0
  23. package/dist/src/connection/stream-manager.events.d.ts +83 -0
  24. package/dist/src/connection/stream-manager.events.d.ts.map +1 -0
  25. package/dist/src/connection/stream-manager.events.js +18 -0
  26. package/dist/src/o-node.tool.d.ts +0 -1
  27. package/dist/src/o-node.tool.d.ts.map +1 -1
  28. package/dist/src/o-node.tool.js +30 -20
  29. package/dist/test/helpers/stream-pool-test-helpers.d.ts +0 -75
  30. package/dist/test/helpers/stream-pool-test-helpers.d.ts.map +1 -1
  31. package/dist/test/helpers/stream-pool-test-helpers.js +262 -229
  32. package/dist/test/parent-child-registration.spec.js +2 -1
  33. package/package.json +7 -7
  34. package/dist/src/connection/interfaces/o-node-connection-stream.config.d.ts.map +0 -1
  35. package/dist/src/connection/interfaces/stream-pool-manager.config.d.ts +0 -41
  36. package/dist/src/connection/interfaces/stream-pool-manager.config.d.ts.map +0 -1
  37. package/dist/src/connection/o-node-connection-stream.d.ts.map +0 -1
  38. package/dist/src/connection/stream-handler.d.ts +0 -102
  39. package/dist/src/connection/stream-handler.d.ts.map +0 -1
  40. package/dist/src/connection/stream-handler.js +0 -357
  41. package/dist/src/connection/stream-pool-manager.d.ts +0 -86
  42. package/dist/src/connection/stream-pool-manager.d.ts.map +0 -1
  43. package/dist/src/connection/stream-pool-manager.events.d.ts +0 -57
  44. package/dist/src/connection/stream-pool-manager.events.d.ts.map +0 -1
  45. package/dist/src/connection/stream-pool-manager.events.js +0 -14
  46. package/dist/src/connection/stream-pool-manager.js +0 -356
  47. /package/dist/src/connection/interfaces/{o-node-connection-stream.config.js → o-node-stream.config.js} +0 -0
  48. /package/dist/src/connection/interfaces/{stream-pool-manager.config.js → stream-manager.config.js} +0 -0
@@ -1,356 +0,0 @@
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
- }