@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.
Files changed (60) hide show
  1. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts +1 -0
  2. package/dist/src/connection/interfaces/o-node-connection-manager.config.d.ts.map +1 -1
  3. package/dist/src/connection/o-node-connection.d.ts +0 -1
  4. package/dist/src/connection/o-node-connection.d.ts.map +1 -1
  5. package/dist/src/connection/o-node-connection.js +0 -8
  6. package/dist/src/connection/o-node-connection.manager.d.ts +41 -4
  7. package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
  8. package/dist/src/connection/o-node-connection.manager.js +187 -45
  9. package/dist/src/connection/stream-handler.d.ts.map +1 -1
  10. package/dist/src/connection/stream-handler.js +0 -2
  11. package/dist/src/managers/o-connection-heartbeat.manager.d.ts.map +1 -1
  12. package/dist/src/managers/o-connection-heartbeat.manager.js +15 -1
  13. package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -1
  14. package/dist/src/managers/o-reconnection.manager.js +12 -7
  15. package/dist/src/o-node.d.ts +19 -0
  16. package/dist/src/o-node.d.ts.map +1 -1
  17. package/dist/src/o-node.js +89 -11
  18. package/dist/src/o-node.tool.d.ts.map +1 -1
  19. package/dist/src/o-node.tool.js +5 -0
  20. package/dist/src/router/o-node.router.d.ts.map +1 -1
  21. package/dist/src/router/o-node.router.js +16 -6
  22. package/dist/src/router/o-node.routing-policy.d.ts.map +1 -1
  23. package/dist/src/router/o-node.routing-policy.js +4 -0
  24. package/dist/test/connection-management.spec.d.ts +2 -0
  25. package/dist/test/connection-management.spec.d.ts.map +1 -0
  26. package/dist/test/connection-management.spec.js +370 -0
  27. package/dist/test/helpers/connection-spy.d.ts +124 -0
  28. package/dist/test/helpers/connection-spy.d.ts.map +1 -0
  29. package/dist/test/helpers/connection-spy.js +229 -0
  30. package/dist/test/helpers/index.d.ts +6 -0
  31. package/dist/test/helpers/index.d.ts.map +1 -0
  32. package/dist/test/helpers/index.js +12 -0
  33. package/dist/test/helpers/network-builder.d.ts +109 -0
  34. package/dist/test/helpers/network-builder.d.ts.map +1 -0
  35. package/dist/test/helpers/network-builder.js +309 -0
  36. package/dist/test/helpers/simple-node-builder.d.ts +50 -0
  37. package/dist/test/helpers/simple-node-builder.d.ts.map +1 -0
  38. package/dist/test/helpers/simple-node-builder.js +66 -0
  39. package/dist/test/helpers/test-environment.d.ts +140 -0
  40. package/dist/test/helpers/test-environment.d.ts.map +1 -0
  41. package/dist/test/helpers/test-environment.js +184 -0
  42. package/dist/test/helpers/test-node.tool.d.ts +31 -0
  43. package/dist/test/helpers/test-node.tool.d.ts.map +1 -1
  44. package/dist/test/helpers/test-node.tool.js +49 -0
  45. package/dist/test/leader-transport-validation.spec.d.ts +2 -0
  46. package/dist/test/leader-transport-validation.spec.d.ts.map +1 -0
  47. package/dist/test/leader-transport-validation.spec.js +177 -0
  48. package/dist/test/network-communication.spec.d.ts +2 -0
  49. package/dist/test/network-communication.spec.d.ts.map +1 -0
  50. package/dist/test/network-communication.spec.js +256 -0
  51. package/dist/test/o-node.spec.d.ts +2 -0
  52. package/dist/test/o-node.spec.d.ts.map +1 -0
  53. package/dist/test/o-node.spec.js +247 -0
  54. package/dist/test/parent-child-registration.spec.d.ts +2 -0
  55. package/dist/test/parent-child-registration.spec.d.ts.map +1 -0
  56. package/dist/test/parent-child-registration.spec.js +177 -0
  57. package/dist/test/search-resolver.spec.d.ts +2 -0
  58. package/dist/test/search-resolver.spec.d.ts.map +1 -0
  59. package/dist/test/search-resolver.spec.js +648 -0
  60. package/package.json +12 -7
@@ -75,11 +75,11 @@ export class oNode extends oToolBase {
75
75
  params: {
76
76
  eventType: 'node:stopping',
77
77
  eventData: {
78
- address: this.address.toString(),
78
+ address: this.address?.toString(),
79
79
  reason: 'graceful_shutdown',
80
80
  expectedDowntime: null,
81
81
  },
82
- source: this.address.toString(),
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.toString(),
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
- await this.use(this.config.parent, {
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.leader &&
309
- !this.address.protocol.includes(this.config.leader.protocol)) {
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.p2pNode && this.state !== NodeState.STOPPED) {
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;IAUhC,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"}
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"}
@@ -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;IAyB3E;;;OAGG;IACH,UAAU,CAAC,qBAAqB,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO;CAGtE"}
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;IAuB1D;;;;;;;;OAQG;IACH,0BAA0B,CACxB,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,KAAK,GACV,aAAa,GAAG,IAAI;CAmBxB"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=connection-management.spec.d.ts.map
@@ -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
+ });