@olane/o-node 0.7.13-alpha.0 → 0.7.13-alpha.2
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/interfaces/o-node-connection-manager.config.d.ts +1 -0
- package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.d.ts +0 -1
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +0 -8
- package/dist/src/connection/o-node-connection.manager.d.ts +41 -4
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +187 -45
- package/dist/src/connection/stream-handler.d.ts.map +1 -1
- package/dist/src/connection/stream-handler.js +0 -2
- package/dist/src/managers/o-connection-heartbeat.manager.d.ts.map +1 -1
- package/dist/src/managers/o-connection-heartbeat.manager.js +15 -1
- package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
- package/dist/src/managers/o-reconnection.manager.js +12 -7
- package/dist/src/o-node.d.ts +19 -0
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +89 -11
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +5 -0
- package/dist/src/router/o-node.router.d.ts.map +1 -1
- package/dist/src/router/o-node.router.js +16 -6
- package/dist/src/router/o-node.routing-policy.d.ts.map +1 -1
- package/dist/src/router/o-node.routing-policy.js +4 -0
- package/dist/test/connection-management.spec.d.ts +2 -0
- package/dist/test/connection-management.spec.d.ts.map +1 -0
- package/dist/test/connection-management.spec.js +370 -0
- package/dist/test/helpers/connection-spy.d.ts +124 -0
- package/dist/test/helpers/connection-spy.d.ts.map +1 -0
- package/dist/test/helpers/connection-spy.js +229 -0
- package/dist/test/helpers/index.d.ts +6 -0
- package/dist/test/helpers/index.d.ts.map +1 -0
- package/dist/test/helpers/index.js +12 -0
- package/dist/test/helpers/network-builder.d.ts +109 -0
- package/dist/test/helpers/network-builder.d.ts.map +1 -0
- package/dist/test/helpers/network-builder.js +309 -0
- package/dist/test/helpers/simple-node-builder.d.ts +50 -0
- package/dist/test/helpers/simple-node-builder.d.ts.map +1 -0
- package/dist/test/helpers/simple-node-builder.js +66 -0
- package/dist/test/helpers/test-environment.d.ts +140 -0
- package/dist/test/helpers/test-environment.d.ts.map +1 -0
- package/dist/test/helpers/test-environment.js +184 -0
- package/dist/test/helpers/test-node.tool.d.ts +31 -0
- package/dist/test/helpers/test-node.tool.d.ts.map +1 -1
- package/dist/test/helpers/test-node.tool.js +49 -0
- package/dist/test/leader-transport-validation.spec.d.ts +2 -0
- package/dist/test/leader-transport-validation.spec.d.ts.map +1 -0
- package/dist/test/leader-transport-validation.spec.js +177 -0
- package/dist/test/network-communication.spec.d.ts +2 -0
- package/dist/test/network-communication.spec.d.ts.map +1 -0
- package/dist/test/network-communication.spec.js +256 -0
- package/dist/test/o-node.spec.d.ts +2 -0
- package/dist/test/o-node.spec.d.ts.map +1 -0
- package/dist/test/o-node.spec.js +247 -0
- package/dist/test/parent-child-registration.spec.d.ts +2 -0
- package/dist/test/parent-child-registration.spec.d.ts.map +1 -0
- package/dist/test/parent-child-registration.spec.js +177 -0
- package/dist/test/search-resolver.spec.d.ts +2 -0
- package/dist/test/search-resolver.spec.d.ts.map +1 -0
- package/dist/test/search-resolver.spec.js +648 -0
- package/package.json +12 -7
package/dist/src/o-node.js
CHANGED
|
@@ -75,11 +75,11 @@ export class oNode extends oToolBase {
|
|
|
75
75
|
params: {
|
|
76
76
|
eventType: 'node:stopping',
|
|
77
77
|
eventData: {
|
|
78
|
-
address: this.address
|
|
78
|
+
address: this.address?.toString(),
|
|
79
79
|
reason: 'graceful_shutdown',
|
|
80
80
|
expectedDowntime: null,
|
|
81
81
|
},
|
|
82
|
-
source: this.address
|
|
82
|
+
source: this.address?.toString(),
|
|
83
83
|
},
|
|
84
84
|
}),
|
|
85
85
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000)),
|
|
@@ -94,12 +94,16 @@ export class oNode extends oToolBase {
|
|
|
94
94
|
this.logger.debug('No leader found, skipping unregistration');
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
+
// no need to unregister since we did not generate a peerID yet
|
|
98
|
+
if (!this.peerId) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
97
101
|
const address = new oNodeAddress(RestrictedAddresses.REGISTRY);
|
|
98
102
|
// attempt to unregister from the network
|
|
99
103
|
const params = {
|
|
100
104
|
method: 'remove',
|
|
101
105
|
params: {
|
|
102
|
-
peerId: this.peerId
|
|
106
|
+
peerId: this.peerId?.toString(),
|
|
103
107
|
},
|
|
104
108
|
};
|
|
105
109
|
this.use(address, params).catch((error) => {
|
|
@@ -135,6 +139,10 @@ export class oNode extends oToolBase {
|
|
|
135
139
|
this.logger.debug('Skipping parent registration, node is leader');
|
|
136
140
|
return;
|
|
137
141
|
}
|
|
142
|
+
if (!this.parent) {
|
|
143
|
+
this.logger.warn('no parent, skipping registration');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
138
146
|
if (!this.parent?.libp2pTransports?.length) {
|
|
139
147
|
this.logger.debug('Parent has no transports, waiting for reconnection & leader ack');
|
|
140
148
|
if (this.parent?.toString() === oAddress.leader().toString()) {
|
|
@@ -149,7 +157,8 @@ export class oNode extends oToolBase {
|
|
|
149
157
|
// TODO: should we remove the transports check to make this more consistent?
|
|
150
158
|
if (this.config.parent) {
|
|
151
159
|
this.logger.debug('Registering node with parent...', this.config.parent?.toString());
|
|
152
|
-
|
|
160
|
+
// avoid transports to ensure we do not try direct connection, we need to route via the leader for proper access controls
|
|
161
|
+
await this.use(new oNodeAddress(this.config.parent.value), {
|
|
153
162
|
method: 'child_register',
|
|
154
163
|
params: {
|
|
155
164
|
address: this.address.toString(),
|
|
@@ -162,6 +171,11 @@ export class oNode extends oToolBase {
|
|
|
162
171
|
}
|
|
163
172
|
}
|
|
164
173
|
async registerLeader() {
|
|
174
|
+
this.logger.info('Register leader called...');
|
|
175
|
+
if (!this.leader) {
|
|
176
|
+
this.logger.warn('No leader defined, skipping registration');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
165
179
|
const address = oAddress.registry();
|
|
166
180
|
const params = {
|
|
167
181
|
method: 'commit',
|
|
@@ -187,6 +201,7 @@ export class oNode extends oToolBase {
|
|
|
187
201
|
}
|
|
188
202
|
this.didRegister = true;
|
|
189
203
|
this.logger.debug('Registering node...');
|
|
204
|
+
await this.registerParent();
|
|
190
205
|
// register with the leader global registry
|
|
191
206
|
if (!this.config.leader) {
|
|
192
207
|
this.logger.warn('No leaders found, skipping registration');
|
|
@@ -195,7 +210,6 @@ export class oNode extends oToolBase {
|
|
|
195
210
|
else {
|
|
196
211
|
this.logger.debug('Registering node with leader...');
|
|
197
212
|
}
|
|
198
|
-
await this.registerParent();
|
|
199
213
|
await this.registerLeader();
|
|
200
214
|
this.logger.debug('Registration successful');
|
|
201
215
|
}
|
|
@@ -290,7 +304,6 @@ export class oNode extends oToolBase {
|
|
|
290
304
|
if (this.config.type === NodeType.LEADER) {
|
|
291
305
|
return false;
|
|
292
306
|
}
|
|
293
|
-
// deny everything else
|
|
294
307
|
return true;
|
|
295
308
|
},
|
|
296
309
|
// allow the user to override the default connection gater
|
|
@@ -305,9 +318,8 @@ export class oNode extends oToolBase {
|
|
|
305
318
|
maxParallelReconnects: 10,
|
|
306
319
|
};
|
|
307
320
|
// handle the address encapsulation
|
|
308
|
-
if (this.config.
|
|
309
|
-
|
|
310
|
-
const parentAddress = this.config.parent || this.config.leader;
|
|
321
|
+
if (this.config.parent) {
|
|
322
|
+
const parentAddress = this.config.parent;
|
|
311
323
|
this.address = CoreUtils.childAddress(parentAddress, this.address);
|
|
312
324
|
}
|
|
313
325
|
return params;
|
|
@@ -342,6 +354,7 @@ export class oNode extends oToolBase {
|
|
|
342
354
|
defaultReadTimeoutMs: this.config.connectionTimeouts?.readTimeoutMs,
|
|
343
355
|
defaultDrainTimeoutMs: this.config.connectionTimeouts?.drainTimeoutMs,
|
|
344
356
|
runOnLimitedConnection: this.config.runOnLimitedConnection ?? false,
|
|
357
|
+
originAddress: this.address?.value
|
|
345
358
|
});
|
|
346
359
|
}
|
|
347
360
|
async hookInitializeFinished() { }
|
|
@@ -358,9 +371,45 @@ export class oNode extends oToolBase {
|
|
|
358
371
|
this.logger.info(`Connection heartbeat config: leader=${this.connectionHeartbeatManager.getConfig().checkLeader}, ` +
|
|
359
372
|
`parent=${this.connectionHeartbeatManager.getConfig().checkParent}`);
|
|
360
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Validates that if a leader address is defined, it has associated transports.
|
|
376
|
+
* This is critical for non-leader nodes to be able to connect to their leader.
|
|
377
|
+
* @throws Error if leader is defined but has no transports
|
|
378
|
+
*/
|
|
379
|
+
async validateLeaderTransports() {
|
|
380
|
+
// Skip validation if this node is the leader itself
|
|
381
|
+
if (this.isLeader) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Skip validation if no leader is configured
|
|
385
|
+
const leaderAddress = this.config?.leader;
|
|
386
|
+
if (!leaderAddress) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Check if leader has transports
|
|
390
|
+
if (!leaderAddress.transports || leaderAddress.transports.length === 0) {
|
|
391
|
+
throw new Error(`Leader address is defined (${leaderAddress.toString()}) but has no transports. ` +
|
|
392
|
+
`Non-leader nodes require leader transports for network connectivity. ` +
|
|
393
|
+
`Please provide transports in the leader address configuration.`);
|
|
394
|
+
}
|
|
395
|
+
this.logger.debug('Leader transport validation passed', {
|
|
396
|
+
leader: leaderAddress.toString(),
|
|
397
|
+
transportCount: leaderAddress.transports.length
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* oNode-specific validation that runs before core start() logic.
|
|
402
|
+
*
|
|
403
|
+
* This ensures that leader transport configuration errors are
|
|
404
|
+
* surfaced quickly and before any libp2p nodes or other heavy
|
|
405
|
+
* resources are created.
|
|
406
|
+
*/
|
|
407
|
+
async validate() {
|
|
408
|
+
await this.validateLeaderTransports();
|
|
409
|
+
}
|
|
361
410
|
async initialize() {
|
|
362
411
|
this.logger.debug('Initializing node...');
|
|
363
|
-
if (this.
|
|
412
|
+
if (this.state !== NodeState.STOPPED && this.state !== NodeState.STARTING) {
|
|
364
413
|
throw new Error('Node is not in a valid state to be initialized');
|
|
365
414
|
}
|
|
366
415
|
if (!this.address.validate()) {
|
|
@@ -379,7 +428,7 @@ export class oNode extends oToolBase {
|
|
|
379
428
|
this.router.addResolver(new oMethodResolver(this.address));
|
|
380
429
|
this.router.addResolver(new oNodeResolver(this.address));
|
|
381
430
|
// setup a fallback resolver for non-leader nodes
|
|
382
|
-
if (this.isLeader === false) {
|
|
431
|
+
if (this.isLeader === false && !!this.leader) {
|
|
383
432
|
this.logger.debug('Adding leader resolver fallback...');
|
|
384
433
|
this.router.addResolver(new oLeaderResolverFallback(this.address));
|
|
385
434
|
}
|
|
@@ -421,6 +470,35 @@ export class oNode extends oToolBase {
|
|
|
421
470
|
if (this.p2pNode) {
|
|
422
471
|
await this.p2pNode.stop();
|
|
423
472
|
}
|
|
473
|
+
// Reset state to allow restart
|
|
474
|
+
this.resetState();
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Reset node state to allow restart after stop
|
|
478
|
+
* Called at the end of teardown()
|
|
479
|
+
*/
|
|
480
|
+
resetState() {
|
|
481
|
+
// Reset registration flag
|
|
482
|
+
this.didRegister = false;
|
|
483
|
+
// Clear peer references
|
|
484
|
+
this.peerId = undefined;
|
|
485
|
+
this.p2pNode = undefined;
|
|
486
|
+
// Clear managers
|
|
487
|
+
this.connectionManager = undefined;
|
|
488
|
+
this.connectionHeartbeatManager = undefined;
|
|
489
|
+
this.reconnectionManager = undefined;
|
|
490
|
+
// Reset address to staticAddress with no transports
|
|
491
|
+
this.address = new oNodeAddress(this.staticAddress.value, []);
|
|
492
|
+
// Reset hierarchy manager
|
|
493
|
+
this.hierarchyManager = new oNodeHierarchyManager({
|
|
494
|
+
leaders: this.config.leader ? [this.config.leader] : [],
|
|
495
|
+
parents: this.config.parent ? [this.config.parent] : [],
|
|
496
|
+
children: [],
|
|
497
|
+
});
|
|
498
|
+
// Clear router (will be recreated in initialize)
|
|
499
|
+
this.router = undefined;
|
|
500
|
+
// Call parent reset
|
|
501
|
+
super.resetState();
|
|
424
502
|
}
|
|
425
503
|
// IHeartbeatableNode interface methods
|
|
426
504
|
getLeaders() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAKrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,cAAc,CAAC,OAAO,EAAE,QAAQ;
|
|
1
|
+
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,QAAQ,EAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAKrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAehC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CA2B5D"}
|
package/dist/src/o-node.tool.js
CHANGED
|
@@ -11,6 +11,11 @@ import { StreamHandler } from './connection/stream-handler.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export class oNodeTool extends oTool(oServerNode) {
|
|
13
13
|
async handleProtocol(address) {
|
|
14
|
+
const protocols = this.p2pNode.getProtocols();
|
|
15
|
+
if (protocols.find((p) => p === address.protocol)) {
|
|
16
|
+
// already handling
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
this.logger.debug('Handling protocol: ' + address.protocol);
|
|
15
20
|
await this.p2pNode.handle(address.protocol, this.handleStream.bind(this), {
|
|
16
21
|
maxInboundStreams: 10000,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.router.d.ts","sourceRoot":"","sources":["../../../src/router/o-node.router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAML,cAAc,EAGd,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAM5C,qBAAa,WAAY,SAAQ,WAAW;IAC1C,OAAO,CAAC,aAAa,CAAqB;;IAO1C;;;;;;;;OAQG;cACa,OAAO,CACrB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,KAAK,GACV,OAAO,CAAC,GAAG,CAAC;IA6Bf;;;OAGG;YACW,kBAAkB;IA8DhC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAS5B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAWhC;;OAEG;YACW,eAAe;IAsC7B;;;OAGG;IACG,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"o-node.router.d.ts","sourceRoot":"","sources":["../../../src/router/o-node.router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAML,cAAc,EAGd,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAM5C,qBAAa,WAAY,SAAQ,WAAW;IAC1C,OAAO,CAAC,aAAa,CAAqB;;IAO1C;;;;;;;;OAQG;cACa,OAAO,CACrB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,KAAK,GACV,OAAO,CAAC,GAAG,CAAC;IA6Bf;;;OAGG;YACW,kBAAkB;IA8DhC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAS5B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAWhC;;OAEG;YACW,eAAe;IAsC7B;;;OAGG;IACG,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC;IAmC3E;;;OAGG;IACH,UAAU,CAAC,qBAAqB,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO;CAGtE"}
|
|
@@ -141,7 +141,7 @@ export class oNodeRouter extends oToolRouter {
|
|
|
141
141
|
}
|
|
142
142
|
catch (error) {
|
|
143
143
|
if (error?.name === 'UnsupportedProtocolError') {
|
|
144
|
-
throw new oError(oErrorCodes.NOT_FOUND, 'Address not found');
|
|
144
|
+
throw new oError(oErrorCodes.NOT_FOUND, 'Address not found: ' + address.value);
|
|
145
145
|
}
|
|
146
146
|
throw error;
|
|
147
147
|
}
|
|
@@ -151,17 +151,27 @@ export class oNodeRouter extends oToolRouter {
|
|
|
151
151
|
* First checks routing policy for external routing, then applies resolver chain.
|
|
152
152
|
*/
|
|
153
153
|
async translate(address, node) {
|
|
154
|
-
// Check if external routing is needed
|
|
155
|
-
const externalRoute = this.routingPolicy.getExternalRoutingStrategy(address, node);
|
|
156
|
-
if (externalRoute) {
|
|
157
|
-
return externalRoute;
|
|
158
|
-
}
|
|
159
154
|
// Apply resolver chain for internal routing
|
|
155
|
+
if (!node.parent && !node.leader && address.transports?.length > 0) {
|
|
156
|
+
// independent node
|
|
157
|
+
return {
|
|
158
|
+
nextHopAddress: address,
|
|
159
|
+
targetAddress: address,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
160
162
|
const { nextHopAddress, targetAddress, requestOverride } = await this.addressResolution.resolve({
|
|
161
163
|
address,
|
|
162
164
|
node,
|
|
163
165
|
targetAddress: address,
|
|
164
166
|
});
|
|
167
|
+
// if we defaulted back to the leader
|
|
168
|
+
if (nextHopAddress.value === oAddress.leader().value) {
|
|
169
|
+
// Check if external routing is needed for leader routing
|
|
170
|
+
const externalRoute = this.routingPolicy.getExternalRoutingStrategy(address, node);
|
|
171
|
+
if (externalRoute) {
|
|
172
|
+
return externalRoute;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
165
175
|
return {
|
|
166
176
|
nextHopAddress,
|
|
167
177
|
targetAddress: targetAddress,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.routing-policy.d.ts","sourceRoot":"","sources":["../../../src/router/o-node.routing-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAG1C;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,cAAc;IACpD;;;;;;;;;OASG;IACH,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO;
|
|
1
|
+
{"version":3,"file":"o-node.routing-policy.d.ts","sourceRoot":"","sources":["../../../src/router/o-node.routing-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAG1C;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,cAAc;IACpD;;;;;;;;;OASG;IACH,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO;IA4B1D;;;;;;;;OAQG;IACH,0BAA0B,CACxB,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,KAAK,GACV,aAAa,GAAG,IAAI;CAmBxB"}
|
|
@@ -21,6 +21,10 @@ export class oNodeRoutingPolicy extends oRoutingPolicy {
|
|
|
21
21
|
if (node.hierarchyManager.parents.some((p) => p.equals(address))) {
|
|
22
22
|
return true;
|
|
23
23
|
}
|
|
24
|
+
// if we are trying to connect to a child, it's internal
|
|
25
|
+
if (node.hierarchyManager.children.some((p) => p.equals(address))) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
24
28
|
if (nodeAddress.paths.indexOf(oAddress.leader().paths) !== -1 && // if the address has a leader
|
|
25
29
|
nodeAddress.libp2pTransports?.length > 0) {
|
|
26
30
|
// transports are provided, let's see if they match our known leaders
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-management.spec.d.ts","sourceRoot":"","sources":["../../test/connection-management.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { TestEnvironment } from './helpers/index.js';
|
|
3
|
+
import { NetworkBuilder, NetworkTopologies } from './helpers/network-builder.js';
|
|
4
|
+
import { createConnectionSpy } from './helpers/connection-spy.js';
|
|
5
|
+
import { oNodeAddress } from '../src/router/o-node.address.js';
|
|
6
|
+
import { oNodeTransport } from '../src/index.js';
|
|
7
|
+
describe('Connection Management', () => {
|
|
8
|
+
const env = new TestEnvironment();
|
|
9
|
+
let builder;
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
if (builder) {
|
|
12
|
+
await builder.cleanup();
|
|
13
|
+
}
|
|
14
|
+
await env.cleanup();
|
|
15
|
+
});
|
|
16
|
+
describe('Connection Pooling', () => {
|
|
17
|
+
it('should cache and reuse connections', async () => {
|
|
18
|
+
builder = await NetworkTopologies.twoNode();
|
|
19
|
+
const leader = builder.getNode('o://leader');
|
|
20
|
+
const child = builder.getNode('o://child');
|
|
21
|
+
const spy = createConnectionSpy(leader);
|
|
22
|
+
spy.start();
|
|
23
|
+
// Make first request (establishes connection)
|
|
24
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
25
|
+
method: 'ping',
|
|
26
|
+
params: { message: 'first' },
|
|
27
|
+
});
|
|
28
|
+
const connectionsAfterFirst = spy.getSummary().currentConnections;
|
|
29
|
+
// Make second request (should reuse connection)
|
|
30
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
31
|
+
method: 'ping',
|
|
32
|
+
params: { message: 'second' },
|
|
33
|
+
});
|
|
34
|
+
const connectionsAfterSecond = spy.getSummary().currentConnections;
|
|
35
|
+
// Connection count should remain the same (reused)
|
|
36
|
+
expect(connectionsAfterFirst).to.equal(connectionsAfterSecond);
|
|
37
|
+
expect(connectionsAfterFirst).to.be.greaterThan(0);
|
|
38
|
+
spy.stop();
|
|
39
|
+
});
|
|
40
|
+
it('should maintain separate connections to different nodes', async () => {
|
|
41
|
+
builder = await NetworkTopologies.fiveNode();
|
|
42
|
+
const leader = builder.getNode('o://leader');
|
|
43
|
+
const parent1 = builder.getNode('o://parent1');
|
|
44
|
+
const parent2 = builder.getNode('o://parent2');
|
|
45
|
+
const spy = createConnectionSpy(leader);
|
|
46
|
+
spy.start();
|
|
47
|
+
// Call different nodes
|
|
48
|
+
await leader.use(parent1.address, {
|
|
49
|
+
method: 'echo',
|
|
50
|
+
params: { message: 'to parent1' },
|
|
51
|
+
});
|
|
52
|
+
await leader.use(parent2.address, {
|
|
53
|
+
method: 'echo',
|
|
54
|
+
params: { message: 'to parent2' },
|
|
55
|
+
});
|
|
56
|
+
const stats = spy.getConnectionStats();
|
|
57
|
+
// Should have connections to both parents
|
|
58
|
+
expect(stats.length).to.be.greaterThan(1);
|
|
59
|
+
spy.stop();
|
|
60
|
+
});
|
|
61
|
+
it('should handle connection pool efficiently under load', async () => {
|
|
62
|
+
builder = await NetworkTopologies.fiveNode();
|
|
63
|
+
const leader = builder.getNode('o://leader');
|
|
64
|
+
const child1 = builder.getNode('o://child1');
|
|
65
|
+
const child2 = builder.getNode('o://child2');
|
|
66
|
+
const spy = createConnectionSpy(leader);
|
|
67
|
+
spy.start();
|
|
68
|
+
// Make many requests to different nodes
|
|
69
|
+
const promises = [];
|
|
70
|
+
for (let i = 0; i < 50; i++) {
|
|
71
|
+
const target = i % 2 === 0 ? child1 : child2;
|
|
72
|
+
promises.push(leader.use(target.address, {
|
|
73
|
+
method: 'echo',
|
|
74
|
+
params: { message: `request-${i}` },
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
await Promise.all(promises);
|
|
78
|
+
const summary = spy.getSummary();
|
|
79
|
+
// Connection count should be reasonable (not 50)
|
|
80
|
+
expect(summary.currentConnections).to.be.lessThan(10);
|
|
81
|
+
expect(summary.currentConnections).to.be.greaterThan(0);
|
|
82
|
+
spy.stop();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Connection Status', () => {
|
|
86
|
+
it('should report correct connection status', async () => {
|
|
87
|
+
builder = await NetworkTopologies.twoNode();
|
|
88
|
+
const leader = builder.getNode('o://leader');
|
|
89
|
+
const child = builder.getNode('o://child');
|
|
90
|
+
const spy = createConnectionSpy(leader);
|
|
91
|
+
spy.start();
|
|
92
|
+
// Establish connection
|
|
93
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
94
|
+
method: 'echo',
|
|
95
|
+
params: { message: 'test' },
|
|
96
|
+
});
|
|
97
|
+
const stats = spy.getConnectionStats();
|
|
98
|
+
expect(stats.length).to.be.greaterThan(0);
|
|
99
|
+
const connection = stats[0];
|
|
100
|
+
expect(connection.status).to.equal('open');
|
|
101
|
+
expect(connection.peerId).to.be.a('string');
|
|
102
|
+
expect(connection.remoteAddr).to.be.a('string');
|
|
103
|
+
spy.stop();
|
|
104
|
+
});
|
|
105
|
+
it('should detect open connections', async () => {
|
|
106
|
+
builder = await NetworkTopologies.twoNode();
|
|
107
|
+
const leader = builder.getNode('o://leader');
|
|
108
|
+
const child = builder.getNode('o://child');
|
|
109
|
+
// Get child's peer ID
|
|
110
|
+
const childPeerId = child.address.libp2pTransports[0].toPeerId();
|
|
111
|
+
const spy = createConnectionSpy(leader);
|
|
112
|
+
spy.start();
|
|
113
|
+
// Establish connection
|
|
114
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
115
|
+
method: 'echo',
|
|
116
|
+
params: { message: 'test' },
|
|
117
|
+
});
|
|
118
|
+
// Verify connection exists
|
|
119
|
+
const hasConnection = spy.hasConnectionToPeer(childPeerId);
|
|
120
|
+
expect(hasConnection).to.be.true;
|
|
121
|
+
spy.stop();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('Connection Validation', () => {
|
|
125
|
+
it('should validate connection before transmission', async () => {
|
|
126
|
+
builder = await NetworkTopologies.twoNode();
|
|
127
|
+
const leader = builder.getNode('o://leader');
|
|
128
|
+
const child = builder.getNode('o://child');
|
|
129
|
+
// Valid connection should work
|
|
130
|
+
const response = await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
131
|
+
method: 'echo',
|
|
132
|
+
params: { message: 'test' },
|
|
133
|
+
});
|
|
134
|
+
expect(response.result.success).to.be.true;
|
|
135
|
+
});
|
|
136
|
+
it('should handle connection to unreachable node', async () => {
|
|
137
|
+
builder = new NetworkBuilder();
|
|
138
|
+
const leader = await builder.addNode('o://leader');
|
|
139
|
+
// Create address to non-existent node
|
|
140
|
+
const fakeAddress = new oNodeAddress('o://nonexistent', [
|
|
141
|
+
new oNodeTransport('/ip4/127.0.0.1/tcp/4099'),
|
|
142
|
+
]);
|
|
143
|
+
// Attempt to connect should fail gracefully
|
|
144
|
+
await leader.use(fakeAddress, {
|
|
145
|
+
method: 'echo',
|
|
146
|
+
params: { message: 'test' },
|
|
147
|
+
}).catch((err) => {
|
|
148
|
+
expect(err.code).to.be.equal('ECONNREFUSED');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should verify connection is open before use', async () => {
|
|
152
|
+
builder = await NetworkTopologies.twoNode();
|
|
153
|
+
const leader = builder.getNode('o://leader');
|
|
154
|
+
const child = builder.getNode('o://child');
|
|
155
|
+
const spy = createConnectionSpy(leader);
|
|
156
|
+
spy.start();
|
|
157
|
+
// Make request
|
|
158
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
159
|
+
method: 'echo',
|
|
160
|
+
params: { message: 'test' },
|
|
161
|
+
});
|
|
162
|
+
// Verify connection is open
|
|
163
|
+
const stats = spy.getConnectionStats();
|
|
164
|
+
expect(stats.length).to.be.greaterThan(0);
|
|
165
|
+
expect(stats[0].status).to.equal('open');
|
|
166
|
+
spy.stop();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('Connection Recovery', () => {
|
|
170
|
+
it('should handle transient connection errors', async () => {
|
|
171
|
+
builder = await NetworkTopologies.twoNode();
|
|
172
|
+
const leader = builder.getNode('o://leader');
|
|
173
|
+
const child = builder.getNode('o://child');
|
|
174
|
+
// Make successful request
|
|
175
|
+
const response1 = await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
176
|
+
method: 'echo',
|
|
177
|
+
params: { message: 'before' },
|
|
178
|
+
});
|
|
179
|
+
expect(response1.result.success).to.be.true;
|
|
180
|
+
// Simulate brief disconnection by stopping and restarting child
|
|
181
|
+
await child.stop();
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
183
|
+
await child.start();
|
|
184
|
+
// Wait for reconnection
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
186
|
+
// Subsequent request should eventually work
|
|
187
|
+
// Note: May need retry logic depending on implementation
|
|
188
|
+
const response2 = await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
189
|
+
method: 'echo',
|
|
190
|
+
params: { message: 'after' },
|
|
191
|
+
});
|
|
192
|
+
// This may fail if connection not re-established
|
|
193
|
+
// Test verifies graceful error handling
|
|
194
|
+
if (response2.result.success) {
|
|
195
|
+
expect(response2.result.data.message).to.equal('after');
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
expect(response2.result.error).to.exist;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
it('should maintain other connections when one fails', async () => {
|
|
202
|
+
builder = await NetworkTopologies.fiveNode();
|
|
203
|
+
const leader = builder.getNode('o://leader');
|
|
204
|
+
const child1 = builder.getNode('o://child1');
|
|
205
|
+
const child2 = builder.getNode('o://child2');
|
|
206
|
+
// Establish connections to both children
|
|
207
|
+
await leader.use(child1.address, {
|
|
208
|
+
method: 'echo',
|
|
209
|
+
params: { message: 'child1' },
|
|
210
|
+
});
|
|
211
|
+
await leader.use(child2.address, {
|
|
212
|
+
method: 'echo',
|
|
213
|
+
params: { message: 'child2' },
|
|
214
|
+
});
|
|
215
|
+
// Stop child1
|
|
216
|
+
await builder.stopNode('o://child1');
|
|
217
|
+
// Connection to child2 should still work
|
|
218
|
+
const response = await leader.use(child2.address, {
|
|
219
|
+
method: 'echo',
|
|
220
|
+
params: { message: 'child2-after' },
|
|
221
|
+
});
|
|
222
|
+
expect(response.result.success).to.be.true;
|
|
223
|
+
expect(response.result.data.message).to.equal('child2-after');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('Multi-node Connection Management', () => {
|
|
227
|
+
it('should manage connections in complex topology', async () => {
|
|
228
|
+
builder = await NetworkTopologies.complex();
|
|
229
|
+
const leader = builder.getNode('o://leader');
|
|
230
|
+
const spy = createConnectionSpy(leader);
|
|
231
|
+
spy.start();
|
|
232
|
+
// Make requests to various nodes
|
|
233
|
+
for (let i = 1; i <= 3; i++) {
|
|
234
|
+
const parent = builder.getNode(`o://parent${i}`);
|
|
235
|
+
await leader.use(parent.address, {
|
|
236
|
+
method: 'echo',
|
|
237
|
+
params: { message: `parent${i}` },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const summary = spy.getSummary();
|
|
241
|
+
// Should have connections to parents
|
|
242
|
+
expect(summary.currentConnections).to.be.greaterThan(0);
|
|
243
|
+
expect(summary.currentConnections).to.be.lessThan(10);
|
|
244
|
+
spy.stop();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe('Connection Metadata', () => {
|
|
248
|
+
it('should track connection creation time', async () => {
|
|
249
|
+
builder = await NetworkTopologies.twoNode();
|
|
250
|
+
const leader = builder.getNode('o://leader');
|
|
251
|
+
const child = builder.getNode('o://child');
|
|
252
|
+
const spy = createConnectionSpy(leader);
|
|
253
|
+
spy.start();
|
|
254
|
+
const beforeTime = Date.now();
|
|
255
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
256
|
+
method: 'echo',
|
|
257
|
+
params: { message: 'test' },
|
|
258
|
+
});
|
|
259
|
+
const afterTime = Date.now();
|
|
260
|
+
const stats = spy.getConnectionStats();
|
|
261
|
+
if (stats.length > 0) {
|
|
262
|
+
expect(stats[0].created).to.be.at.least(beforeTime);
|
|
263
|
+
expect(stats[0].created).to.be.at.most(afterTime);
|
|
264
|
+
}
|
|
265
|
+
spy.stop();
|
|
266
|
+
});
|
|
267
|
+
it('should track remote peer information', async () => {
|
|
268
|
+
builder = await NetworkTopologies.twoNode();
|
|
269
|
+
const leader = builder.getNode('o://leader');
|
|
270
|
+
const child = builder.getNode('o://child');
|
|
271
|
+
const spy = createConnectionSpy(leader);
|
|
272
|
+
spy.start();
|
|
273
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
274
|
+
method: 'echo',
|
|
275
|
+
params: { message: 'test' },
|
|
276
|
+
});
|
|
277
|
+
const stats = spy.getConnectionStats();
|
|
278
|
+
if (stats.length > 0) {
|
|
279
|
+
const connection = stats[0];
|
|
280
|
+
expect(connection.peerId).to.be.a('string');
|
|
281
|
+
expect(connection.peerId.length).to.be.greaterThan(0);
|
|
282
|
+
expect(connection.remoteAddr).to.be.a('string');
|
|
283
|
+
expect(connection.remoteAddr.length).to.be.greaterThan(0);
|
|
284
|
+
}
|
|
285
|
+
spy.stop();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('Connection Gating', () => {
|
|
289
|
+
it('should enforce connection gating rules', async () => {
|
|
290
|
+
builder = await NetworkTopologies.twoNode();
|
|
291
|
+
const leader = builder.getNode('o://leader');
|
|
292
|
+
const child = builder.getNode('o://child');
|
|
293
|
+
// Parent-child connections should be allowed
|
|
294
|
+
const response = await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
295
|
+
method: 'echo',
|
|
296
|
+
params: { message: 'test' },
|
|
297
|
+
});
|
|
298
|
+
expect(response.result.success).to.be.true;
|
|
299
|
+
});
|
|
300
|
+
it('should allow connections within hierarchy', async () => {
|
|
301
|
+
builder = await NetworkTopologies.threeNode();
|
|
302
|
+
const leader = builder.getNode('o://leader');
|
|
303
|
+
const parent = builder.getNode('o://parent');
|
|
304
|
+
const child = builder.getNode('o://child');
|
|
305
|
+
// All connections in hierarchy should work
|
|
306
|
+
const response1 = await leader.use(parent.address, {
|
|
307
|
+
method: 'echo',
|
|
308
|
+
params: { message: 'leader-to-parent' },
|
|
309
|
+
});
|
|
310
|
+
const response2 = await parent.use(child.address, {
|
|
311
|
+
method: 'echo',
|
|
312
|
+
params: { message: 'parent-to-child' },
|
|
313
|
+
});
|
|
314
|
+
const response3 = await child.use(parent.address, {
|
|
315
|
+
method: 'echo',
|
|
316
|
+
params: { message: 'child-to-parent' },
|
|
317
|
+
});
|
|
318
|
+
expect(response1.result.success).to.be.true;
|
|
319
|
+
expect(response2.result.success).to.be.true;
|
|
320
|
+
expect(response3.result.success).to.be.true;
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('Connection Cleanup', () => {
|
|
324
|
+
it('should clean up connections on node stop', async () => {
|
|
325
|
+
builder = await NetworkTopologies.twoNode();
|
|
326
|
+
const leader = builder.getNode('o://leader');
|
|
327
|
+
const child = builder.getNode('o://child');
|
|
328
|
+
// Establish connection
|
|
329
|
+
await leader.use(new oNodeAddress(child.address.toString(), child.address.libp2pTransports), {
|
|
330
|
+
method: 'echo',
|
|
331
|
+
params: { message: 'test' },
|
|
332
|
+
});
|
|
333
|
+
const spy = createConnectionSpy(leader);
|
|
334
|
+
spy.start();
|
|
335
|
+
const beforeConnections = spy.getSummary().currentConnections;
|
|
336
|
+
expect(beforeConnections).to.be.greaterThan(0);
|
|
337
|
+
// Stop child
|
|
338
|
+
await builder.stopNode('o://child');
|
|
339
|
+
// Wait for cleanup
|
|
340
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
341
|
+
// Note: Connection may still exist until cleanup cycle runs
|
|
342
|
+
// This test verifies stop mechanism doesn't throw errors
|
|
343
|
+
spy.stop();
|
|
344
|
+
});
|
|
345
|
+
it('should handle cleanup of multiple connections', async () => {
|
|
346
|
+
builder = await NetworkTopologies.fiveNode();
|
|
347
|
+
const leader = builder.getNode('o://leader');
|
|
348
|
+
// Establish connections
|
|
349
|
+
const child1 = builder.getNode('o://child1');
|
|
350
|
+
const child2 = builder.getNode('o://child2');
|
|
351
|
+
await leader.use(child1.address, {
|
|
352
|
+
method: 'echo',
|
|
353
|
+
params: { message: 'child1' },
|
|
354
|
+
});
|
|
355
|
+
await leader.use(child2.address, {
|
|
356
|
+
method: 'echo',
|
|
357
|
+
params: { message: 'child2' },
|
|
358
|
+
});
|
|
359
|
+
// Stop all children
|
|
360
|
+
await builder.stopNode('o://child1');
|
|
361
|
+
await builder.stopNode('o://child2');
|
|
362
|
+
// Leader should remain operational
|
|
363
|
+
const response = await leader.use(leader.address, {
|
|
364
|
+
method: 'get_info',
|
|
365
|
+
params: {},
|
|
366
|
+
});
|
|
367
|
+
expect(response.result.success).to.be.true;
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|