@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.
@@ -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 connectionsByPeerId;
11
- private pendingDialsByPeerId;
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,mBAAmB,CAAsC;IACjE,OAAO,CAAC,oBAAoB,CAA+C;gBAEtD,MAAM,EAAE,4BAA4B;IAWzD;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAUhC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAatB,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;YAmDR,WAAW;IAwBzB;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA8BlE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAmCpC;;;;OAIG;IACH,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI;IA0C/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"}
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.connectionsByPeerId = new Map();
8
- this.pendingDialsByPeerId = new Map();
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 peerId = event.detail?.remotePeer?.toString();
22
- if (peerId && this.connectionsByPeerId.has(peerId)) {
23
- this.logger.debug('Connection closed, removing from cache:', peerId);
24
- this.connectionsByPeerId.delete(peerId);
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
- // Extract peer ID as the cache key
48
- const peerId = this.getPeerIdFromAddress(nextHopAddress);
49
- if (!peerId) {
50
- throw new Error(`Unable to extract peer ID from address: ${nextHopAddress.toString()}`);
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 peer ID
53
- const cachedConnection = this.connectionsByPeerId.get(peerId);
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 peer:', peerId);
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 peer:', peerId);
61
- this.connectionsByPeerId.delete(peerId);
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 to this peer
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 peer:', peerId);
67
- this.connectionsByPeerId.set(peerId, libp2pConnection);
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 peer
71
- const pendingDial = this.pendingDialsByPeerId.get(peerId);
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 peer:', peerId);
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 peer ID
77
- const dialPromise = this.performDial(nextHopAddress, peerId);
78
- this.pendingDialsByPeerId.set(peerId, dialPromise);
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 peer ID
82
- this.connectionsByPeerId.set(peerId, connection);
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.pendingDialsByPeerId.delete(peerId);
114
+ this.pendingDialsByTransportKey.delete(transportKey);
87
115
  }
88
116
  }
89
- async performDial(nextHopAddress, peerId) {
117
+ async performDial(nextHopAddress, transportKey) {
90
118
  this.logger.debug('Dialing new connection', {
91
119
  address: nextHopAddress.value,
92
- peerId,
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
- peerId,
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 peerId = this.getPeerIdFromAddress(address);
132
- if (!peerId) {
159
+ const transportKey = this.getTransportKeyFromAddress(address);
160
+ if (!transportKey) {
133
161
  return false;
134
162
  }
135
- // Check our peer ID-based cache first
136
- const cachedConnection = this.connectionsByPeerId.get(peerId);
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.connectionsByPeerId.set(peerId, openConnection);
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 peerId = this.getPeerIdFromAddress(address);
167
- if (!peerId) {
195
+ const transportKey = this.getTransportKeyFromAddress(address);
196
+ if (!transportKey) {
168
197
  return null;
169
198
  }
170
- // Check peer ID-based cache first
171
- const cachedConnection = this.connectionsByPeerId.get(peerId);
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 peer ID
213
+ // If we found an open connection in libp2p, cache it by transport key
181
214
  if (openConnection) {
182
- this.connectionsByPeerId.set(peerId, openConnection);
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.connectionsByPeerId.delete(peerId);
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.connectionsByPeerId.size,
203
- pendingDials: this.pendingDialsByPeerId.size,
204
- connectionsByPeer: Array.from(this.connectionsByPeerId.entries()).map(([peerId, conn]) => ({
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 [peerId, connection] of this.connectionsByPeerId.entries()) {
249
+ for (const [transportKey, connection] of this.connectionsByTransportKey.entries()) {
217
250
  if (connection.status !== 'open') {
218
- this.connectionsByPeerId.delete(peerId);
251
+ this.connectionsByTransportKey.delete(transportKey);
219
252
  removed++;
220
253
  }
221
254
  }
@@ -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
@@ -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;IAwD3B,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCrD,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IA4C/B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB/B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B/B,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM;IAItC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,mBAAmB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;IAG1D;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;cA8HxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAMvC,OAAO,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IAsBhE,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtC,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAEvC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBlC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CjC;;OAEG;IAiBG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB/B;;;OAGG;IACH,SAAS,CAAC,UAAU,IAAI,IAAI;IA+B5B,UAAU,IAAI,YAAY,EAAE;IAI5B,UAAU,IAAI,YAAY,EAAE;IAI5B,WAAW,IAAI,YAAY,EAAE;IAI7B,WAAW,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAI7C;;;OAGG;IACH,cAAc,IAAI,MAAM;IAUxB;;;OAGG;IACG,wBAAwB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAwEhE"}
1
+ {"version":3,"file":"o-node.d.ts","sourceRoot":"","sources":["../../src/o-node.ts"],"names":[],"mappings":"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"}
@@ -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) => {
@@ -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.message).to.be.equal('Unable to extract peer ID from address: o://nonexistent');
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=leader-transport-validation.spec.d.ts.map
@@ -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-alpha.1",
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-alpha.1",
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-alpha.1",
63
- "@olane/o-core": "0.7.13-alpha.1",
64
- "@olane/o-protocol": "0.7.13-alpha.1",
65
- "@olane/o-tool": "0.7.13-alpha.1",
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": "7abba2239f85eb07a87401528b6df96e72449fe0"
69
+ "gitHead": "e86440c6502a9dff5569a99a8b0d22de8800c9b4"
70
70
  }