diodejs 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,22 +38,40 @@ DIODE_TICKET_BYTES_THRESHOLD=512000
38
38
  DIODE_TICKET_UPDATE_INTERVAL=30000
39
39
  ```
40
40
 
41
- These settings can also be configured programmatically:
41
+ These settings can also be configured programmatically on the relay connections managed by `DiodeClientManager`:
42
42
  ```javascript
43
- connection.setReconnectOptions({
44
- maxRetries: 10,
45
- retryDelay: 2000,
46
- maxRetryDelay: 20000,
47
- autoReconnect: true,
48
- ticketBytesThreshold: 512000,
49
- ticketUpdateInterval: 30000
50
- });
43
+ const { DiodeClientManager } = require('diodejs');
44
+
45
+ async function main() {
46
+ const client = new DiodeClientManager({ keyLocation: './db/keys.json' });
47
+ await client.connect();
48
+
49
+ for (const connection of client.getConnections()) {
50
+ connection.setReconnectOptions({
51
+ maxRetries: 10,
52
+ retryDelay: 2000,
53
+ maxRetryDelay: 20000,
54
+ autoReconnect: true
55
+ });
56
+
57
+ connection.setTicketBatchingOptions({
58
+ threshold: 512000,
59
+ interval: 30000
60
+ });
61
+ }
62
+ }
63
+
64
+ main();
51
65
  ```
52
66
 
53
67
  ### Multi-Relay Connections (Recommended)
54
68
 
55
69
  You can connect to multiple Diode relays and automatically route binds to the relay where the target device is connected.
56
70
 
71
+ `DiodeClientManager` now ranks relays by observed latency. On startup it probes the required candidate set, persists relay scores to disk, and prefers the lowest-latency connected relay for control-plane RPC calls. It does not ping the full network at startup.
72
+
73
+ When neither `host` nor `hosts` is specified, the manager starts from the default seed pool, any discovery-provider candidates, built-in `dio_network` candidates, and previously successful non-provider relays saved in `relay-scores.json`. Without live network discovery it probes all default seeds once. When live network discovery returns usable relays, startup resolves from a region-diverse seed bootstrap subset plus the bounded discovery sample, then continues measuring the remaining seeds in the background while keeping region diversity in the warm set. If you pass `host` or `hosts`, startup stays constrained to those configured relays unless you explicitly opt into using the discovery provider alongside them.
74
+
57
75
  ```javascript
58
76
  const { DiodeClientManager, BindPort } = require('diodejs');
59
77
 
@@ -69,6 +87,66 @@ async function main() {
69
87
  }
70
88
  ```
71
89
 
90
+ You can tune relay selection if needed:
91
+
92
+ ```javascript
93
+ const client = new DiodeClientManager({
94
+ keyLocation: './db/keys.json',
95
+ relaySelection: {
96
+ startupConcurrency: 2,
97
+ minReadyConnections: 2,
98
+ probeTimeoutMs: 1200,
99
+ warmConnectionBudget: 3,
100
+ probeAllInitialCandidates: true,
101
+ continueProbingUntestedSeeds: true,
102
+ regionDiverseSeedOrdering: true,
103
+ discoveryProviderTimeoutMs: 1500,
104
+ backgroundProbeIntervalMs: 300000,
105
+ slowRelayThresholdMs: 250,
106
+ slowDeviceRetryTtlMs: 5000,
107
+ scoreCachePath: './db/relay-scores.json'
108
+ }
109
+ });
110
+ ```
111
+
112
+ Routing note: the initiating client can prefer a better local relay, but it cannot override the relay encoded in the remote device ticket. Best results come when both clients use the manager's relay ranking so each side reconnects toward a closer relay over time.
113
+
114
+ Discovery note: startup discovery sources are now `configured/seed`, `discoveryProvider`, built-in live `dio_network` discovery, cached non-provider/non-network relays, and target-on-demand relay resolution from `getNode(serverId)`. Provider and network membership are not cached independently; only RTT/history is persisted in `relay-scores.json`.
115
+
116
+ Example discovery provider:
117
+
118
+ ```javascript
119
+ const fs = require('fs/promises');
120
+
121
+ const client = new DiodeClientManager({
122
+ keyLocation: './db/keys.json',
123
+ relaySelection: {
124
+ discoveryProvider: async () => {
125
+ const data = JSON.parse(await fs.readFile('./relays.json', 'utf8'));
126
+ return data;
127
+ },
128
+ networkDiscovery: {
129
+ endpoint: 'wss://prenet.diode.io:8443/ws',
130
+ startupProbeCount: 2,
131
+ backgroundBatchSize: 12
132
+ }
133
+ }
134
+ });
135
+ ```
136
+
137
+ `discoveryProvider(context)` may return either relay strings such as `'relay.example.com:41046'` or objects like `{ host, port, priority, region, metadata }`. The callback receives a read-only `context` object with:
138
+
139
+ - `defaultPort`
140
+ - `keyLocation`
141
+ - `explicitHost`
142
+ - `explicitHosts`
143
+ - `initialHosts`
144
+ - `knownRelayScores`
145
+
146
+ `knownRelayScores` contains the manager's current score snapshot for previously seen relays. Provider membership itself is not cached across runs; only relay score history is persisted.
147
+
148
+ Built-in network discovery uses the Diode JSON-RPC websocket endpoint and requests `dio_network`. It is enabled by default only when neither `host` nor `hosts` is set. Only connected `server` nodes are considered, and private/unroutable addresses are filtered unless `includePrivateAddresses` is enabled. When discovery succeeds, startup keeps the seed safety baseline by probing one seed per region before adding the configured discovery sample, instead of blocking on every seed before ready.
149
+
72
150
  If you provide a host, only that relay is used initially (similar to `-diodeaddrs`):
73
151
 
74
152
  ```javascript
@@ -85,25 +163,35 @@ await client.connect();
85
163
  Here's a quick example to get you started with RPC functions using `DiodeRPC` Class
86
164
 
87
165
  ```javascript
88
- const { DiodeConnection, DiodeRPC, makeReadable } = require('diodejs');
166
+ const { DiodeClientManager, DiodeRPC, makeReadable } = require('diodejs');
89
167
 
90
168
  async function main() {
91
169
  const host = 'eu2.prenet.diode.io';
92
170
  const port = 41046;
93
171
  const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
94
172
 
95
- const connection = new DiodeConnection(host, port, keyLocation);
96
-
173
+ const client = new DiodeClientManager({ host, port, keyLocation });
174
+ await client.connect();
175
+
176
+ const connection = client.getConnections()[0];
177
+ if (!connection) {
178
+ throw new Error('No relay connection available');
179
+ }
180
+
97
181
  // Configure reconnection (optional - overrides environment variables)
98
182
  connection.setReconnectOptions({
99
183
  maxRetries: Infinity, // Unlimited reconnection attempts
100
184
  retryDelay: 1000, // Initial delay of 1 second
101
185
  maxRetryDelay: 30000, // Maximum delay of 30 seconds
102
- autoReconnect: true, // Automatically reconnect on disconnection
103
- ticketBytesThreshold: 512000, // Bytes threshold for ticket updates
104
- ticketUpdateInterval: 30000 // Time interval for ticket updates
186
+ autoReconnect: true // Automatically reconnect on disconnection
105
187
  });
106
-
188
+
189
+ // Configure ticket batching (optional - overrides environment variables)
190
+ connection.setTicketBatchingOptions({
191
+ threshold: 512000, // Bytes threshold for ticket updates
192
+ interval: 30000 // Time interval for ticket updates
193
+ });
194
+
107
195
  // Listen for reconnection events (optional)
108
196
  connection.on('reconnecting', (info) => {
109
197
  console.log(`Reconnecting... Attempt #${info.attempt} in ${info.delay}ms`);
@@ -114,8 +202,6 @@ async function main() {
114
202
  connection.on('reconnect_failed', () => {
115
203
  console.log('Failed to reconnect after maximum attempts');
116
204
  });
117
-
118
- await connection.connect();
119
205
 
120
206
  const rpc = new DiodeRPC(connection);
121
207
 
@@ -130,7 +216,7 @@ async function main() {
130
216
  } catch (error) {
131
217
  console.error('RPC Error:', error);
132
218
  } finally {
133
- connection.close();
219
+ client.close();
134
220
  }
135
221
  }
136
222
 
@@ -143,15 +229,15 @@ Here's a quick example to get you started with port forwarding using the `BindPo
143
229
 
144
230
  #### Port Binding
145
231
  ```javascript
146
- const { DiodeConnection, BindPort } = require('diodejs');
232
+ const { DiodeClientManager, BindPort } = require('diodejs');
147
233
 
148
234
  async function main() {
149
235
  const host = 'eu2.prenet.diode.io';
150
236
  const port = 41046;
151
237
  const keyLocation = './db/keys.json';
152
238
 
153
- const connection = new DiodeConnection(host, port, keyLocation);
154
- await connection.connect();
239
+ const client = new DiodeClientManager({ host, port, keyLocation });
240
+ await client.connect();
155
241
 
156
242
  // Multiple or single port binding with configuration object
157
243
  const portsConfig = {
@@ -168,7 +254,7 @@ async function main() {
168
254
  }
169
255
  };
170
256
 
171
- const portForward = new BindPort(connection, portsConfig);
257
+ const portForward = new BindPort(client, portsConfig);
172
258
  portForward.bind();
173
259
 
174
260
  // You can also dynamically add ports with protocol specification
@@ -181,18 +267,18 @@ main();
181
267
 
182
268
  #### Single Port Binding (Legacy)
183
269
  ```javascript
184
- const { DiodeConnection, BindPort } = require('diodejs');
270
+ const { DiodeClientManager, BindPort } = require('diodejs');
185
271
 
186
272
  async function main() {
187
273
  const host = 'eu2.prenet.diode.io';
188
274
  const port = 41046;
189
275
  const keyLocation = './db/keys.json';
190
276
 
191
- const connection = new DiodeConnection(host, port, keyLocation);
192
- await connection.connect();
277
+ const client = new DiodeClientManager({ host, port, keyLocation });
278
+ await client.connect();
193
279
 
194
280
  // Legacy method - single port binding (defaults to TLS protocol)
195
- const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
281
+ const portForward = new BindPort(client, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
196
282
  portForward.bind();
197
283
  }
198
284
 
@@ -204,15 +290,15 @@ main();
204
290
  Here's a quick example to get you started with publishing ports using the `PublishPort` class:
205
291
 
206
292
  ```javascript
207
- const { DiodeConnection, PublishPort } = require('diodejs');
293
+ const { DiodeClientManager, PublishPort } = require('diodejs');
208
294
 
209
295
  async function main() {
210
296
  const host = 'us2.prenet.diode.io';
211
297
  const port = 41046;
212
298
  const keyLocation = './db/keys.json';
213
299
 
214
- const connection = new DiodeConnection(host, port, keyLocation);
215
- await connection.connect();
300
+ const client = new DiodeClientManager({ host, port, keyLocation });
301
+ await client.connect();
216
302
 
217
303
  // Option 1: Simple array of ports (all public)
218
304
  const publishedPorts = [8080, 3000];
@@ -227,7 +313,7 @@ async function main() {
227
313
  };
228
314
 
229
315
  // certPath parameter is maintained for backward compatibility but not required
230
- const publishPort = new PublishPort(connection, publishedPortsWithConfig);
316
+ const publishPort = new PublishPort(client, publishedPortsWithConfig);
231
317
  }
232
318
 
233
319
  main();
@@ -237,35 +323,6 @@ main();
237
323
 
238
324
  ### Classes and Methods
239
325
 
240
- #### `DiodeConnection`
241
-
242
- - **Constructor**: `new DiodeConnection(host, port, keyLocation)`
243
- - `host` (string): The host address of the Diode server.
244
- - `port` (number): The port number of the Diode server.
245
- - `keyLocation` (string)(default: './db/keys.json'): The path to the key storage file. If the file doesn't exist, keys are generated automatically.
246
-
247
- - **Methods**:
248
- - `connect()`: Connects to the Diode server. Returns a promise.
249
- - `sendCommand(commandArray)`: Sends a command to the Diode server. Returns a promise.
250
- - `sendCommandWithSessionId(commandArray, sessionId)`: Sends a command with a session ID. Returns a promise.
251
- - `getEthereumAddress()`: Returns the Ethereum address derived from the device keys.
252
- - `getServerEthereumAddress()`: Returns the Ethereum address of the server.
253
- - `createTicketCommand()`: Creates a ticket command for authentication. Returns a promise.
254
- - `close()`: Closes the connection to the Diode server.
255
- - `getDeviceCertificate()`: Returns the generated certificate PEM.
256
- - `setReconnectOptions(options)`: Configures reconnection behavior with the following options:
257
- - `maxRetries` (number): Maximum reconnection attempts (default: Infinity)
258
- - `retryDelay` (number): Initial delay between retries in ms (default: 1000)
259
- - `maxRetryDelay` (number): Maximum delay between retries in ms (default: 30000)
260
- - `autoReconnect` (boolean): Whether to automatically reconnect on disconnection (default: true)
261
- - `ticketBytesThreshold` (number): Bytes threshold for ticket updates (default: 512000)
262
- - `ticketUpdateInterval` (number): Time interval for ticket updates in ms (default: 30000)
263
-
264
- - **Events**:
265
- - `reconnecting`: Emitted when a reconnection attempt is about to start, with `attempt` and `delay` information
266
- - `reconnected`: Emitted when reconnection is successful
267
- - `reconnect_failed`: Emitted when all reconnection attempts have failed
268
-
269
326
  #### `DiodeClientManager`
270
327
 
271
328
  - **Constructor**: `new DiodeClientManager(options)`
@@ -274,17 +331,51 @@ main();
274
331
  - `options.hosts` (string[] or comma-separated string, optional): Explicit relay list.
275
332
  - `options.keyLocation` (string, optional): Key storage path (default: `./db/keys.json`).
276
333
  - `options.deviceCacheTtlMs` (number, optional): Cache TTL for device relay resolution (default: `30000`).
334
+ - `options.relaySelection` (object, optional): Relay ranking and probing options.
335
+ - `enabled` (boolean, optional): Enables smart relay ranking. Defaults to `true`.
336
+ - `startupConcurrency` (number, optional): Parallel startup probe limit. Defaults to `2`.
337
+ - `minReadyConnections` (number, optional): Minimum target connection count for the startup probe set. Defaults to `2`.
338
+ - `probeTimeoutMs` (number, optional): Ping timeout for relay probes. Defaults to `1200`.
339
+ - `warmConnectionBudget` (number, optional): Maximum number of idle control relays to keep after startup coverage completes. Defaults to `3`.
340
+ - `probeAllInitialCandidates` (boolean, optional): Probes all initial configured/default candidates once before final startup ranking is trusted. Defaults to `true`.
341
+ - `continueProbingUntestedSeeds` (boolean, optional): Continues probing untested candidates after startup coverage completes. Defaults to `true`.
342
+ - `regionDiverseSeedOrdering` (boolean, optional): Interleaves seed regions during first-pass probing and warm retention. Defaults to `true`.
343
+ - `discoveryProvider` (function, optional): Async or sync callback that returns extra relay candidates as strings or `{ host, port, priority, region, metadata }` objects. The callback receives `{ defaultPort, keyLocation, explicitHost, explicitHosts, initialHosts, knownRelayScores }`.
344
+ - `discoveryProviderTimeoutMs` (number, optional): Timeout for `discoveryProvider` results. Defaults to `1500`.
345
+ - `useProviderWithExplicitHost` (boolean, optional): Allows provider candidates when `options.host` is set. Defaults to `false`.
346
+ - `useProviderWithExplicitHosts` (boolean, optional): Allows provider candidates when `options.hosts` is set. Defaults to `false`.
347
+ - `networkDiscovery` (object, optional): Built-in live directory discovery via the JSON-RPC websocket endpoint.
348
+ - `enabled` (boolean, optional): Enables live network discovery when no explicit `host` or `hosts` is configured. Defaults to `true` in default mode.
349
+ - `endpoint` (string, optional): Directory endpoint. Defaults to `wss://prenet.diode.io:8443/ws`.
350
+ - `method` (string, optional): JSON-RPC method name. Defaults to `dio_network`.
351
+ - `timeoutMs` (number, optional): Timeout for the discovery websocket request. Defaults to `1500`.
352
+ - `startupProbeCount` (number, optional): Number of discovered network relays added to the startup coverage probe set. Defaults to `2`.
353
+ - `backgroundBatchSize` (number, optional): Number of additional discovered relays measured in the background queue per run. Defaults to `12`.
354
+ - `includePrivateAddresses` (boolean, optional): Allows private or unroutable discovery results to be used. Defaults to `false`.
355
+ - `backgroundProbeIntervalMs` (number, optional): Minimum interval before background refresh probes are retried for a relay. Defaults to `300000`.
356
+ - `slowRelayThresholdMs` (number, optional): RTT threshold used to shorten target relay cache entries. Defaults to `250`.
357
+ - `slowDeviceRetryTtlMs` (number, optional): TTL used for slow target relays. Defaults to `5000`.
358
+ - `deviceRelayReconciliation` (object, optional): Bounded fallback that re-resolves a device through alternate connected control relays when the initially resolved target relay is much slower than the current control relay baseline.
359
+ - `enabled` (boolean, optional): Enables reconciliation. Defaults to `true`.
360
+ - `maxControlRelays` (number, optional): Maximum number of alternate connected control relays queried per reconciliation attempt. Defaults to `2`.
361
+ - `timeoutMs` (number, optional): Per-control-relay timeout for reconciliation lookups. Defaults to `probeTimeoutMs`.
362
+ - `minLatencyDeltaMs` (number, optional): Minimum RTT gap between the target relay and the control relay baseline before reconciliation triggers. Defaults to `150`.
363
+ - `slowdownFactor` (number, optional): Minimum multiple by which the target relay RTT must exceed the control relay baseline before reconciliation triggers. Defaults to `4`.
364
+ - `scoreCachePath` (string|null, optional): Relay score cache file. Defaults to `./db/relay-scores.json` next to `keyLocation`. Set to `null` to disable persistence.
277
365
 
278
366
  - **Methods**:
279
- - `connect()`: Connects to the initial relay pool. Returns a promise.
367
+ - `connect()`: Probes the required initial relays, merges provider and optional `dio_network` candidates, ranks the successful relays by latency, trims the warm relay set, and returns a promise. In live network discovery mode this starts from a region-diverse seed bootstrap subset plus the bounded discovery sample, then continues probing the remaining seeds in the background.
368
+ - `getNearestConnection()`: Returns the preferred connected relay. With relay selection enabled, this is the lowest-latency scored relay.
280
369
  - `getConnectionForDevice(deviceId)`: Resolves and returns a relay connection for the device. Returns a promise.
281
370
  - `getConnections()`: Returns a list of active connections.
282
371
  - `close()`: Closes all managed connections.
283
372
 
373
+ Connections returned by `getConnections()` or `getConnectionForDevice()` emit `reconnecting`, `reconnected`, and `reconnect_failed` events, and support `setReconnectOptions(...)` and `setTicketBatchingOptions({ threshold, interval })`.
374
+
284
375
  #### `DiodeRPC`
285
376
 
286
377
  - **Constructor**: `new DiodeRPC(connection)`
287
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
378
+ - `connection` (object): A relay connection from `DiodeClientManager.getConnections()` or `DiodeClientManager.getConnectionForDevice()`.
288
379
 
289
380
  - **Methods**:
290
381
  - `getBlockPeak()`: Retrieves the current block peak. Returns a promise.
@@ -308,14 +399,14 @@ main();
308
399
 
309
400
  Legacy Constructor:
310
401
  - `new BindPort(connection, localPort, targetPort, deviceIdHex)`
311
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
402
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
312
403
  - `localPort` (number): The local port to bind.
313
404
  - `targetPort` (number): The target port on the device.
314
405
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
315
406
 
316
407
  New Constructor:
317
408
  - `new BindPort(connection, portsConfig)`
318
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
409
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
319
410
  - `portsConfig` (object): A configuration object where keys are local ports and values are objects with:
320
411
  - `targetPort` (number): The target port on the device.
321
412
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
@@ -334,7 +425,7 @@ main();
334
425
  #### `PublishPort`
335
426
 
336
427
  - **Constructor**: `new PublishPort(connection, publishedPorts, _certPath)`
337
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
428
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
338
429
  - `publishedPorts` (array|object): Either:
339
430
  - An array of ports to publish (all public mode)
340
431
  - An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'] } }`