@olane/o-node 0.7.13-alpha.1 → 0.7.13
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/o-node-connection.manager.d.ts +10 -2
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +81 -48
- package/dist/src/o-node.d.ts +14 -0
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +43 -3
- package/dist/test/connection-management.spec.js +1 -1
- 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/package.json +7 -7
|
@@ -7,13 +7,21 @@ export declare class oNodeConnectionManager extends oConnectionManager {
|
|
|
7
7
|
protected p2pNode: Libp2p;
|
|
8
8
|
private defaultReadTimeoutMs?;
|
|
9
9
|
private defaultDrainTimeoutMs?;
|
|
10
|
-
private
|
|
11
|
-
private
|
|
10
|
+
private connectionsByTransportKey;
|
|
11
|
+
private pendingDialsByTransportKey;
|
|
12
12
|
constructor(config: oNodeConnectionManagerConfig);
|
|
13
13
|
/**
|
|
14
14
|
* Set up listeners to maintain connection cache state
|
|
15
15
|
*/
|
|
16
16
|
private setupConnectionListeners;
|
|
17
|
+
/**
|
|
18
|
+
* Build a stable cache key from the libp2p transports on an address.
|
|
19
|
+
*
|
|
20
|
+
* We intentionally key the cache by transports (multiaddrs) instead of peer IDs
|
|
21
|
+
* to avoid ambiguity when multiple peers may share a peer ID or when addresses
|
|
22
|
+
* change but the libp2p transports are the true dial targets.
|
|
23
|
+
*/
|
|
24
|
+
private getTransportKeyFromAddress;
|
|
17
25
|
/**
|
|
18
26
|
* Extract peer ID string from an address
|
|
19
27
|
* @param address - The address to extract peer ID from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAU,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAOhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IANzD,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"o-node-connection.manager.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAU,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kDAAkD,CAAC;AAEhG,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,qBAAa,sBAAuB,SAAQ,kBAAkB;IAOhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IANzD,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,yBAAyB,CAAsC;IACvE,OAAO,CAAC,0BAA0B,CAA+C;gBAE5D,MAAM,EAAE,4BAA4B;IAWzD;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAmBhC;;;;;;OAMG;IACH,OAAO,CAAC,0BAA0B;IAiBlC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAatB,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;YAsDR,WAAW;IAwBzB;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA8BlE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAoCpC;;;;OAIG;IACH,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI;IA+C/D;;;OAGG;IACH,aAAa,IAAI;QACf,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;QACrB,iBAAiB,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC9D;IAaD;;;OAGG;IACH,uBAAuB,IAAI,MAAM;CAalC"}
|
|
@@ -4,8 +4,8 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
4
4
|
constructor(config) {
|
|
5
5
|
super(config);
|
|
6
6
|
this.config = config;
|
|
7
|
-
this.
|
|
8
|
-
this.
|
|
7
|
+
this.connectionsByTransportKey = new Map();
|
|
8
|
+
this.pendingDialsByTransportKey = new Map();
|
|
9
9
|
this.p2pNode = config.p2pNode;
|
|
10
10
|
this.defaultReadTimeoutMs = config.defaultReadTimeoutMs;
|
|
11
11
|
this.defaultDrainTimeoutMs = config.defaultDrainTimeoutMs;
|
|
@@ -18,13 +18,41 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
18
18
|
*/
|
|
19
19
|
setupConnectionListeners() {
|
|
20
20
|
this.p2pNode.addEventListener('connection:close', (event) => {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const connection = event.detail;
|
|
22
|
+
if (!connection) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const [transportKey, cachedConnection] of this.connectionsByTransportKey.entries()) {
|
|
26
|
+
if (cachedConnection === connection) {
|
|
27
|
+
this.logger.debug('Connection closed, removing from cache for transport key:', transportKey);
|
|
28
|
+
this.connectionsByTransportKey.delete(transportKey);
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
});
|
|
27
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a stable cache key from the libp2p transports on an address.
|
|
35
|
+
*
|
|
36
|
+
* We intentionally key the cache by transports (multiaddrs) instead of peer IDs
|
|
37
|
+
* to avoid ambiguity when multiple peers may share a peer ID or when addresses
|
|
38
|
+
* change but the libp2p transports are the true dial targets.
|
|
39
|
+
*/
|
|
40
|
+
getTransportKeyFromAddress(address) {
|
|
41
|
+
try {
|
|
42
|
+
const nodeAddress = address;
|
|
43
|
+
const transports = nodeAddress.libp2pTransports;
|
|
44
|
+
if (!transports?.length) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Sort to ensure deterministic keys regardless of transport ordering.
|
|
48
|
+
const parts = transports.map((t) => t.toString()).sort();
|
|
49
|
+
return parts.join('|');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.logger.debug('Error extracting transport key from address:', error);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
28
56
|
/**
|
|
29
57
|
* Extract peer ID string from an address
|
|
30
58
|
* @param address - The address to extract peer ID from
|
|
@@ -44,56 +72,56 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
44
72
|
}
|
|
45
73
|
}
|
|
46
74
|
async getOrCreateConnection(nextHopAddress, address) {
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
throw new Error(`Unable to extract
|
|
75
|
+
// Build a transport-based cache key from the next hop address
|
|
76
|
+
const transportKey = this.getTransportKeyFromAddress(nextHopAddress);
|
|
77
|
+
if (!transportKey) {
|
|
78
|
+
throw new Error(`Unable to extract libp2p transports from address: ${nextHopAddress.toString()}`);
|
|
51
79
|
}
|
|
52
|
-
// Check if we have a cached connection by
|
|
53
|
-
const cachedConnection = this.
|
|
80
|
+
// Check if we have a cached connection by transport key
|
|
81
|
+
const cachedConnection = this.connectionsByTransportKey.get(transportKey);
|
|
54
82
|
if (cachedConnection && cachedConnection.status === 'open') {
|
|
55
|
-
this.logger.debug('Reusing cached connection for
|
|
83
|
+
this.logger.debug('Reusing cached connection for transports:', transportKey);
|
|
56
84
|
return cachedConnection;
|
|
57
85
|
}
|
|
58
86
|
// Clean up stale connection if it exists but is not open
|
|
59
87
|
if (cachedConnection && cachedConnection.status !== 'open') {
|
|
60
|
-
this.logger.debug('Removing stale connection for
|
|
61
|
-
this.
|
|
88
|
+
this.logger.debug('Removing stale connection for transports:', transportKey);
|
|
89
|
+
this.connectionsByTransportKey.delete(transportKey);
|
|
62
90
|
}
|
|
63
|
-
// Check if libp2p has an active connection
|
|
91
|
+
// Check if libp2p has an active connection for this address
|
|
64
92
|
const libp2pConnection = this.getCachedLibp2pConnection(nextHopAddress);
|
|
65
93
|
if (libp2pConnection && libp2pConnection.status === 'open') {
|
|
66
|
-
this.logger.debug('Caching existing libp2p connection for
|
|
67
|
-
this.
|
|
94
|
+
this.logger.debug('Caching existing libp2p connection for transports:', transportKey);
|
|
95
|
+
this.connectionsByTransportKey.set(transportKey, libp2pConnection);
|
|
68
96
|
return libp2pConnection;
|
|
69
97
|
}
|
|
70
|
-
// Check if dial is already in progress for this
|
|
71
|
-
const pendingDial = this.
|
|
98
|
+
// Check if dial is already in progress for this transport key
|
|
99
|
+
const pendingDial = this.pendingDialsByTransportKey.get(transportKey);
|
|
72
100
|
if (pendingDial) {
|
|
73
|
-
this.logger.debug('Awaiting existing dial for
|
|
101
|
+
this.logger.debug('Awaiting existing dial for transports:', transportKey);
|
|
74
102
|
return pendingDial;
|
|
75
103
|
}
|
|
76
|
-
// Start new dial and cache the promise by
|
|
77
|
-
const dialPromise = this.performDial(nextHopAddress,
|
|
78
|
-
this.
|
|
104
|
+
// Start new dial and cache the promise by transport key
|
|
105
|
+
const dialPromise = this.performDial(nextHopAddress, transportKey);
|
|
106
|
+
this.pendingDialsByTransportKey.set(transportKey, dialPromise);
|
|
79
107
|
try {
|
|
80
108
|
const connection = await dialPromise;
|
|
81
|
-
// Cache the established connection by
|
|
82
|
-
this.
|
|
109
|
+
// Cache the established connection by transport key
|
|
110
|
+
this.connectionsByTransportKey.set(transportKey, connection);
|
|
83
111
|
return connection;
|
|
84
112
|
}
|
|
85
113
|
finally {
|
|
86
|
-
this.
|
|
114
|
+
this.pendingDialsByTransportKey.delete(transportKey);
|
|
87
115
|
}
|
|
88
116
|
}
|
|
89
|
-
async performDial(nextHopAddress,
|
|
117
|
+
async performDial(nextHopAddress, transportKey) {
|
|
90
118
|
this.logger.debug('Dialing new connection', {
|
|
91
119
|
address: nextHopAddress.value,
|
|
92
|
-
|
|
120
|
+
transportKey,
|
|
93
121
|
});
|
|
94
122
|
const connection = await this.p2pNode.dial(nextHopAddress.libp2pTransports.map((ma) => ma.toMultiaddr()));
|
|
95
123
|
this.logger.debug('Successfully dialed connection', {
|
|
96
|
-
|
|
124
|
+
transportKey,
|
|
97
125
|
status: connection.status,
|
|
98
126
|
remotePeer: connection.remotePeer?.toString(),
|
|
99
127
|
});
|
|
@@ -128,16 +156,17 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
128
156
|
*/
|
|
129
157
|
isCached(address) {
|
|
130
158
|
try {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
159
|
+
const transportKey = this.getTransportKeyFromAddress(address);
|
|
160
|
+
if (!transportKey) {
|
|
133
161
|
return false;
|
|
134
162
|
}
|
|
135
|
-
// Check our
|
|
136
|
-
const cachedConnection = this.
|
|
163
|
+
// Check our transport-based cache first
|
|
164
|
+
const cachedConnection = this.connectionsByTransportKey.get(transportKey);
|
|
137
165
|
if (cachedConnection?.status === 'open') {
|
|
138
166
|
return true;
|
|
139
167
|
}
|
|
140
168
|
// Fall back to checking libp2p's connections
|
|
169
|
+
const peerId = this.getPeerIdFromAddress(address);
|
|
141
170
|
// the following works since the peer id param is not really required: https://github.com/libp2p/js-libp2p/blob/0bbf5021b53938b2bffcffca6c13c479a95c2a60/packages/libp2p/src/connection-manager/index.ts#L508
|
|
142
171
|
const connections = this.p2pNode.getConnections(peerId); // ignore since converting to a proper peer id breaks the browser implementation
|
|
143
172
|
// Check if we have at least one open connection
|
|
@@ -146,7 +175,7 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
146
175
|
if (hasOpenConnection) {
|
|
147
176
|
const openConnection = connections.find((conn) => conn.status === 'open');
|
|
148
177
|
if (openConnection) {
|
|
149
|
-
this.
|
|
178
|
+
this.connectionsByTransportKey.set(transportKey, openConnection);
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
return hasOpenConnection;
|
|
@@ -163,28 +192,32 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
163
192
|
*/
|
|
164
193
|
getCachedLibp2pConnection(address) {
|
|
165
194
|
try {
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
195
|
+
const transportKey = this.getTransportKeyFromAddress(address);
|
|
196
|
+
if (!transportKey) {
|
|
168
197
|
return null;
|
|
169
198
|
}
|
|
170
|
-
// Check
|
|
171
|
-
const cachedConnection = this.
|
|
199
|
+
// Check transport-based cache first
|
|
200
|
+
const cachedConnection = this.connectionsByTransportKey.get(transportKey);
|
|
172
201
|
if (cachedConnection?.status === 'open') {
|
|
173
202
|
return cachedConnection;
|
|
174
203
|
}
|
|
204
|
+
const peerId = this.getPeerIdFromAddress(address);
|
|
205
|
+
if (!peerId) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
175
208
|
// Query libp2p for connections to this peer
|
|
176
209
|
const connections = this.p2pNode.getConnections();
|
|
177
210
|
const filteredConnections = connections.filter((conn) => conn.remotePeer?.toString() === peerId);
|
|
178
211
|
// Find the first open connection
|
|
179
212
|
const openConnection = filteredConnections.find((conn) => conn.status === 'open');
|
|
180
|
-
// If we found an open connection in libp2p, cache it by
|
|
213
|
+
// If we found an open connection in libp2p, cache it by transport key
|
|
181
214
|
if (openConnection) {
|
|
182
|
-
this.
|
|
215
|
+
this.connectionsByTransportKey.set(transportKey, openConnection);
|
|
183
216
|
return openConnection;
|
|
184
217
|
}
|
|
185
218
|
// Clean up stale cache entry if connection is no longer open
|
|
186
219
|
if (cachedConnection) {
|
|
187
|
-
this.
|
|
220
|
+
this.connectionsByTransportKey.delete(transportKey);
|
|
188
221
|
}
|
|
189
222
|
return null;
|
|
190
223
|
}
|
|
@@ -199,10 +232,10 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
199
232
|
*/
|
|
200
233
|
getCacheStats() {
|
|
201
234
|
return {
|
|
202
|
-
cachedConnections: this.
|
|
203
|
-
pendingDials: this.
|
|
204
|
-
connectionsByPeer: Array.from(this.
|
|
205
|
-
peerId,
|
|
235
|
+
cachedConnections: this.connectionsByTransportKey.size,
|
|
236
|
+
pendingDials: this.pendingDialsByTransportKey.size,
|
|
237
|
+
connectionsByPeer: Array.from(this.connectionsByTransportKey.values()).map((conn) => ({
|
|
238
|
+
peerId: conn.remotePeer?.toString() ?? 'unknown',
|
|
206
239
|
status: conn.status,
|
|
207
240
|
})),
|
|
208
241
|
};
|
|
@@ -213,9 +246,9 @@ export class oNodeConnectionManager extends oConnectionManager {
|
|
|
213
246
|
*/
|
|
214
247
|
cleanupStaleConnections() {
|
|
215
248
|
let removed = 0;
|
|
216
|
-
for (const [
|
|
249
|
+
for (const [transportKey, connection] of this.connectionsByTransportKey.entries()) {
|
|
217
250
|
if (connection.status !== 'open') {
|
|
218
|
-
this.
|
|
251
|
+
this.connectionsByTransportKey.delete(transportKey);
|
|
219
252
|
removed++;
|
|
220
253
|
}
|
|
221
254
|
}
|
package/dist/src/o-node.d.ts
CHANGED
|
@@ -49,6 +49,20 @@ export declare class oNode extends oToolBase {
|
|
|
49
49
|
initConnectionManager(): Promise<void>;
|
|
50
50
|
hookInitializeFinished(): Promise<void>;
|
|
51
51
|
hookStartFinished(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Validates that if a leader address is defined, it has associated transports.
|
|
54
|
+
* This is critical for non-leader nodes to be able to connect to their leader.
|
|
55
|
+
* @throws Error if leader is defined but has no transports
|
|
56
|
+
*/
|
|
57
|
+
private validateLeaderTransports;
|
|
58
|
+
/**
|
|
59
|
+
* oNode-specific validation that runs before core start() logic.
|
|
60
|
+
*
|
|
61
|
+
* This ensures that leader transport configuration errors are
|
|
62
|
+
* surfaced quickly and before any libp2p nodes or other heavy
|
|
63
|
+
* resources are created.
|
|
64
|
+
*/
|
|
65
|
+
protected validate(): Promise<void>;
|
|
52
66
|
initialize(): Promise<void>;
|
|
53
67
|
/**
|
|
54
68
|
* Override use() to wrap leader/registry requests with retry logic
|
package/dist/src/o-node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.d.ts","sourceRoot":"","sources":["../../src/o-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,MAAM,EACN,YAAY,EAEb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAKL,QAAQ,EAER,oBAAoB,EAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AAEnF,OAAO,EAAmB,SAAS,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAE5E,qBAAa,KAAM,SAAQ,SAAS;IAC3B,MAAM,EAAG,MAAM,CAAC;IAChB,OAAO,EAAG,MAAM,CAAC;IACjB,OAAO,EAAG,YAAY,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB,iBAAiB,EAAG,sBAAsB,CAAC;IAC3C,gBAAgB,EAAG,qBAAqB,CAAC;IACzC,0BAA0B,CAAC,EAAE,2BAA2B,CAAC;IAChE,SAAS,CAAC,mBAAmB,CAAC,EAAE,oBAAoB,CAAC;IACrD,SAAS,CAAC,WAAW,EAAE,OAAO,CAAS;gBAE3B,MAAM,EAAE,WAAW;IAK/B,IAAI,MAAM,IAAI,YAAY,GAAG,IAAI,CAEhC;IAED,IAAI,aAAa,IAAI,YAAY,CAKhC;IAED,IAAI,YAAY,IAAI,MAAM,GAAG,IAAI,CAOhC;IAED,mBAAmB,IAAI,GAAG,EAAE;IAItB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IASvC,SAAS,CAAC,yBAAyB,IAAI,oBAAoB;IAQ3D,IAAI,aAAa,IAAI,YAAY,CAEhC;IAED,IAAI,gBAAgB,IAAI,cAAc,EAAE,CAEvC;IAED,IAAI,UAAU,IAAI,cAAc,EAAE,CAIjC;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"o-node.d.ts","sourceRoot":"","sources":["../../src/o-node.ts"],"names":[],"mappings":"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"}
|
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) => {
|
|
@@ -367,6 +371,42 @@ export class oNode extends oToolBase {
|
|
|
367
371
|
this.logger.info(`Connection heartbeat config: leader=${this.connectionHeartbeatManager.getConfig().checkLeader}, ` +
|
|
368
372
|
`parent=${this.connectionHeartbeatManager.getConfig().checkParent}`);
|
|
369
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
|
+
}
|
|
370
410
|
async initialize() {
|
|
371
411
|
this.logger.debug('Initializing node...');
|
|
372
412
|
if (this.state !== NodeState.STOPPED && this.state !== NodeState.STARTING) {
|
|
@@ -145,7 +145,7 @@ describe('Connection Management', () => {
|
|
|
145
145
|
method: 'echo',
|
|
146
146
|
params: { message: 'test' },
|
|
147
147
|
}).catch((err) => {
|
|
148
|
-
expect(err.
|
|
148
|
+
expect(err.code).to.be.equal('ECONNREFUSED');
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
151
|
it('should verify connection is open before use', async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader-transport-validation.spec.d.ts","sourceRoot":"","sources":["../../test/leader-transport-validation.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { TestEnvironment } from './helpers/index.js';
|
|
3
|
+
import { oNodeTool } from '../src/o-node.tool.js';
|
|
4
|
+
import { oNodeAddress, oNodeTransport } from '../src/index.js';
|
|
5
|
+
describe('Leader Transport Validation', () => {
|
|
6
|
+
const env = new TestEnvironment();
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await env.cleanup();
|
|
9
|
+
});
|
|
10
|
+
describe('Node with leader configuration', () => {
|
|
11
|
+
it('should fail to start when leader has no transports', async () => {
|
|
12
|
+
const leaderAddress = new oNodeAddress('o://leader', []); // Empty transports
|
|
13
|
+
const node = new oNodeTool({
|
|
14
|
+
address: new oNodeAddress('o://child'),
|
|
15
|
+
leader: leaderAddress,
|
|
16
|
+
parent: null,
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
await node.start();
|
|
20
|
+
expect.fail('Expected node.start() to throw an error');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
expect(error.message).to.include('Leader address is defined');
|
|
24
|
+
expect(error.message).to.include('but has no transports');
|
|
25
|
+
expect(error.message).to.include('o://leader');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
it('should fail to start when leader has undefined transports', async () => {
|
|
29
|
+
const leaderAddress = new oNodeAddress('o://leader');
|
|
30
|
+
// Explicitly set transports to undefined
|
|
31
|
+
leaderAddress.transports = undefined;
|
|
32
|
+
const node = new oNodeTool({
|
|
33
|
+
address: new oNodeAddress('o://child'),
|
|
34
|
+
leader: leaderAddress,
|
|
35
|
+
parent: null,
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await node.start();
|
|
39
|
+
expect.fail('Expected node.start() to throw an error');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
expect(error.message).to.include('Leader address is defined');
|
|
43
|
+
expect(error.message).to.include('but has no transports');
|
|
44
|
+
expect(error.message).to.include('o://leader');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it('should fail to start when leader has null transports', async () => {
|
|
48
|
+
const leaderAddress = new oNodeAddress('o://leader');
|
|
49
|
+
// Explicitly set transports to null
|
|
50
|
+
leaderAddress.transports = null;
|
|
51
|
+
const node = new oNodeTool({
|
|
52
|
+
address: new oNodeAddress('o://child'),
|
|
53
|
+
leader: leaderAddress,
|
|
54
|
+
parent: null,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
await node.start();
|
|
58
|
+
expect.fail('Expected node.start() to throw an error');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
expect(error.message).to.include('Leader address is defined');
|
|
62
|
+
expect(error.message).to.include('but has no transports');
|
|
63
|
+
expect(error.message).to.include('o://leader');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it('should start successfully when leader has valid transports', async () => {
|
|
67
|
+
const leaderAddress = new oNodeAddress('o://leader', [
|
|
68
|
+
new oNodeTransport('/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWTest123'),
|
|
69
|
+
]);
|
|
70
|
+
const node = new oNodeTool({
|
|
71
|
+
address: new oNodeAddress('o://child'),
|
|
72
|
+
leader: leaderAddress,
|
|
73
|
+
parent: null,
|
|
74
|
+
});
|
|
75
|
+
// Should not throw
|
|
76
|
+
await node.start();
|
|
77
|
+
expect(node.state).to.not.equal('STOPPED');
|
|
78
|
+
await node.stop();
|
|
79
|
+
});
|
|
80
|
+
it('should start successfully when leader has multiple transports', async () => {
|
|
81
|
+
const leaderAddress = new oNodeAddress('o://leader', [
|
|
82
|
+
new oNodeTransport('/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWTest123'),
|
|
83
|
+
new oNodeTransport('/ip4/192.168.1.1/tcp/4001/p2p/12D3KooWTest123'),
|
|
84
|
+
]);
|
|
85
|
+
const node = new oNodeTool({
|
|
86
|
+
address: new oNodeAddress('o://child'),
|
|
87
|
+
leader: leaderAddress,
|
|
88
|
+
parent: null,
|
|
89
|
+
});
|
|
90
|
+
// Should not throw
|
|
91
|
+
await node.start();
|
|
92
|
+
expect(node.state).to.not.equal('STOPPED');
|
|
93
|
+
await node.stop();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Node without leader configuration', () => {
|
|
97
|
+
it('should start successfully when leader is null', async () => {
|
|
98
|
+
const node = new oNodeTool({
|
|
99
|
+
address: new oNodeAddress('o://standalone'),
|
|
100
|
+
leader: null,
|
|
101
|
+
parent: null,
|
|
102
|
+
});
|
|
103
|
+
// Should not throw
|
|
104
|
+
await node.start();
|
|
105
|
+
expect(node.state).to.not.equal('STOPPED');
|
|
106
|
+
await node.stop();
|
|
107
|
+
});
|
|
108
|
+
it('should start successfully when leader is undefined', async () => {
|
|
109
|
+
const node = new oNodeTool({
|
|
110
|
+
address: new oNodeAddress('o://standalone'),
|
|
111
|
+
leader: undefined, // Explicitly undefined
|
|
112
|
+
parent: null,
|
|
113
|
+
});
|
|
114
|
+
// Should not throw
|
|
115
|
+
await node.start();
|
|
116
|
+
expect(node.state).to.not.equal('STOPPED');
|
|
117
|
+
await node.stop();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('Leader node (self-referential)', () => {
|
|
121
|
+
it('should start successfully as a leader without transports validation', async () => {
|
|
122
|
+
const node = new oNodeTool({
|
|
123
|
+
address: new oNodeAddress('o://leader'),
|
|
124
|
+
leader: null, // Leader nodes don't need a leader reference
|
|
125
|
+
parent: null,
|
|
126
|
+
type: 'LEADER', // Set as leader node
|
|
127
|
+
});
|
|
128
|
+
// Should not throw - leader nodes don't need leader transports
|
|
129
|
+
await node.start();
|
|
130
|
+
expect(node.state).to.not.equal('STOPPED');
|
|
131
|
+
await node.stop();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('Error message content', () => {
|
|
135
|
+
it('should provide helpful error message with leader address', async () => {
|
|
136
|
+
const leaderAddress = new oNodeAddress('o://my-custom-leader', []);
|
|
137
|
+
const node = new oNodeTool({
|
|
138
|
+
address: new oNodeAddress('o://child'),
|
|
139
|
+
leader: leaderAddress,
|
|
140
|
+
parent: null,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
await node.start();
|
|
144
|
+
expect.fail('Expected node.start() to throw an error');
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
expect(error.message).to.include('o://my-custom-leader');
|
|
148
|
+
expect(error.message).to.include('Non-leader nodes require leader transports');
|
|
149
|
+
expect(error.message).to.include('Please provide transports');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('Validation timing', () => {
|
|
154
|
+
it('should fail before p2pNode creation (fast fail)', async () => {
|
|
155
|
+
const leaderAddress = new oNodeAddress('o://leader', []);
|
|
156
|
+
const node = new oNodeTool({
|
|
157
|
+
address: new oNodeAddress('o://child'),
|
|
158
|
+
leader: leaderAddress,
|
|
159
|
+
parent: null,
|
|
160
|
+
});
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
try {
|
|
163
|
+
await node.start();
|
|
164
|
+
expect.fail('Expected node.start() to throw an error');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const duration = Date.now() - startTime;
|
|
168
|
+
// Validation should happen very quickly (< 100ms)
|
|
169
|
+
// If it takes longer, it means we're doing expensive operations before validation
|
|
170
|
+
expect(duration).to.be.lessThan(100);
|
|
171
|
+
expect(error.message).to.include('Leader address is defined');
|
|
172
|
+
// Verify p2pNode was never created
|
|
173
|
+
expect(node.p2pNode).to.be.undefined;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olane/o-node",
|
|
3
|
-
"version": "0.7.13
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/eslintrc": "^3.3.1",
|
|
42
42
|
"@eslint/js": "^9.29.0",
|
|
43
|
-
"@olane/o-test": "0.7.13
|
|
43
|
+
"@olane/o-test": "0.7.13",
|
|
44
44
|
"@tsconfig/node20": "^20.1.6",
|
|
45
45
|
"@types/jest": "^30.0.0",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"typescript": "5.4.5"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@olane/o-config": "0.7.13
|
|
63
|
-
"@olane/o-core": "0.7.13
|
|
64
|
-
"@olane/o-protocol": "0.7.13
|
|
65
|
-
"@olane/o-tool": "0.7.13
|
|
62
|
+
"@olane/o-config": "0.7.13",
|
|
63
|
+
"@olane/o-core": "0.7.13",
|
|
64
|
+
"@olane/o-protocol": "0.7.13",
|
|
65
|
+
"@olane/o-tool": "0.7.13",
|
|
66
66
|
"debug": "^4.4.1",
|
|
67
67
|
"dotenv": "^16.5.0"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "e86440c6502a9dff5569a99a8b0d22de8800c9b4"
|
|
70
70
|
}
|