@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.
- package/dist/src/connection/enums/o-node-message-event.d.ts +14 -0
- package/dist/src/connection/enums/o-node-message-event.d.ts.map +1 -0
- package/dist/src/connection/enums/o-node-message-event.js +5 -0
- package/dist/src/connection/index.d.ts +0 -3
- package/dist/src/connection/index.d.ts.map +1 -1
- package/dist/src/connection/index.js +0 -3
- package/dist/src/connection/interfaces/abort-signal.config.d.ts +5 -0
- package/dist/src/connection/interfaces/abort-signal.config.d.ts.map +1 -0
- package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts +13 -1
- package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
- package/dist/src/connection/interfaces/o-node-connection.config.d.ts +17 -3
- package/dist/src/connection/interfaces/o-node-connection.config.d.ts.map +1 -1
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts +3 -11
- package/dist/src/connection/interfaces/o-node-stream.config.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.d.ts +33 -11
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +113 -58
- package/dist/src/connection/o-node-connection.manager.d.ts +17 -62
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +65 -189
- package/dist/src/connection/o-node-stream.d.ts +48 -15
- package/dist/src/connection/o-node-stream.d.ts.map +1 -1
- package/dist/src/connection/o-node-stream.js +144 -31
- package/dist/src/connection/stream-handler.config.d.ts +0 -11
- package/dist/src/connection/stream-handler.config.d.ts.map +1 -1
- package/dist/src/connection/stream-manager.events.d.ts +7 -0
- package/dist/src/connection/stream-manager.events.d.ts.map +1 -1
- package/dist/src/connection/stream-manager.events.js +1 -0
- package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts +9 -0
- package/dist/src/lib/interfaces/o-node-request-manager.config.d.ts.map +1 -0
- package/dist/src/lib/interfaces/o-node-request-manager.config.js +1 -0
- package/dist/src/lib/o-node-request-manager.d.ts +46 -0
- package/dist/src/lib/o-node-request-manager.d.ts.map +1 -0
- package/dist/src/lib/o-node-request-manager.js +173 -0
- package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
- package/dist/src/managers/o-reconnection.manager.js +4 -0
- package/dist/src/managers/o-registration.manager.d.ts.map +1 -1
- package/dist/src/managers/o-registration.manager.js +9 -4
- package/dist/src/o-node.d.ts +6 -7
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +22 -37
- package/dist/src/o-node.tool.d.ts +3 -3
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +31 -65
- package/dist/src/router/o-node.router.d.ts.map +1 -1
- package/dist/src/router/o-node.router.js +4 -2
- package/dist/src/utils/connection.utils.d.ts +3 -3
- package/dist/src/utils/connection.utils.d.ts.map +1 -1
- package/dist/src/utils/connection.utils.js +46 -19
- package/dist/test/connection-management.spec.js +3 -0
- package/package.json +7 -7
- package/dist/src/connection/interfaces/stream-init-message.d.ts +0 -64
- package/dist/src/connection/interfaces/stream-init-message.d.ts.map +0 -1
- package/dist/src/connection/interfaces/stream-init-message.js +0 -18
- package/dist/src/connection/interfaces/stream-manager.config.d.ts +0 -8
- package/dist/src/connection/interfaces/stream-manager.config.d.ts.map +0 -1
- package/dist/src/connection/o-node-stream.manager.d.ts +0 -200
- package/dist/src/connection/o-node-stream.manager.d.ts.map +0 -1
- package/dist/src/connection/o-node-stream.manager.js +0 -633
- /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.
|
|
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
|
-
|
|
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
|
|
102
|
-
this.logger.debug('Caching connection for address:',
|
|
103
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 =
|
|
121
|
+
const connection = await this.createConnection({
|
|
185
122
|
nextHopAddress: nextHopAddress,
|
|
186
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
146
|
+
const { targetAddress, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, } = config;
|
|
207
147
|
if (!nextHopAddress) {
|
|
208
148
|
throw new Error('Invalid address passed');
|
|
209
149
|
}
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
154
|
+
const existingConnection = this.getCachedConnectionFromAddress(nextHopAddress);
|
|
216
155
|
if (existingConnection) {
|
|
217
|
-
this.logger.debug('Reusing cached connection for address:',
|
|
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,
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
178
|
+
this.cacheConnection(connection);
|
|
236
179
|
return connection;
|
|
237
180
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
67
|
+
* Add event listener
|
|
34
68
|
*/
|
|
35
|
-
|
|
69
|
+
on<K extends oNodeMessageEvent>(event: K, listener: (data: oNodeMessageEventData[K]) => void): void;
|
|
36
70
|
/**
|
|
37
|
-
*
|
|
71
|
+
* Remove event listener
|
|
38
72
|
*/
|
|
39
|
-
|
|
73
|
+
off<K extends oNodeMessageEvent>(event: K, listener: (data: oNodeMessageEventData[K]) => void): void;
|
|
40
74
|
/**
|
|
41
|
-
*
|
|
75
|
+
* Emit event
|
|
42
76
|
*/
|
|
43
|
-
|
|
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,
|
|
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
|
|
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)
|