@olane/o-node 0.7.12 → 0.7.13-alpha.1
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 +33 -4
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +153 -44
- 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 +5 -0
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +46 -8
- 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/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.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.message).to.be.equal('Unable to extract peer ID from address: o://nonexistent');
|
|
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
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { oNode } from '../../src/o-node.js';
|
|
2
|
+
import { oNodeTool } from '../../src/o-node.tool.js';
|
|
3
|
+
import type { Connection, Stream } from '@libp2p/interface';
|
|
4
|
+
/**
|
|
5
|
+
* Connection event captured by the spy
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectionEvent {
|
|
8
|
+
type: 'peer:connect' | 'peer:disconnect' | 'connection:open' | 'connection:close' | 'stream:open' | 'stream:close';
|
|
9
|
+
timestamp: number;
|
|
10
|
+
peerId?: string;
|
|
11
|
+
remoteAddr?: string;
|
|
12
|
+
protocol?: string;
|
|
13
|
+
streamId?: string;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Stream statistics
|
|
18
|
+
*/
|
|
19
|
+
export interface StreamStats {
|
|
20
|
+
protocol: string;
|
|
21
|
+
status: string;
|
|
22
|
+
writeStatus?: string;
|
|
23
|
+
readStatus?: string;
|
|
24
|
+
created: number;
|
|
25
|
+
closed?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Connection statistics
|
|
29
|
+
*/
|
|
30
|
+
export interface ConnectionStats {
|
|
31
|
+
peerId: string;
|
|
32
|
+
status: string;
|
|
33
|
+
remoteAddr: string;
|
|
34
|
+
streams: StreamStats[];
|
|
35
|
+
created: number;
|
|
36
|
+
closed?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Spy that monitors libp2p connections and streams for testing
|
|
40
|
+
*/
|
|
41
|
+
export declare class ConnectionSpy {
|
|
42
|
+
private events;
|
|
43
|
+
private node;
|
|
44
|
+
private listeners;
|
|
45
|
+
private isMonitoring;
|
|
46
|
+
constructor(node: oNode | oNodeTool);
|
|
47
|
+
/**
|
|
48
|
+
* Start monitoring connection events
|
|
49
|
+
*/
|
|
50
|
+
start(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Stop monitoring
|
|
53
|
+
*/
|
|
54
|
+
stop(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Clear all captured events
|
|
57
|
+
*/
|
|
58
|
+
clear(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Get all captured events
|
|
61
|
+
*/
|
|
62
|
+
getEvents(): ConnectionEvent[];
|
|
63
|
+
/**
|
|
64
|
+
* Get events of a specific type
|
|
65
|
+
*/
|
|
66
|
+
getEventsByType(type: ConnectionEvent['type']): ConnectionEvent[];
|
|
67
|
+
/**
|
|
68
|
+
* Get events for a specific peer
|
|
69
|
+
*/
|
|
70
|
+
getEventsForPeer(peerId: string): ConnectionEvent[];
|
|
71
|
+
/**
|
|
72
|
+
* Get current connection statistics
|
|
73
|
+
*/
|
|
74
|
+
getConnectionStats(): ConnectionStats[];
|
|
75
|
+
/**
|
|
76
|
+
* Get total stream count across all connections
|
|
77
|
+
*/
|
|
78
|
+
getTotalStreamCount(): number;
|
|
79
|
+
/**
|
|
80
|
+
* Get stream count for a specific protocol
|
|
81
|
+
*/
|
|
82
|
+
getStreamCountByProtocol(protocol: string): number;
|
|
83
|
+
/**
|
|
84
|
+
* Check if a connection exists to a peer
|
|
85
|
+
*/
|
|
86
|
+
hasConnectionToPeer(peerId: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Get connection to a specific peer
|
|
89
|
+
*/
|
|
90
|
+
getConnectionToPeer(peerId: string): Connection | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Get all streams for a specific peer
|
|
93
|
+
*/
|
|
94
|
+
getStreamsForPeer(peerId: string): Stream[];
|
|
95
|
+
/**
|
|
96
|
+
* Wait for a specific event to occur
|
|
97
|
+
*/
|
|
98
|
+
waitForEvent(type: ConnectionEvent['type'], timeoutMs?: number): Promise<ConnectionEvent>;
|
|
99
|
+
/**
|
|
100
|
+
* Wait for connection to a specific peer
|
|
101
|
+
*/
|
|
102
|
+
waitForConnectionToPeer(peerId: string, timeoutMs?: number): Promise<Connection>;
|
|
103
|
+
/**
|
|
104
|
+
* Wait for stream count to reach expected value
|
|
105
|
+
*/
|
|
106
|
+
waitForStreamCount(expectedCount: number, timeoutMs?: number): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Get summary of connection activity
|
|
109
|
+
*/
|
|
110
|
+
getSummary(): {
|
|
111
|
+
totalEvents: number;
|
|
112
|
+
connectionOpens: number;
|
|
113
|
+
connectionCloses: number;
|
|
114
|
+
peerConnects: number;
|
|
115
|
+
peerDisconnects: number;
|
|
116
|
+
currentConnections: number;
|
|
117
|
+
currentStreams: number;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a connection spy for a node
|
|
122
|
+
*/
|
|
123
|
+
export declare function createConnectionSpy(node: oNode | oNodeTool): ConnectionSpy;
|
|
124
|
+
//# sourceMappingURL=connection-spy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-spy.d.ts","sourceRoot":"","sources":["../../../test/helpers/connection-spy.ts"],"names":[],"mappings":"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"}
|