@olane/o-node 0.7.44 → 0.7.46

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.
@@ -13,6 +13,10 @@ export declare class oNodeConnection extends oConnection {
13
13
  constructor(config: oNodeConnectionConfig);
14
14
  get remotePeerId(): import("@olane/o-config").PeerId;
15
15
  get remoteAddr(): import("@olane/o-config").Multiaddr;
16
+ /**
17
+ * Get the connection configuration for compatibility checking.
18
+ */
19
+ get connectionConfig(): oNodeConnectionConfig;
16
20
  supportsAddress(address: oNodeAddress): boolean;
17
21
  getOrCreateStream(): Promise<oNodeConnectionStream>;
18
22
  createStream(): Promise<oNodeConnectionStream>;
@@ -1 +1 @@
1
- {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAU,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,qBAAa,eAAgB,SAAQ,WAAW;IAKlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAJrD,aAAa,EAAE,UAAU,CAAC;IACjC,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,WAAW,EAAE,iBAAiB,CAAC;gBAEV,MAAM,EAAE,qBAAqB;IAO5D,IAAI,YAAY,qCAEf;IAED,IAAI,UAAU,wCAEb;IAED,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAMzC,iBAAiB,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAanD,YAAY,IAAI,OAAO,CAAC,qBAAqB,CAAC;IA6BpD,IAAI,OAAO,IAAI,qBAAqB,EAAE,CAcrC;IAEK,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IA6C/C,YAAY,CAAC,MAAM,EAAE,qBAAqB;IAI1C,KAAK,CAAC,KAAK,EAAE,KAAK;IAMlB,KAAK;CAKZ"}
1
+ {"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAU,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,qBAAa,eAAgB,SAAQ,WAAW;IAKlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAJrD,aAAa,EAAE,UAAU,CAAC;IACjC,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,WAAW,EAAE,iBAAiB,CAAC;gBAEV,MAAM,EAAE,qBAAqB;IAQ5D,IAAI,YAAY,qCAEf;IAED,IAAI,UAAU,wCAEb;IAED;;OAEG;IACH,IAAI,gBAAgB,IAAI,qBAAqB,CAE5C;IAED,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAMzC,iBAAiB,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAkBnD,YAAY,IAAI,OAAO,CAAC,qBAAqB,CAAC;IA6BpD,IAAI,OAAO,IAAI,qBAAqB,EAAE,CAcrC;IAEK,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IA6C/C,YAAY,CAAC,MAAM,EAAE,qBAAqB;IAI1C,KAAK,CAAC,KAAK,EAAE,KAAK;IAMlB,KAAK;CAKZ"}
@@ -8,6 +8,7 @@ export class oNodeConnection extends oConnection {
8
8
  this.p2pConnection = config.p2pConnection;
9
9
  this.streamHandler = new StreamHandler(this.logger);
10
10
  this.reusePolicy = config.reusePolicy ?? 'none';
11
+ console.log('oNodeConnection constructor', this.reusePolicy);
11
12
  }
12
13
  get remotePeerId() {
13
14
  return this.p2pConnection.remotePeer;
@@ -15,16 +16,27 @@ export class oNodeConnection extends oConnection {
15
16
  get remoteAddr() {
16
17
  return this.p2pConnection.remoteAddr;
17
18
  }
19
+ /**
20
+ * Get the connection configuration for compatibility checking.
21
+ */
22
+ get connectionConfig() {
23
+ return this.config;
24
+ }
18
25
  supportsAddress(address) {
19
26
  return address.libp2pTransports.some((transport) => {
20
27
  return transport.toString() === this.remoteAddr.toString();
21
28
  });
22
29
  }
23
30
  async getOrCreateStream() {
24
- if (this.reusePolicy === 'reuse' && this.streams.length > 0) {
25
- this.logger.debug('Returning reuse stream: ', this.streams[0].p2pStream.protocol);
31
+ console.log('getOrCreateStream', this.reusePolicy);
32
+ if (this.reusePolicy === 'reuse') {
33
+ this.logger.debug('Reusing stream...');
26
34
  // search for streams that allow re-use
27
- return this.streams[0];
35
+ if (this.streams.length > 0) {
36
+ this.logger.debug('Returning reuse stream: ', this.streams[0].p2pStream.protocol);
37
+ // search for streams that allow re-use
38
+ return this.streams[0];
39
+ }
28
40
  }
29
41
  return this.createStream();
30
42
  }
@@ -9,6 +9,8 @@ export declare class oNodeConnectionManager extends oConnectionManager {
9
9
  private defaultDrainTimeoutMs?;
10
10
  private connectionsByAddress;
11
11
  private pendingDialsByAddress;
12
+ /** Cache of oNodeConnection instances keyed by p2p connection ID */
13
+ private nodeConnectionByConnectionId;
12
14
  constructor(config: oNodeConnectionManagerConfig);
13
15
  /**
14
16
  * Set up listeners to maintain connection cache state
@@ -27,6 +29,26 @@ export declare class oNodeConnectionManager extends oConnectionManager {
27
29
  * @returns The peer ID string or null if not found
28
30
  */
29
31
  private getPeerIdFromAddress;
32
+ /**
33
+ * Select the best connection from an array of connections.
34
+ * Prioritizes connections with active streams, then by direction based on context.
35
+ *
36
+ * @param connections - Array of connections to choose from
37
+ * @param reuseContext - If true, prefer inbound connections; otherwise prefer outbound
38
+ * @returns The best connection or null if none are suitable
39
+ */
40
+ private selectBestConnection;
41
+ /**
42
+ * Get a cached oNodeConnection for the given p2p connection if it exists and is valid.
43
+ * @param p2pConnection - The p2p connection to look up
44
+ * @returns A valid oNodeConnection or null if none found
45
+ */
46
+ private getCachedNodeConnection;
47
+ /**
48
+ * Cache an oNodeConnection by its p2p connection ID for potential reuse.
49
+ * @param conn - The oNodeConnection to cache
50
+ */
51
+ private cacheNodeConnection;
30
52
  getOrCreateConnection(nextHopAddress: oAddress, address: oAddress): Promise<Connection>;
31
53
  private performDial;
32
54
  answer(config: oConnectionConfig & {
@@ -34,7 +56,7 @@ export declare class oNodeConnectionManager extends oConnectionManager {
34
56
  reuse?: boolean;
35
57
  }): Promise<oNodeConnection>;
36
58
  /**
37
- * Connect to a given address, reusing libp2p connections when possible
59
+ * Connect to a given address, reusing oNodeConnection and libp2p connections when possible
38
60
  * @param config - Connection configuration
39
61
  * @returns The connection object
40
62
  */
@@ -56,11 +78,14 @@ export declare class oNodeConnectionManager extends oConnectionManager {
56
78
  * @returns Object containing cache statistics
57
79
  */
58
80
  getCacheStats(): {
59
- cachedConnections: number;
81
+ cachedAddresses: number;
82
+ totalCachedConnections: number;
60
83
  pendingDials: number;
84
+ cachedNodeConnections: number;
61
85
  connectionsByPeer: Array<{
62
86
  peerId: string;
63
87
  status: string;
88
+ addressKey: string;
64
89
  }>;
65
90
  };
66
91
  /**
@@ -68,5 +93,10 @@ export declare class oNodeConnectionManager extends oConnectionManager {
68
93
  * @returns Number of connections removed
69
94
  */
70
95
  cleanupStaleConnections(): number;
96
+ /**
97
+ * Clean up all stale oNodeConnections from cache
98
+ * @returns Number of oNodeConnections removed
99
+ */
100
+ cleanupStaleNodeConnections(): number;
71
101
  }
72
102
  //# sourceMappingURL=o-node-connection.manager.d.ts.map
@@ -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;IAQhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IAPzD,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,oBAAoB,CAAsC;IAClE,OAAO,CAAC,qBAAqB,CACjB;gBAES,MAAM,EAAE,4BAA4B;IAWzD;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAsBhC;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IASrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAatB,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;YA+DR,WAAW;IAwBnB,MAAM,CACV,MAAM,EAAE,iBAAiB,GAAG;QAAE,aAAa,EAAE,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GACzE,OAAO,CAAC,eAAe,CAAC;IAmC3B;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA8BlE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAwCpC;;;;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;CAgBlC"}
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;IAUhD,QAAQ,CAAC,MAAM,EAAE,4BAA4B;IATzD,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,oBAAoB,CAAwC;IACpE,OAAO,CAAC,qBAAqB,CAA+C;IAC5E,oEAAoE;IACpE,OAAO,CAAC,4BAA4B,CACxB;gBAES,MAAM,EAAE,4BAA4B;IAWzD;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAyChC;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IASrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IA8D5B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAc/B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAIrB,qBAAqB,CACzB,cAAc,EAAE,QAAQ,EACxB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,UAAU,CAAC;YAgFR,WAAW;IAwBnB,MAAM,CACV,MAAM,EAAE,iBAAiB,GAAG;QAAE,aAAa,EAAE,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GACzE,OAAO,CAAC,eAAe,CAAC;IA2C3B;;;;OAIG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6ClE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO;IAiDpC;;;;OAIG;IACH,yBAAyB,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI;IA6D/D;;;OAGG;IACH,aAAa,IAAI;QACf,eAAe,EAAE,MAAM,CAAC;QACxB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;QACrB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,iBAAiB,EAAE,KAAK,CAAC;YACvB,MAAM,EAAE,MAAM,CAAC;YACf,MAAM,EAAE,MAAM,CAAC;YACf,UAAU,EAAE,MAAM,CAAC;SACpB,CAAC,CAAC;KACJ;IA6BD;;;OAGG;IACH,uBAAuB,IAAI,MAAM;IA6BjC;;;OAGG;IACH,2BAA2B,IAAI,MAAM;CAgBtC"}
@@ -6,6 +6,8 @@ export class oNodeConnectionManager extends oConnectionManager {
6
6
  this.config = config;
7
7
  this.connectionsByAddress = new Map();
8
8
  this.pendingDialsByAddress = new Map();
9
+ /** Cache of oNodeConnection instances keyed by p2p connection ID */
10
+ this.nodeConnectionByConnectionId = new Map();
9
11
  this.p2pNode = config.p2pNode;
10
12
  this.defaultReadTimeoutMs = config.defaultReadTimeoutMs;
11
13
  this.defaultDrainTimeoutMs = config.defaultDrainTimeoutMs;
@@ -22,11 +24,31 @@ export class oNodeConnectionManager extends oConnectionManager {
22
24
  if (!connection) {
23
25
  return;
24
26
  }
25
- for (const [addressKey, cachedConnection,] of this.connectionsByAddress.entries()) {
26
- if (cachedConnection === connection) {
27
- this.logger.debug('Connection closed, removing from cache for address:', addressKey);
28
- this.connectionsByAddress.delete(addressKey);
29
- }
27
+ // Clean up libp2p connection cache
28
+ // for (const [
29
+ // addressKey,
30
+ // cachedConnections,
31
+ // ] of this.connectionsByAddress.entries()) {
32
+ // const index = cachedConnections.indexOf(connection);
33
+ // if (index !== -1) {
34
+ // this.logger.debug(
35
+ // 'Connection closed, removing from cache for address:',
36
+ // addressKey,
37
+ // );
38
+ // cachedConnections.splice(index, 1);
39
+ // // Remove the address key entirely if no connections remain
40
+ // if (cachedConnections.length === 0) {
41
+ // this.connectionsByAddress.delete(addressKey);
42
+ // } else {
43
+ // this.connectionsByAddress.set(addressKey, cachedConnections);
44
+ // }
45
+ // }
46
+ // }
47
+ // Clean up oNodeConnection cache by connection ID
48
+ const connectionId = connection.id;
49
+ if (this.nodeConnectionByConnectionId.has(connectionId)) {
50
+ this.logger.debug('Connection closed, removing oNodeConnection for connection ID:', connectionId);
51
+ this.nodeConnectionByConnectionId.delete(connectionId);
30
52
  }
31
53
  });
32
54
  }
@@ -63,6 +85,81 @@ export class oNodeConnectionManager extends oConnectionManager {
63
85
  return null;
64
86
  }
65
87
  }
88
+ /**
89
+ * Select the best connection from an array of connections.
90
+ * Prioritizes connections with active streams, then by direction based on context.
91
+ *
92
+ * @param connections - Array of connections to choose from
93
+ * @param reuseContext - If true, prefer inbound connections; otherwise prefer outbound
94
+ * @returns The best connection or null if none are suitable
95
+ */
96
+ selectBestConnection(connections, reuseContext = false) {
97
+ // Filter to only open connections
98
+ const openConnections = connections.filter((c) => c.status === 'open');
99
+ if (openConnections.length === 0) {
100
+ return null;
101
+ }
102
+ // Priority 1: Connections with active o-protocol streams
103
+ const connectionsWithStreams = openConnections
104
+ .map((conn) => ({
105
+ conn,
106
+ activeStreamCount: conn.streams.filter((s) => s.protocol.includes('/o/') &&
107
+ s.status === 'open' &&
108
+ s.writeStatus === 'writable' &&
109
+ s.remoteReadStatus === 'readable').length,
110
+ }))
111
+ .filter((item) => item.activeStreamCount > 0)
112
+ .sort((a, b) => b.activeStreamCount - a.activeStreamCount);
113
+ if (connectionsWithStreams.length > 0) {
114
+ this.logger.debug('Selected connection with active streams', {
115
+ streamCount: connectionsWithStreams[0].activeStreamCount,
116
+ });
117
+ return connectionsWithStreams[0].conn;
118
+ }
119
+ // Priority 2: Based on reuse context
120
+ if (reuseContext) {
121
+ // Prefer inbound connections
122
+ const inbound = openConnections.find((c) => c.direction === 'inbound' || !c.direction);
123
+ if (inbound) {
124
+ this.logger.debug('Selected inbound connection (reuse context)');
125
+ return inbound;
126
+ }
127
+ }
128
+ else {
129
+ // Prefer outbound connections
130
+ const outbound = openConnections.find((c) => c.direction === 'outbound');
131
+ if (outbound) {
132
+ this.logger.debug('Selected outbound connection (non-reuse context)');
133
+ return outbound;
134
+ }
135
+ }
136
+ // Priority 3: Return first open connection
137
+ this.logger.debug('Selected first available open connection', connectionsWithStreams);
138
+ return openConnections[0];
139
+ }
140
+ /**
141
+ * Get a cached oNodeConnection for the given p2p connection if it exists and is valid.
142
+ * @param p2pConnection - The p2p connection to look up
143
+ * @returns A valid oNodeConnection or null if none found
144
+ */
145
+ getCachedNodeConnection(p2pConnection) {
146
+ const cached = this.nodeConnectionByConnectionId.get(p2pConnection.id);
147
+ if (cached && cached.p2pConnection?.status === 'open') {
148
+ return cached;
149
+ }
150
+ // Clean up stale entry if connection is no longer open
151
+ if (cached) {
152
+ this.nodeConnectionByConnectionId.delete(p2pConnection.id);
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Cache an oNodeConnection by its p2p connection ID for potential reuse.
158
+ * @param conn - The oNodeConnection to cache
159
+ */
160
+ cacheNodeConnection(conn) {
161
+ this.nodeConnectionByConnectionId.set(conn.p2pConnection.id, conn);
162
+ }
66
163
  async getOrCreateConnection(nextHopAddress, address) {
67
164
  if (!nextHopAddress) {
68
165
  throw new Error('Invalid address passed');
@@ -72,22 +169,32 @@ export class oNodeConnectionManager extends oConnectionManager {
72
169
  if (!addressKey) {
73
170
  throw new Error(`Unable to extract address key from address: ${nextHopAddress.toString()}`);
74
171
  }
75
- // Check if we have a cached connection by address key
76
- const cachedConnection = this.connectionsByAddress.get(addressKey);
77
- if (cachedConnection && cachedConnection.status === 'open') {
78
- this.logger.debug('Reusing cached connection for address:', nextHopAddress?.value);
79
- return cachedConnection;
172
+ // Check if we have cached connections by address key
173
+ const cachedConnections = this.connectionsByAddress.get(addressKey) || [];
174
+ const bestConnection = this.selectBestConnection(cachedConnections, false);
175
+ if (bestConnection) {
176
+ this.logger.debug('Reusing cached connection for address:', nextHopAddress?.value, bestConnection.id);
177
+ return bestConnection;
80
178
  }
81
- // Clean up stale connection if it exists but is not open
82
- if (cachedConnection && cachedConnection.status !== 'open') {
83
- this.logger.debug('Removing stale connection for address:', addressKey);
84
- this.connectionsByAddress.delete(addressKey);
179
+ // Clean up stale connections if they exist but are not open
180
+ if (cachedConnections.length > 0) {
181
+ const openConnections = cachedConnections.filter((c) => c.status === 'open');
182
+ if (openConnections.length === 0) {
183
+ this.logger.debug('Removing all stale connections for address:', addressKey);
184
+ this.connectionsByAddress.delete(addressKey);
185
+ }
186
+ else if (openConnections.length < cachedConnections.length) {
187
+ this.logger.debug('Cleaning up some stale connections for address:', addressKey);
188
+ this.connectionsByAddress.set(addressKey, openConnections);
189
+ }
85
190
  }
86
191
  // Check if libp2p has an active connection for this address
87
192
  const libp2pConnection = this.getCachedLibp2pConnection(nextHopAddress);
88
193
  if (libp2pConnection && libp2pConnection.status === 'open') {
89
194
  this.logger.debug('Caching existing libp2p connection for address:', addressKey);
90
- this.connectionsByAddress.set(addressKey, libp2pConnection);
195
+ const connections = this.connectionsByAddress.get(addressKey) || [];
196
+ connections.push(libp2pConnection);
197
+ this.connectionsByAddress.set(addressKey, connections);
91
198
  return libp2pConnection;
92
199
  }
93
200
  // Check if dial is already in progress for this address key
@@ -101,8 +208,10 @@ export class oNodeConnectionManager extends oConnectionManager {
101
208
  this.pendingDialsByAddress.set(addressKey, dialPromise);
102
209
  try {
103
210
  const connection = await dialPromise;
104
- // Cache the established connection by address key
105
- this.connectionsByAddress.set(addressKey, connection);
211
+ // Add the established connection to the cache array
212
+ const connections = this.connectionsByAddress.get(addressKey) || [];
213
+ connections.push(connection);
214
+ this.connectionsByAddress.set(addressKey, connections);
106
215
  return connection;
107
216
  }
108
217
  finally {
@@ -137,9 +246,16 @@ export class oNodeConnectionManager extends oConnectionManager {
137
246
  requestHandler: config.requestHandler ?? undefined,
138
247
  reusePolicy: reuse ? 'reuse' : 'none',
139
248
  });
249
+ // Cache the new connection by its p2p connection ID
250
+ this.cacheNodeConnection(connection);
140
251
  const addressKey = this.getAddressKey(nextHopAddress);
141
252
  if (addressKey) {
142
- this.connectionsByAddress.set(addressKey, p2pConnection);
253
+ const connections = this.connectionsByAddress.get(addressKey) || [];
254
+ // Only add if not already in the cache
255
+ if (!connections.includes(p2pConnection)) {
256
+ connections.push(p2pConnection);
257
+ this.connectionsByAddress.set(addressKey, connections);
258
+ }
143
259
  }
144
260
  else {
145
261
  this.logger.error('Should not happen! Failed to generate an address key for address:', nextHopAddress);
@@ -147,13 +263,21 @@ export class oNodeConnectionManager extends oConnectionManager {
147
263
  return connection;
148
264
  }
149
265
  /**
150
- * Connect to a given address, reusing libp2p connections when possible
266
+ * Connect to a given address, reusing oNodeConnection and libp2p connections when possible
151
267
  * @param config - Connection configuration
152
268
  * @returns The connection object
153
269
  */
154
270
  async connect(config) {
155
271
  const { address, nextHopAddress, callerAddress, readTimeoutMs, drainTimeoutMs, } = config;
272
+ // First get or create the underlying p2p connection
156
273
  const p2pConnection = await this.getOrCreateConnection(nextHopAddress, address);
274
+ // Check for existing valid oNodeConnection for this p2p connection
275
+ const existingConnection = this.getCachedNodeConnection(p2pConnection);
276
+ if (existingConnection) {
277
+ this.logger.debug('Reusing cached oNodeConnection for connection ID:', p2pConnection.id);
278
+ return existingConnection;
279
+ }
280
+ // No valid cached connection, create new one
157
281
  const connection = new oNodeConnection({
158
282
  nextHopAddress: nextHopAddress,
159
283
  address: address,
@@ -166,6 +290,8 @@ export class oNodeConnectionManager extends oConnectionManager {
166
290
  runOnLimitedConnection: this.config.runOnLimitedConnection ?? false,
167
291
  requestHandler: config.requestHandler ?? undefined,
168
292
  });
293
+ // Cache the new connection by its p2p connection ID
294
+ this.cacheNodeConnection(connection);
169
295
  return connection;
170
296
  }
171
297
  /**
@@ -180,8 +306,9 @@ export class oNodeConnectionManager extends oConnectionManager {
180
306
  return false;
181
307
  }
182
308
  // Check our address-based cache first
183
- const cachedConnection = this.connectionsByAddress.get(addressKey);
184
- if (cachedConnection?.status === 'open') {
309
+ const cachedConnections = this.connectionsByAddress.get(addressKey) || [];
310
+ const bestConnection = this.selectBestConnection(cachedConnections, false);
311
+ if (bestConnection) {
185
312
  return true;
186
313
  }
187
314
  // Fall back to checking libp2p's connections
@@ -194,7 +321,11 @@ export class oNodeConnectionManager extends oConnectionManager {
194
321
  if (hasOpenConnection) {
195
322
  const openConnection = connections.find((conn) => conn.status === 'open');
196
323
  if (openConnection) {
197
- this.connectionsByAddress.set(addressKey, openConnection);
324
+ const existingConnections = this.connectionsByAddress.get(addressKey) || [];
325
+ if (!existingConnections.includes(openConnection)) {
326
+ existingConnections.push(openConnection);
327
+ this.connectionsByAddress.set(addressKey, existingConnections);
328
+ }
198
329
  }
199
330
  }
200
331
  return hasOpenConnection;
@@ -216,9 +347,10 @@ export class oNodeConnectionManager extends oConnectionManager {
216
347
  return null;
217
348
  }
218
349
  // Check address-based cache first
219
- const cachedConnection = this.connectionsByAddress.get(addressKey);
220
- if (cachedConnection?.status === 'open') {
221
- return cachedConnection;
350
+ const cachedConnections = this.connectionsByAddress.get(addressKey) || [];
351
+ const bestConnection = this.selectBestConnection(cachedConnections, false);
352
+ if (bestConnection) {
353
+ return bestConnection;
222
354
  }
223
355
  const peerId = this.getPeerIdFromAddress(address);
224
356
  if (!peerId) {
@@ -227,15 +359,22 @@ export class oNodeConnectionManager extends oConnectionManager {
227
359
  // Query libp2p for connections to this peer
228
360
  const connections = this.p2pNode.getConnections();
229
361
  const filteredConnections = connections.filter((conn) => conn.remotePeer?.toString() === peerId);
230
- // Find the first open connection
231
- const openConnection = filteredConnections.find((conn) => conn.status === 'open');
232
- // If we found an open connection in libp2p, cache it by address key
233
- if (openConnection) {
234
- this.connectionsByAddress.set(addressKey, openConnection);
235
- return openConnection;
362
+ // Find open connections
363
+ const openConnections = filteredConnections.filter((conn) => conn.status === 'open');
364
+ // If we found open connections in libp2p, add them to cache and select best
365
+ if (openConnections.length > 0) {
366
+ const existingConnections = this.connectionsByAddress.get(addressKey) || [];
367
+ // Add any new connections that aren't already cached
368
+ for (const conn of openConnections) {
369
+ if (!existingConnections.includes(conn)) {
370
+ existingConnections.push(conn);
371
+ }
372
+ }
373
+ this.connectionsByAddress.set(addressKey, existingConnections);
374
+ return this.selectBestConnection(existingConnections, false);
236
375
  }
237
- // Clean up stale cache entry if connection is no longer open
238
- if (cachedConnection) {
376
+ // Clean up stale cache entries if connections are no longer open
377
+ if (cachedConnections.length > 0) {
239
378
  this.connectionsByAddress.delete(addressKey);
240
379
  }
241
380
  return null;
@@ -250,13 +389,22 @@ export class oNodeConnectionManager extends oConnectionManager {
250
389
  * @returns Object containing cache statistics
251
390
  */
252
391
  getCacheStats() {
392
+ const allConnections = [];
393
+ for (const [addressKey, connections,] of this.connectionsByAddress.entries()) {
394
+ for (const conn of connections) {
395
+ allConnections.push({
396
+ peerId: conn.remotePeer?.toString() ?? 'unknown',
397
+ status: conn.status,
398
+ addressKey,
399
+ });
400
+ }
401
+ }
253
402
  return {
254
- cachedConnections: this.connectionsByAddress.size,
403
+ cachedAddresses: this.connectionsByAddress.size,
404
+ totalCachedConnections: allConnections.length,
255
405
  pendingDials: this.pendingDialsByAddress.size,
256
- connectionsByPeer: Array.from(this.connectionsByAddress.values()).map((conn) => ({
257
- peerId: conn.remotePeer?.toString() ?? 'unknown',
258
- status: conn.status,
259
- })),
406
+ cachedNodeConnections: this.nodeConnectionByConnectionId.size,
407
+ connectionsByPeer: allConnections,
260
408
  };
261
409
  }
262
410
  /**
@@ -265,10 +413,19 @@ export class oNodeConnectionManager extends oConnectionManager {
265
413
  */
266
414
  cleanupStaleConnections() {
267
415
  let removed = 0;
268
- for (const [addressKey, connection,] of this.connectionsByAddress.entries()) {
269
- if (connection.status !== 'open') {
270
- this.connectionsByAddress.delete(addressKey);
271
- removed++;
416
+ for (const [addressKey, connections,] of this.connectionsByAddress.entries()) {
417
+ const openConnections = connections.filter((conn) => conn.status === 'open');
418
+ const staleCount = connections.length - openConnections.length;
419
+ if (staleCount > 0) {
420
+ removed += staleCount;
421
+ if (openConnections.length === 0) {
422
+ // Remove the entire entry if no connections remain
423
+ this.connectionsByAddress.delete(addressKey);
424
+ }
425
+ else {
426
+ // Keep only the open connections
427
+ this.connectionsByAddress.set(addressKey, openConnections);
428
+ }
272
429
  }
273
430
  }
274
431
  if (removed > 0) {
@@ -276,4 +433,21 @@ export class oNodeConnectionManager extends oConnectionManager {
276
433
  }
277
434
  return removed;
278
435
  }
436
+ /**
437
+ * Clean up all stale oNodeConnections from cache
438
+ * @returns Number of oNodeConnections removed
439
+ */
440
+ cleanupStaleNodeConnections() {
441
+ let removed = 0;
442
+ for (const [connectionId, conn,] of this.nodeConnectionByConnectionId.entries()) {
443
+ if (conn.p2pConnection?.status !== 'open') {
444
+ this.nodeConnectionByConnectionId.delete(connectionId);
445
+ removed++;
446
+ }
447
+ }
448
+ if (removed > 0) {
449
+ this.logger.debug(`Cleaned up ${removed} stale oNodeConnections`);
450
+ }
451
+ return removed;
452
+ }
279
453
  }
@@ -0,0 +1,2 @@
1
+ export * from './synchronized.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/decorators/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './synchronized.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Method decorator that automatically acquires and releases a lock
3
+ * when the decorated method is called. Ensures only one invocation
4
+ * of the method executes at a time.
5
+ *
6
+ * @param lockName - Optional custom lock name. If not provided,
7
+ * defaults to "ClassName:methodName"
8
+ * @param timeoutMs - Optional timeout for lock acquisition in milliseconds
9
+ *
10
+ * @example
11
+ * class MyNode extends oNode {
12
+ * protected lockManager = new LockManager();
13
+ *
14
+ * @Synchronized('register:leader')
15
+ * async registerLeader(): Promise<void> {
16
+ * // This method is automatically synchronized
17
+ * }
18
+ * }
19
+ */
20
+ export declare function Synchronized(lockName?: string, timeoutMs?: number): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
21
+ //# sourceMappingURL=synchronized.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"synchronized.d.ts","sourceRoot":"","sources":["../../../src/decorators/synchronized.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,YAEtD,GAAG,eACE,MAAM,cACP,kBAAkB,wBA8BjC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Method decorator that automatically acquires and releases a lock
3
+ * when the decorated method is called. Ensures only one invocation
4
+ * of the method executes at a time.
5
+ *
6
+ * @param lockName - Optional custom lock name. If not provided,
7
+ * defaults to "ClassName:methodName"
8
+ * @param timeoutMs - Optional timeout for lock acquisition in milliseconds
9
+ *
10
+ * @example
11
+ * class MyNode extends oNode {
12
+ * protected lockManager = new LockManager();
13
+ *
14
+ * @Synchronized('register:leader')
15
+ * async registerLeader(): Promise<void> {
16
+ * // This method is automatically synchronized
17
+ * }
18
+ * }
19
+ */
20
+ export function Synchronized(lockName, timeoutMs) {
21
+ return function (target, propertyKey, descriptor) {
22
+ const originalMethod = descriptor.value;
23
+ // Generate lock name if not provided
24
+ const finalLockName = lockName || `${target.constructor.name}:${propertyKey}`;
25
+ descriptor.value = async function (...args) {
26
+ // Ensure lockManager exists on the instance
27
+ const lockManager = this.lockManager;
28
+ if (!lockManager) {
29
+ throw new Error(`LockManager not initialized on instance. ` +
30
+ `Ensure 'protected lockManager = new LockManager()' is declared in ${target.constructor.name}`);
31
+ }
32
+ // Execute the method with automatic lock management
33
+ return lockManager.withLock(finalLockName, async () => {
34
+ return originalMethod.apply(this, args);
35
+ }, timeoutMs);
36
+ };
37
+ return descriptor;
38
+ };
39
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './o-node.js';
2
2
  export * from './utils/index.js';
3
+ export * from './decorators/index.js';
3
4
  export * from './o-node.hierarchy-manager.js';
4
5
  export * from './interfaces/o-node.config.js';
5
6
  export * from './connection/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oCAAoC,CAAC;AACnD,cAAc,mBAAmB,CAAC;AAClC,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oCAAoC,CAAC;AACnD,cAAc,mBAAmB,CAAC;AAClC,cAAc,kCAAkC,CAAC"}
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './o-node.js';
2
2
  export * from './utils/index.js';
3
+ export * from './decorators/index.js';
3
4
  export * from './o-node.hierarchy-manager.js';
4
5
  export * from './interfaces/o-node.config.js';
5
6
  export * from './connection/index.js';
@@ -11,6 +11,7 @@ import { oToolBase } from '@olane/o-tool';
11
11
  import { oConnectionHeartbeatManager } from './managers/o-connection-heartbeat.manager.js';
12
12
  import { oNodeConnectionConfig } from './connection/index.js';
13
13
  import { oReconnectionManager } from './managers/o-reconnection.manager.js';
14
+ import { LockManager } from './utils/lock-manager.js';
14
15
  export declare class oNode extends oToolBase {
15
16
  peerId: PeerId;
16
17
  p2pNode: Libp2p;
@@ -23,6 +24,7 @@ export declare class oNode extends oToolBase {
23
24
  protected didRegister: boolean;
24
25
  protected hooksStartFinished: any[];
25
26
  protected hooksInitFinished: any[];
27
+ protected lockManager: LockManager;
26
28
  constructor(config: oNodeConfig);
27
29
  get leader(): oNodeAddress | null;
28
30
  get networkConfig(): Libp2pConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.d.ts","sourceRoot":"","sources":["../../src/o-node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"o-node.d.ts","sourceRoot":"","sources":["../../src/o-node.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,12 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
1
10
  import { createNode, defaultLibp2pConfig, KEEP_ALIVE, } from '@olane/o-config';
2
11
  import { v4 as uuidv4 } from 'uuid';
3
12
  import { oNodeRouter } from './router/o-node.router.js';
@@ -11,12 +20,15 @@ import { oMethodResolver, oToolBase } from '@olane/o-tool';
11
20
  import { oLeaderResolverFallback } from './router/index.js';
12
21
  import { oNodeNotificationManager } from './o-node.notification-manager.js';
13
22
  import { oReconnectionManager } from './managers/o-reconnection.manager.js';
23
+ import { LockManager } from './utils/lock-manager.js';
24
+ import { Synchronized } from './decorators/synchronized.js';
14
25
  export class oNode extends oToolBase {
15
26
  constructor(config) {
16
27
  super(config);
17
28
  this.didRegister = false;
18
29
  this.hooksStartFinished = [];
19
30
  this.hooksInitFinished = [];
31
+ this.lockManager = new LockManager();
20
32
  this.config = config;
21
33
  }
22
34
  get leader() {
@@ -522,6 +534,8 @@ export class oNode extends oToolBase {
522
534
  resetState() {
523
535
  // Reset registration flag
524
536
  this.didRegister = false;
537
+ // Clear all locks
538
+ this.lockManager.clearAll();
525
539
  // Clear peer references
526
540
  this.peerId = undefined;
527
541
  this.p2pNode = undefined;
@@ -638,3 +652,15 @@ export class oNode extends oToolBase {
638
652
  }
639
653
  }
640
654
  }
655
+ __decorate([
656
+ Synchronized('register:parent'),
657
+ __metadata("design:type", Function),
658
+ __metadata("design:paramtypes", []),
659
+ __metadata("design:returntype", Promise)
660
+ ], oNode.prototype, "registerParent", null);
661
+ __decorate([
662
+ Synchronized('register:leader'),
663
+ __metadata("design:type", Function),
664
+ __metadata("design:paramtypes", []),
665
+ __metadata("design:returntype", Promise)
666
+ ], oNode.prototype, "registerLeader", null);
@@ -1 +1 @@
1
- {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAMrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;IAgBrC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAoBhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC;IAwCV,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAgC5D"}
1
+ {"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAMrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,mBAAmB,CAAC,OAAO,EAAE,QAAQ;IAsBrC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAoBhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC;IAyCV,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAiC5D"}
@@ -13,6 +13,7 @@ import { ConnectionUtils } from './utils/connection.utils.js';
13
13
  export class oNodeTool extends oTool(oServerNode) {
14
14
  async handleProtocolReuse(address) {
15
15
  const reuseProtocol = address.protocol + '/reuse';
16
+ this.logger.debug('Handling protocol reuse: ' + reuseProtocol);
16
17
  const protocols = this.p2pNode.getProtocols();
17
18
  if (protocols.find((p) => p === reuseProtocol)) {
18
19
  // already handling
@@ -21,10 +22,11 @@ export class oNodeTool extends oTool(oServerNode) {
21
22
  const maxOutboundsStreams = process.env.MAX_OUTBOUND_STREAMS
22
23
  ? parseInt(process.env.MAX_OUTBOUND_STREAMS)
23
24
  : 1000;
24
- await this.p2pNode.handle(reuseProtocol, this.handleStream.bind(this), {
25
+ await this.p2pNode.handle(reuseProtocol, this.handleStreamReuse.bind(this), {
25
26
  maxInboundStreams: 10000,
26
27
  maxOutboundStreams: maxOutboundsStreams,
27
28
  });
29
+ this.logger.debug('Handled protocol reuse: ' + reuseProtocol);
28
30
  }
29
31
  async handleProtocol(address) {
30
32
  const protocols = this.p2pNode.getProtocols();
@@ -72,6 +74,7 @@ export class oNodeTool extends oTool(oServerNode) {
72
74
  callerAddress: this.address,
73
75
  p2pConnection: connection,
74
76
  reuse,
77
+ // requestHandler: this.execute.bind(this), TODO: do we need this?
75
78
  });
76
79
  // Use StreamHandler for consistent stream handling
77
80
  // This follows libp2p v3 best practices for length-prefixed streaming
@@ -108,6 +111,7 @@ export class oNodeTool extends oTool(oServerNode) {
108
111
  }));
109
112
  }
110
113
  // create downward direction connection
114
+ this.logger.debug('Pinging child to confirm access');
111
115
  await this.useChild(childAddress, {
112
116
  method: 'ping',
113
117
  params: {},
@@ -1,4 +1,5 @@
1
1
  export * from './stream.utils.js';
2
2
  export * from './network.utils.js';
3
3
  export * from './connection.utils.js';
4
+ export * from './lock-manager.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
@@ -1,3 +1,4 @@
1
1
  export * from './stream.utils.js';
2
2
  export * from './network.utils.js';
3
3
  export * from './connection.utils.js';
4
+ export * from './lock-manager.js';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Promise-based lock manager for handling concurrent access to critical sections
3
+ * Supports named locks for fine-grained concurrency control
4
+ */
5
+ export declare class LockManager {
6
+ private locks;
7
+ private lockStates;
8
+ private resolvers;
9
+ /**
10
+ * Acquire a named lock, waiting if necessary
11
+ * @param lockName - Unique identifier for the lock
12
+ * @param timeoutMs - Optional timeout in milliseconds
13
+ * @throws Error if timeout is reached
14
+ */
15
+ acquire(lockName: string, timeoutMs?: number): Promise<void>;
16
+ /**
17
+ * Release a named lock
18
+ * @param lockName - Unique identifier for the lock to release
19
+ */
20
+ release(lockName: string): void;
21
+ /**
22
+ * Execute a function with automatic lock acquisition/release
23
+ * @param lockName - Unique identifier for the lock
24
+ * @param fn - Async function to execute while holding the lock
25
+ * @param timeoutMs - Optional timeout for lock acquisition
26
+ * @returns The result of the function execution
27
+ */
28
+ withLock<T>(lockName: string, fn: () => Promise<T>, timeoutMs?: number): Promise<T>;
29
+ /**
30
+ * Check if a lock is currently held
31
+ * @param lockName - Unique identifier for the lock
32
+ * @returns true if the lock is currently held
33
+ */
34
+ isLocked(lockName: string): boolean;
35
+ /**
36
+ * Clear all locks (for teardown/reset)
37
+ * Resolves any pending waiters
38
+ */
39
+ clearAll(): void;
40
+ /**
41
+ * Get statistics about current lock states (useful for debugging)
42
+ * @returns Object containing lock statistics
43
+ */
44
+ getStats(): {
45
+ totalLocks: number;
46
+ activeLocks: number;
47
+ lockNames: string[];
48
+ };
49
+ }
50
+ //# sourceMappingURL=lock-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-manager.d.ts","sourceRoot":"","sources":["../../../src/utils/lock-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAyC;IACtD,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,SAAS,CAAsC;IAEvD;;;;;OAKG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BlE;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAS/B;;;;;;OAMG;IACG,QAAQ,CAAC,CAAC,EACd,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,CAAC;IASb;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAInC;;;OAGG;IACH,QAAQ,IAAI,IAAI;IAUhB;;;OAGG;IACH,QAAQ,IAAI;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;KACrB;CAWF"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Promise-based lock manager for handling concurrent access to critical sections
3
+ * Supports named locks for fine-grained concurrency control
4
+ */
5
+ export class LockManager {
6
+ constructor() {
7
+ this.locks = new Map();
8
+ this.lockStates = new Map();
9
+ this.resolvers = new Map();
10
+ }
11
+ /**
12
+ * Acquire a named lock, waiting if necessary
13
+ * @param lockName - Unique identifier for the lock
14
+ * @param timeoutMs - Optional timeout in milliseconds
15
+ * @throws Error if timeout is reached
16
+ */
17
+ async acquire(lockName, timeoutMs) {
18
+ // Wait for any existing lock to be released
19
+ while (this.lockStates.get(lockName)) {
20
+ const currentLock = this.locks.get(lockName);
21
+ if (currentLock) {
22
+ if (timeoutMs) {
23
+ await Promise.race([
24
+ currentLock,
25
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Lock timeout: ${lockName}`)), timeoutMs)),
26
+ ]);
27
+ }
28
+ else {
29
+ await currentLock;
30
+ }
31
+ }
32
+ }
33
+ // Acquire the lock
34
+ this.lockStates.set(lockName, true);
35
+ const lockPromise = new Promise((resolve) => {
36
+ this.resolvers.set(lockName, resolve);
37
+ });
38
+ this.locks.set(lockName, lockPromise);
39
+ }
40
+ /**
41
+ * Release a named lock
42
+ * @param lockName - Unique identifier for the lock to release
43
+ */
44
+ release(lockName) {
45
+ const resolver = this.resolvers.get(lockName);
46
+ if (resolver) {
47
+ resolver();
48
+ this.resolvers.delete(lockName);
49
+ }
50
+ this.lockStates.set(lockName, false);
51
+ }
52
+ /**
53
+ * Execute a function with automatic lock acquisition/release
54
+ * @param lockName - Unique identifier for the lock
55
+ * @param fn - Async function to execute while holding the lock
56
+ * @param timeoutMs - Optional timeout for lock acquisition
57
+ * @returns The result of the function execution
58
+ */
59
+ async withLock(lockName, fn, timeoutMs) {
60
+ await this.acquire(lockName, timeoutMs);
61
+ try {
62
+ return await fn();
63
+ }
64
+ finally {
65
+ this.release(lockName);
66
+ }
67
+ }
68
+ /**
69
+ * Check if a lock is currently held
70
+ * @param lockName - Unique identifier for the lock
71
+ * @returns true if the lock is currently held
72
+ */
73
+ isLocked(lockName) {
74
+ return this.lockStates.get(lockName) ?? false;
75
+ }
76
+ /**
77
+ * Clear all locks (for teardown/reset)
78
+ * Resolves any pending waiters
79
+ */
80
+ clearAll() {
81
+ // Resolve any pending waiters
82
+ for (const lockName of this.lockStates.keys()) {
83
+ this.release(lockName);
84
+ }
85
+ this.locks.clear();
86
+ this.lockStates.clear();
87
+ this.resolvers.clear();
88
+ }
89
+ /**
90
+ * Get statistics about current lock states (useful for debugging)
91
+ * @returns Object containing lock statistics
92
+ */
93
+ getStats() {
94
+ const activeLocks = Array.from(this.lockStates.entries()).filter(([_, isLocked]) => isLocked);
95
+ return {
96
+ totalLocks: this.lockStates.size,
97
+ activeLocks: activeLocks.length,
98
+ lockNames: activeLocks.map(([name]) => name),
99
+ };
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olane/o-node",
3
- "version": "0.7.44",
3
+ "version": "0.7.46",
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.44",
43
+ "@olane/o-test": "0.7.46",
44
44
  "@tsconfig/node20": "^20.1.6",
45
45
  "@types/jest": "^30.0.0",
46
46
  "@types/json5": "^2.2.0",
@@ -60,13 +60,13 @@
60
60
  "typescript": "5.4.5"
61
61
  },
62
62
  "dependencies": {
63
- "@olane/o-config": "0.7.44",
64
- "@olane/o-core": "0.7.44",
65
- "@olane/o-protocol": "0.7.44",
66
- "@olane/o-tool": "0.7.44",
63
+ "@olane/o-config": "0.7.46",
64
+ "@olane/o-core": "0.7.46",
65
+ "@olane/o-protocol": "0.7.46",
66
+ "@olane/o-tool": "0.7.46",
67
67
  "debug": "^4.4.1",
68
68
  "dotenv": "^16.5.0",
69
69
  "json5": "^2.2.3"
70
70
  },
71
- "gitHead": "de7bc5247c97fb219d522cc5874f7459634e03ec"
71
+ "gitHead": "48c45f37c6e2e1ee626ce1fb0f0de175ca2db3b6"
72
72
  }