@olane/o-node 0.7.54 → 0.7.56

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 (60) hide show
  1. package/dist/src/connection/enums/o-node-message-event.d.ts +14 -0
  2. package/dist/src/connection/enums/o-node-message-event.d.ts.map +1 -0
  3. package/dist/src/connection/enums/o-node-message-event.js +5 -0
  4. package/dist/src/connection/index.d.ts +0 -3
  5. package/dist/src/connection/index.d.ts.map +1 -1
  6. package/dist/src/connection/index.js +0 -3
  7. package/dist/src/connection/interfaces/abort-signal.config.d.ts +5 -0
  8. package/dist/src/connection/interfaces/abort-signal.config.d.ts.map +1 -0
  9. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts +13 -1
  10. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
  11. package/dist/src/connection/interfaces/o-node-connection.config.d.ts +17 -3
  12. package/dist/src/connection/interfaces/o-node-connection.config.d.ts.map +1 -1
  13. package/dist/src/connection/interfaces/o-node-stream.config.d.ts +3 -11
  14. package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -1
  15. package/dist/src/connection/o-node-connection.d.ts +33 -11
  16. package/dist/src/connection/o-node-connection.d.ts.map +1 -1
  17. package/dist/src/connection/o-node-connection.js +113 -58
  18. package/dist/src/connection/o-node-connection.manager.d.ts +17 -62
  19. package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
  20. package/dist/src/connection/o-node-connection.manager.js +65 -189
  21. package/dist/src/connection/o-node-stream.d.ts +48 -15
  22. package/dist/src/connection/o-node-stream.d.ts.map +1 -1
  23. package/dist/src/connection/o-node-stream.js +144 -31
  24. package/dist/src/connection/stream-handler.config.d.ts +0 -11
  25. package/dist/src/connection/stream-handler.config.d.ts.map +1 -1
  26. package/dist/src/connection/stream-manager.events.d.ts +7 -0
  27. package/dist/src/connection/stream-manager.events.d.ts.map +1 -1
  28. package/dist/src/connection/stream-manager.events.js +1 -0
  29. package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts +9 -0
  30. package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts.map +1 -0
  31. package/dist/src/lib/interfaces/o-node-request-manager.config.js +1 -0
  32. package/dist/src/lib/o-node-request-manager.d.ts +46 -0
  33. package/dist/src/lib/o-node-request-manager.d.ts.map +1 -0
  34. package/dist/src/lib/o-node-request-manager.js +173 -0
  35. package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
  36. package/dist/src/managers/o-reconnection.manager.js +4 -0
  37. package/dist/src/managers/o-registration.manager.d.ts.map +1 -1
  38. package/dist/src/managers/o-registration.manager.js +9 -4
  39. package/dist/src/o-node.d.ts +6 -7
  40. package/dist/src/o-node.d.ts.map +1 -1
  41. package/dist/src/o-node.js +22 -37
  42. package/dist/src/o-node.tool.d.ts +3 -3
  43. package/dist/src/o-node.tool.d.ts.map +1 -1
  44. package/dist/src/o-node.tool.js +31 -65
  45. package/dist/src/router/o-node.router.d.ts.map +1 -1
  46. package/dist/src/router/o-node.router.js +4 -2
  47. package/dist/src/utils/connection.utils.d.ts +3 -3
  48. package/dist/src/utils/connection.utils.d.ts.map +1 -1
  49. package/dist/src/utils/connection.utils.js +46 -19
  50. package/dist/test/connection-management.spec.js +3 -0
  51. package/package.json +7 -7
  52. package/dist/src/connection/interfaces/stream-init-message.d.ts +0 -64
  53. package/dist/src/connection/interfaces/stream-init-message.d.ts.map +0 -1
  54. package/dist/src/connection/interfaces/stream-init-message.js +0 -18
  55. package/dist/src/connection/interfaces/stream-manager.config.d.ts +0 -8
  56. package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +0 -1
  57. package/dist/src/connection/o-node-stream.manager.d.ts +0 -200
  58. package/dist/src/connection/o-node-stream.manager.d.ts.map +0 -1
  59. package/dist/src/connection/o-node-stream.manager.js +0 -633
  60. /package/dist/src/connection/interfaces/{stream-manager.config.js → abort-signal.config.js} +0 -0
@@ -1,5 +1,10 @@
1
1
  import { oConnectionManager } from '@olane/o-core';
2
2
  import { oNodeConnection } from './o-node-connection.js';
3
+ import { EventEmitter } from 'events';
4
+ import { oNodeStream } from './o-node-stream.js';
5
+ /**
6
+ * Manages oNodeConnection instances, reusing connections when possible.
7
+ */
3
8
  export class oNodeConnectionManager extends oConnectionManager {
4
9
  constructor(config) {
5
10
  super(config);
@@ -7,10 +12,11 @@ export class oNodeConnectionManager extends oConnectionManager {
7
12
  /** Single cache of oNodeConnection instances keyed by address */
8
13
  this.cachedConnections = new Map();
9
14
  this.pendingDialsByAddress = new Map();
15
+ this.eventEmitter = new EventEmitter();
10
16
  this.p2pNode = config.p2pNode;
11
17
  this.defaultReadTimeoutMs = config.defaultReadTimeoutMs;
12
18
  this.defaultDrainTimeoutMs = config.defaultDrainTimeoutMs;
13
- this.logger.setNamespace(`oNodeConnectionManager[${config.originAddress}]`);
19
+ this.logger.setNamespace(`oNodeConnectionManager[${config.callerAddress}]`);
14
20
  // Set up connection lifecycle listeners for cache management
15
21
  this.setupConnectionListeners();
16
22
  }
@@ -24,35 +30,9 @@ export class oNodeConnectionManager extends oConnectionManager {
24
30
  return;
25
31
  }
26
32
  const connectionId = connection.id;
27
- // Clean up cached connections by filtering out the closed connection
28
- for (const [key, conns] of this.cachedConnections.entries()) {
29
- const filtered = conns.filter((c) => c.p2pConnection.id !== connectionId);
30
- if (filtered.length === 0) {
31
- this.logger.debug('Connection closed, removing all cached connections for address:', key);
32
- this.cachedConnections.delete(key);
33
- }
34
- else if (filtered.length !== conns.length) {
35
- this.logger.debug('Connection closed, updating cached connections for address:', key);
36
- this.cachedConnections.set(key, filtered);
37
- }
38
- }
33
+ this.cachedConnections.delete(connectionId);
39
34
  });
40
35
  }
41
- /**
42
- * Build a stable cache key from an address.
43
- *
44
- * We key the cache by address value (e.g., "o://my-tool") to maintain
45
- * a simple one-to-one mapping between addresses and connections.
46
- */
47
- getAddressKey(address) {
48
- try {
49
- return address.value || null;
50
- }
51
- catch (error) {
52
- this.logger.debug('Error extracting address key from address:', error);
53
- return null;
54
- }
55
- }
56
36
  /**
57
37
  * Extract peer ID string from an address
58
38
  * @param address - The address to extract peer ID from
@@ -71,71 +51,22 @@ export class oNodeConnectionManager extends oConnectionManager {
71
51
  return null;
72
52
  }
73
53
  }
74
- /**
75
- * Get the first valid (open) connection for the given address key.
76
- * Cleans up stale connections from the cache automatically.
77
- *
78
- * @param addressKey - The address key to look up
79
- * @returns A valid oNodeConnection or null if none found
80
- */
81
- getValidConnection(addressKey) {
82
- const connections = this.cachedConnections.get(addressKey) || [];
83
- // Filter to open connections
84
- const valid = connections.filter((c) => c.p2pConnection?.status === 'open');
85
- // Update cache if we cleaned any stale connections
86
- if (valid.length !== connections.length) {
87
- if (valid.length === 0) {
88
- this.cachedConnections.delete(addressKey);
89
- }
90
- else {
91
- this.cachedConnections.set(addressKey, valid);
92
- }
93
- }
94
- return valid[0] ?? null;
95
- }
96
54
  /**
97
55
  * Cache an oNodeConnection by its address key.
98
56
  * @param conn - The oNodeConnection to cache
99
57
  * @param addressKey - The address key to cache under
100
58
  */
101
- cacheConnection(conn, addressKey) {
102
- this.logger.debug('Caching connection for address:', addressKey, conn.p2pConnection.id, conn.p2pConnection.direction);
103
- const existing = this.cachedConnections.get(addressKey) || [];
104
- existing.push(conn);
105
- this.cachedConnections.set(addressKey, existing);
106
- }
107
- /**
108
- * Get oNodeConnection by libp2p Connection reference
109
- * Used to find the correct oNodeConnection for incoming streams
110
- * @param p2pConnection - The libp2p connection to search for
111
- * @returns The oNodeConnection or undefined if not found
112
- */
113
- getConnectionByP2pConnection(p2pConnection) {
114
- // Search through all cached connections
115
- for (const connections of this.cachedConnections.values()) {
116
- const found = connections.find((conn) => conn.p2pConnection.id === p2pConnection.id);
117
- if (found) {
118
- return found;
119
- }
120
- }
121
- return undefined;
59
+ cacheConnection(conn) {
60
+ this.logger.debug('Caching connection for address:', conn.p2pConnection.id, conn.p2pConnection.direction, conn.nextHopAddress.value, conn.p2pConnection.streams.map((s) => s.protocol).join(', '));
61
+ this.cachedConnections.set(conn.p2pConnection.id, conn);
122
62
  }
123
63
  /**
124
64
  * Get or create a raw p2p connection to the given address.
125
65
  * Subclasses can override connect() and use this method to get the underlying p2p connection.
126
66
  */
127
67
  async getOrCreateP2pConnection(nextHopAddress, addressKey) {
128
- // Check if libp2p already has an active connection for this peer
129
- const peerId = this.getPeerIdFromAddress(nextHopAddress);
130
- if (peerId) {
131
- const connections = this.p2pNode.getConnections();
132
- const existingConnection = connections.find((conn) => conn.remotePeer?.toString() === peerId && conn.status === 'open');
133
- if (existingConnection) {
134
- this.logger.debug('Found existing libp2p connection for address:', addressKey);
135
- return existingConnection;
136
- }
137
- }
138
68
  // Check if dial is already in progress for this address key
69
+ this.logger.debug('Checking for pending dial for address:', addressKey);
139
70
  const pendingDial = this.pendingDialsByAddress.get(addressKey);
140
71
  if (pendingDial) {
141
72
  this.logger.debug('Awaiting existing dial for address:', addressKey);
@@ -164,37 +95,46 @@ export class oNodeConnectionManager extends oConnectionManager {
164
95
  });
165
96
  return connection;
166
97
  }
167
- async answer(config) {
168
- const { address, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, p2pConnection, reuse, } = config;
169
- const addressKey = this.getAddressKey(nextHopAddress);
170
- if (!addressKey) {
171
- this.logger.error('Failed to generate an address key for address:', nextHopAddress);
172
- throw new Error(`Unable to extract address key from address: ${nextHopAddress.toString()}`);
173
- }
98
+ async createConnection(config) {
99
+ return new oNodeConnection(config);
100
+ }
101
+ async answer(config, stream) {
102
+ const { targetAddress, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, p2pConnection, } = config;
103
+ if (!p2pConnection) {
104
+ throw new Error('Failed to answer connection, p2pConnection is undefined');
105
+ }
106
+ this.logger.debug('Answering connection for address:', {
107
+ address: nextHopAddress?.value,
108
+ connectionId: p2pConnection.id,
109
+ direction: p2pConnection.direction,
110
+ });
174
111
  // Check if we already have a cached connection for this address with the same connection id
175
- const connections = this.cachedConnections.get(addressKey) || [];
176
- // Filter to open connections
177
- const validConnections = connections.filter((c) => c.p2pConnection?.id === p2pConnection.id &&
178
- c.p2pConnection?.status === 'open');
179
- if (validConnections.length > 0) {
180
- const existingConnection = validConnections[0];
181
- this.logger.debug('Reusing cached connection for answer:', addressKey, existingConnection.p2pConnection.id);
112
+ const existingConnection = this.cachedConnections.get(p2pConnection.id);
113
+ if (existingConnection) {
114
+ this.logger.debug('Reusing cached connection for answer:', existingConnection.id);
115
+ existingConnection.trackStream(new oNodeStream(stream, {
116
+ remoteAddress: nextHopAddress,
117
+ limited: this.config.runOnLimitedConnection,
118
+ }), config);
182
119
  return existingConnection;
183
120
  }
184
- const connection = new oNodeConnection({
121
+ const connection = await this.createConnection({
185
122
  nextHopAddress: nextHopAddress,
186
- address: address,
187
- p2pConnection: p2pConnection,
123
+ targetAddress: targetAddress,
188
124
  callerAddress: callerAddress,
189
125
  readTimeoutMs: readTimeoutMs ?? this.defaultReadTimeoutMs,
190
126
  drainTimeoutMs: drainTimeoutMs ?? this.defaultDrainTimeoutMs,
191
127
  isStream: config.isStream ?? false,
192
128
  abortSignal: config.abortSignal,
193
129
  runOnLimitedConnection: this.config.runOnLimitedConnection ?? false,
194
- reusePolicy: reuse ? 'reuse' : 'none',
130
+ p2pConnection: p2pConnection,
195
131
  });
132
+ connection.trackStream(new oNodeStream(stream, {
133
+ remoteAddress: nextHopAddress,
134
+ limited: this.config.runOnLimitedConnection,
135
+ }), config);
196
136
  // Cache the new connection
197
- this.cacheConnection(connection, addressKey);
137
+ this.cacheConnection(connection);
198
138
  return connection;
199
139
  }
200
140
  /**
@@ -203,26 +143,29 @@ export class oNodeConnectionManager extends oConnectionManager {
203
143
  * @returns The connection object
204
144
  */
205
145
  async connect(config) {
206
- const { address, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, } = config;
146
+ const { targetAddress, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, } = config;
207
147
  if (!nextHopAddress) {
208
148
  throw new Error('Invalid address passed');
209
149
  }
210
- const addressKey = this.getAddressKey(nextHopAddress);
211
- if (!addressKey) {
212
- throw new Error(`Unable to extract address key from address: ${nextHopAddress.toString()}`);
150
+ if (nextHopAddress.libp2pTransports?.length === 0) {
151
+ throw new Error('No transports provided for the address, cannot connect');
213
152
  }
214
153
  // Check for existing valid cached connection
215
- const existingConnection = this.getValidConnection(addressKey);
154
+ const existingConnection = this.getCachedConnectionFromAddress(nextHopAddress);
216
155
  if (existingConnection) {
217
- this.logger.debug('Reusing cached connection for address:', addressKey, existingConnection.p2pConnection.id);
156
+ this.logger.debug('Reusing cached connection for address:', existingConnection.p2pConnection.id);
218
157
  return existingConnection;
219
158
  }
159
+ else {
160
+ this.logger.debug('No cached connection found for address:', {
161
+ address: nextHopAddress.value,
162
+ });
163
+ }
220
164
  // Get or create the underlying p2p connection
221
- const p2pConnection = await this.getOrCreateP2pConnection(nextHopAddress, addressKey);
222
- // Create new oNodeConnection
223
- const connection = new oNodeConnection({
165
+ const p2pConnection = await this.getOrCreateP2pConnection(nextHopAddress, nextHopAddress.value);
166
+ const connection = await this.createConnection({
224
167
  nextHopAddress: nextHopAddress,
225
- address: address,
168
+ targetAddress: targetAddress,
226
169
  p2pConnection: p2pConnection,
227
170
  callerAddress: callerAddress,
228
171
  readTimeoutMs: readTimeoutMs ?? this.defaultReadTimeoutMs,
@@ -232,89 +175,22 @@ export class oNodeConnectionManager extends oConnectionManager {
232
175
  runOnLimitedConnection: this.config.runOnLimitedConnection ?? false,
233
176
  });
234
177
  // Cache the new connection
235
- this.cacheConnection(connection, addressKey);
178
+ this.cacheConnection(connection);
236
179
  return connection;
237
180
  }
238
- /**
239
- * Check if we have an active connection to the target peer
240
- * @param address - The address to check
241
- * @returns true if an active connection exists
242
- */
243
- isCached(address) {
244
- try {
245
- const addressKey = this.getAddressKey(address);
246
- if (!addressKey) {
247
- return false;
181
+ getCachedConnectionFromAddress(address) {
182
+ const vals = Array.from(this.cachedConnections.values());
183
+ for (const c in vals) {
184
+ const connection = c;
185
+ const peerId = address.libp2pTransports?.[0].toPeerId();
186
+ if (connection.p2pConnection?.remotePeer.toString() === peerId &&
187
+ connection.isOpen) {
188
+ return connection;
248
189
  }
249
- return this.getValidConnection(addressKey) !== null;
250
- }
251
- catch (error) {
252
- this.logger.debug('Error checking cached connection:', error);
253
- return false;
254
190
  }
191
+ return null;
255
192
  }
256
- /**
257
- * Get an existing cached oNodeConnection for the target address
258
- * @param address - The address to get a connection for
259
- * @returns The oNodeConnection or null if not found
260
- */
261
- getCachedConnection(address) {
262
- try {
263
- const addressKey = this.getAddressKey(address);
264
- if (!addressKey) {
265
- return null;
266
- }
267
- return this.getValidConnection(addressKey);
268
- }
269
- catch (error) {
270
- this.logger.debug('Error getting cached connection:', error);
271
- return null;
272
- }
273
- }
274
- /**
275
- * Get cache statistics for monitoring and debugging
276
- * @returns Object containing cache statistics
277
- */
278
- getCacheStats() {
279
- const allConnections = [];
280
- for (const [addressKey, connections] of this.cachedConnections.entries()) {
281
- for (const conn of connections) {
282
- allConnections.push({
283
- peerId: conn.p2pConnection?.remotePeer?.toString() ?? 'unknown',
284
- status: conn.p2pConnection?.status ?? 'unknown',
285
- addressKey,
286
- });
287
- }
288
- }
289
- return {
290
- cachedAddresses: this.cachedConnections.size,
291
- totalCachedConnections: allConnections.length,
292
- pendingDials: this.pendingDialsByAddress.size,
293
- connectionsByPeer: allConnections,
294
- };
295
- }
296
- /**
297
- * Clean up all stale (non-open) connections from cache
298
- * @returns Number of connections removed
299
- */
300
- cleanupStaleConnections() {
301
- let removed = 0;
302
- for (const [addressKey, connections] of this.cachedConnections.entries()) {
303
- const openConnections = connections.filter((conn) => conn.p2pConnection?.status === 'open');
304
- const staleCount = connections.length - openConnections.length;
305
- if (staleCount > 0) {
306
- removed += staleCount;
307
- if (openConnections.length === 0) {
308
- this.cachedConnections.delete(addressKey);
309
- }
310
- else {
311
- this.cachedConnections.set(addressKey, openConnections);
312
- }
313
- }
314
- }
315
- if (removed > 0) {
316
- this.logger.debug(`Cleaned up ${removed} stale connections`);
317
- }
318
- return removed;
193
+ get connectionCount() {
194
+ return this.cachedConnections.size;
319
195
  }
320
196
  }
@@ -1,21 +1,53 @@
1
+ /// <reference types="node" />
1
2
  import type { Stream } from '@libp2p/interface';
2
- import { oObject } from '@olane/o-core';
3
+ import { oObject, oRequest, oResponse } from '@olane/o-core';
3
4
  import { oNodeStreamConfig } from './interfaces/o-node-stream.config.js';
5
+ import { LengthPrefixedStream } from '@olane/o-config';
6
+ import { AbortSignalConfig } from './interfaces/abort-signal.config.js';
7
+ import { EventEmitter } from 'events';
8
+ import { oNodeMessageEvent, oNodeMessageEventData } from './enums/o-node-message-event.js';
4
9
  /**
5
- * oNodeStream wraps a libp2p Stream with caller/receiver address metadata
6
- * to enable proper stream reuse based on address pairs rather than protocol only.
7
- *
8
- * Key features:
9
- * - Bidirectional cache keys: A↔B === B↔A
10
- * - Automatic reusability checking
11
- * - Idle time tracking for cleanup
10
+ * oNodeStream wraps a libp2p Stream and transmits or receives messages across the libp2p streams
12
11
  */
13
12
  export declare class oNodeStream extends oObject {
14
13
  readonly p2pStream: Stream;
15
14
  readonly config: oNodeStreamConfig;
16
15
  readonly createdAt: number;
16
+ protected readonly lp: LengthPrefixedStream;
17
+ protected readonly eventEmitter: EventEmitter;
17
18
  constructor(p2pStream: Stream, config: oNodeStreamConfig);
19
+ listenForLibp2pEvents(): void;
18
20
  validate(): void;
21
+ /**
22
+ * Extracts and parses JSON from various formats including:
23
+ * - Already parsed objects
24
+ * - Plain JSON
25
+ * - Markdown code blocks (```json ... ``` or ``` ... ```)
26
+ * - Mixed content with explanatory text
27
+ * - JSON5 format (trailing commas, comments, unquoted keys, etc.)
28
+ *
29
+ * @param decoded - The decoded string that may contain JSON, or an already parsed object
30
+ * @returns Parsed JSON object
31
+ * @throws Error if JSON parsing fails even with JSON5 fallback
32
+ */
33
+ protected extractAndParseJSON(decoded: string | any): any;
34
+ send(request: oRequest, options: AbortSignalConfig): Promise<void>;
35
+ /**
36
+ * listen - process every message inbound on the stream and emit it for the connection to bubble up
37
+ * @param options
38
+ */
39
+ listen(options: AbortSignalConfig): Promise<void>;
40
+ listenOnce(options: AbortSignalConfig): Promise<void>;
41
+ waitForResponse(requestId: string): Promise<oResponse>;
42
+ /**
43
+ * Detects if a decoded message is a request
44
+ * Requests have a 'method' field and no 'result' field
45
+ */
46
+ isRequest(message: any): boolean;
47
+ /**
48
+ * Detects if a decoded message is a response
49
+ */
50
+ isResponse(message: any): boolean;
19
51
  /**
20
52
  * Checks if the stream is in a valid state:
21
53
  * - Stream status is 'open'
@@ -29,18 +61,19 @@ export declare class oNodeStream extends oObject {
29
61
  * Gets the age of the stream in milliseconds
30
62
  */
31
63
  get age(): number;
64
+ close(): Promise<void>;
65
+ get id(): string;
32
66
  /**
33
- * Returns the stream type (defaults to 'general' for backward compatibility)
67
+ * Add event listener
34
68
  */
35
- get streamType(): string;
69
+ on<K extends oNodeMessageEvent>(event: K, listener: (data: oNodeMessageEventData[K]) => void): void;
36
70
  /**
37
- * Checks if this stream is designated as a dedicated reader
71
+ * Remove event listener
38
72
  */
39
- get isDedicatedReader(): boolean;
73
+ off<K extends oNodeMessageEvent>(event: K, listener: (data: oNodeMessageEventData[K]) => void): void;
40
74
  /**
41
- * Checks if this stream is designated for request-response cycles
75
+ * Emit event
42
76
  */
43
- get isRequestResponse(): boolean;
44
- close(): Promise<void>;
77
+ private emit;
45
78
  }
46
79
  //# sourceMappingURL=o-node-stream.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-stream.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAuB,OAAO,EAAiB,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAEzE;;;;;;;;GAQG;AACH,qBAAa,WAAY,SAAQ,OAAO;aAIpB,SAAS,EAAE,MAAM;aACjB,MAAM,EAAE,iBAAiB;IAJ3C,SAAgB,SAAS,EAAE,MAAM,CAAC;gBAGhB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB;IAO3C,QAAQ;IA6BR;;;;;;;OAOG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;IACH,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAED;;OAEG;IACH,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB7B"}
1
+ {"version":3,"file":"o-node-stream.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-stream.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAGL,OAAO,EACP,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAY,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AAGzC;;GAEG;AACH,qBAAa,WAAY,SAAQ,OAAO;aAMpB,SAAS,EAAE,MAAM;aACjB,MAAM,EAAE,iBAAiB;IAN3C,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAsB;gBAGjD,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB;IAQ3C,qBAAqB;IAOrB,QAAQ;IA6BR;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG;IAqCnD,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,iBAAiB;IAUxD;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjD,UAAU,CAAC,OAAO,EAAE,iBAAiB;IAqBrC,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAe5D;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO;IAQhC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO;IAIjC;;;;;;;OAOG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,CAEhB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,IAAI,EAAE,IAAI,MAAM,CAEf;IAED;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,iBAAiB,EAC5B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,GACjD,IAAI;IAIP;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,iBAAiB,EAC7B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,GACjD,IAAI;IAIP;;OAEG;IACH,OAAO,CAAC,IAAI;CAMb"}
@@ -1,19 +1,26 @@
1
- import { oError, oErrorCodes, oObject } from '@olane/o-core';
1
+ import { oError, oErrorCodes, oObject, oResponse, } from '@olane/o-core';
2
+ import { lpStream } from '@olane/o-config';
3
+ import JSON5 from 'json5';
4
+ import { EventEmitter } from 'events';
5
+ import { oNodeMessageEvent, } from './enums/o-node-message-event.js';
6
+ import { oStreamRequest } from './o-stream.request.js';
2
7
  /**
3
- * oNodeStream wraps a libp2p Stream with caller/receiver address metadata
4
- * to enable proper stream reuse based on address pairs rather than protocol only.
5
- *
6
- * Key features:
7
- * - Bidirectional cache keys: A↔B === B↔A
8
- * - Automatic reusability checking
9
- * - Idle time tracking for cleanup
8
+ * oNodeStream wraps a libp2p Stream and transmits or receives messages across the libp2p streams
10
9
  */
11
10
  export class oNodeStream extends oObject {
12
11
  constructor(p2pStream, config) {
13
12
  super();
14
13
  this.p2pStream = p2pStream;
15
14
  this.config = config;
15
+ this.eventEmitter = new EventEmitter();
16
16
  this.createdAt = Date.now();
17
+ this.lp = lpStream(p2pStream);
18
+ this.listenForLibp2pEvents();
19
+ }
20
+ listenForLibp2pEvents() {
21
+ this.p2pStream.addEventListener('close', () => {
22
+ this.close();
23
+ });
17
24
  }
18
25
  // callable pattern to disrupt flow if not in valid state
19
26
  validate() {
@@ -30,6 +37,113 @@ export class oNodeStream extends oObject {
30
37
  throw new oError(oErrorCodes.INVALID_STATE, 'Session is not in a valid state');
31
38
  }
32
39
  }
40
+ /**
41
+ * Extracts and parses JSON from various formats including:
42
+ * - Already parsed objects
43
+ * - Plain JSON
44
+ * - Markdown code blocks (```json ... ``` or ``` ... ```)
45
+ * - Mixed content with explanatory text
46
+ * - JSON5 format (trailing commas, comments, unquoted keys, etc.)
47
+ *
48
+ * @param decoded - The decoded string that may contain JSON, or an already parsed object
49
+ * @returns Parsed JSON object
50
+ * @throws Error if JSON parsing fails even with JSON5 fallback
51
+ */
52
+ extractAndParseJSON(decoded) {
53
+ // If already an object (not a string), return it directly
54
+ if (typeof decoded !== 'string') {
55
+ return decoded;
56
+ }
57
+ let jsonString = decoded.trim();
58
+ // Attempt standard JSON.parse first
59
+ try {
60
+ return JSON.parse(jsonString);
61
+ }
62
+ catch (jsonError) {
63
+ this.logger.debug('Standard JSON parse failed, trying JSON5', {
64
+ error: jsonError.message,
65
+ position: jsonError.message.match(/position (\d+)/)?.[1],
66
+ preview: jsonString,
67
+ });
68
+ // Fallback to JSON5 for more relaxed parsing
69
+ try {
70
+ return JSON5.parse(jsonString);
71
+ }
72
+ catch (json5Error) {
73
+ // Enhanced error with context
74
+ this.logger.error('JSON5 parse also failed', {
75
+ originalError: jsonError.message,
76
+ json5Error: json5Error.message,
77
+ preview: jsonString.substring(0, 200),
78
+ length: jsonString.length,
79
+ });
80
+ throw new Error(`Failed to parse JSON: ${jsonError.message}\nJSON5 also failed: ${json5Error.message}\nPreview: ${jsonString.substring(0, 200)}${jsonString.length > 200 ? '...' : ''}`);
81
+ }
82
+ }
83
+ }
84
+ async send(request, options) {
85
+ // Ensure stream is valid
86
+ this.validate();
87
+ // Send the request with backpressure handling
88
+ const data = new TextEncoder().encode(request.toString());
89
+ await this.lp.write(data, { signal: options?.abortSignal });
90
+ }
91
+ /**
92
+ * listen - process every message inbound on the stream and emit it for the connection to bubble up
93
+ * @param options
94
+ */
95
+ async listen(options) {
96
+ while (this.isValid && !options?.abortSignal?.aborted) {
97
+ await this.listenOnce(options);
98
+ }
99
+ }
100
+ async listenOnce(options) {
101
+ const messageBytes = await this.lp.read({ signal: options?.abortSignal });
102
+ const decoded = new TextDecoder().decode(messageBytes.subarray());
103
+ const message = this.extractAndParseJSON(decoded);
104
+ if (this.isRequest(message)) {
105
+ // package up the request + stream and emit
106
+ const request = new oStreamRequest({
107
+ ...message,
108
+ stream: this.p2pStream,
109
+ });
110
+ this.emit(oNodeMessageEvent.request, request);
111
+ }
112
+ else if (this.isResponse(message)) {
113
+ const response = new oResponse({
114
+ ...message.result,
115
+ id: message.id,
116
+ });
117
+ this.emit(oNodeMessageEvent.response, response);
118
+ }
119
+ }
120
+ async waitForResponse(requestId) {
121
+ return new Promise((resolve, reject) => {
122
+ const handler = (data) => {
123
+ if (data.id === requestId) {
124
+ this.off(oNodeMessageEvent.response, handler);
125
+ this.logger.debug('Stream stopped listening for responses due to "waitForResponse", technically should continue if listen was called elsewhere');
126
+ resolve(data);
127
+ }
128
+ };
129
+ this.on(oNodeMessageEvent.response, handler);
130
+ });
131
+ }
132
+ /**
133
+ * Detects if a decoded message is a request
134
+ * Requests have a 'method' field and no 'result' field
135
+ */
136
+ isRequest(message) {
137
+ return (typeof message?.method === 'string' &&
138
+ message.result === undefined &&
139
+ message.error === undefined);
140
+ }
141
+ /**
142
+ * Detects if a decoded message is a response
143
+ */
144
+ isResponse(message) {
145
+ return message?.result !== undefined || message?.error !== undefined;
146
+ }
33
147
  /**
34
148
  * Checks if the stream is in a valid state:
35
149
  * - Stream status is 'open'
@@ -49,33 +163,11 @@ export class oNodeStream extends oObject {
49
163
  get age() {
50
164
  return Date.now() - this.createdAt;
51
165
  }
52
- /**
53
- * Returns the stream type (defaults to 'general' for backward compatibility)
54
- */
55
- get streamType() {
56
- return this.config.streamType || 'general';
57
- }
58
- /**
59
- * Checks if this stream is designated as a dedicated reader
60
- */
61
- get isDedicatedReader() {
62
- return this.streamType === 'dedicated-reader';
63
- }
64
- /**
65
- * Checks if this stream is designated for request-response cycles
66
- */
67
- get isRequestResponse() {
68
- return this.streamType === 'request-response';
69
- }
70
166
  async close() {
71
- // Don't close if reuse policy is enabled
72
- if (this.config.reusePolicy === 'reuse') {
73
- this.logger.debug('Stream reuse enabled, not closing stream');
74
- return;
75
- }
76
167
  if (this.p2pStream.status === 'open') {
77
168
  try {
78
169
  // force the close for now until we can implement a proper close
170
+ this.logger.debug('Closing p2p stream');
79
171
  await this.p2pStream.abort(new Error('Stream closed'));
80
172
  }
81
173
  catch (error) {
@@ -83,4 +175,25 @@ export class oNodeStream extends oObject {
83
175
  }
84
176
  }
85
177
  }
178
+ get id() {
179
+ return this.p2pStream?.id;
180
+ }
181
+ /**
182
+ * Add event listener
183
+ */
184
+ on(event, listener) {
185
+ this.eventEmitter.on(event, listener);
186
+ }
187
+ /**
188
+ * Remove event listener
189
+ */
190
+ off(event, listener) {
191
+ this.eventEmitter.off(event, listener);
192
+ }
193
+ /**
194
+ * Emit event
195
+ */
196
+ emit(event, data) {
197
+ this.eventEmitter.emit(event, data);
198
+ }
86
199
  }
@@ -1,20 +1,9 @@
1
1
  /// <reference types="node" />
2
2
  import type { Stream } from '@libp2p/interface';
3
- /**
4
- * Stream reuse policy determines how streams are managed across multiple requests
5
- */
6
- export type StreamReusePolicy = 'none' | 'reuse' | 'pool';
7
3
  /**
8
4
  * Configuration for StreamHandler behavior
9
5
  */
10
6
  export interface StreamHandlerConfig {
11
- /**
12
- * Stream reuse policy:
13
- * - 'none': Create new stream for each request (default)
14
- * - 'reuse': Reuse existing open streams
15
- * - 'pool': (Future) Maintain a pool of streams
16
- */
17
- reusePolicy?: StreamReusePolicy;
18
7
  /**
19
8
  * Timeout in milliseconds to wait for stream drain when buffer is full
20
9
  * @default 30000 (30 seconds)