diodejs 0.4.0 → 0.4.2

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,61 @@ 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
 
67
+ Fleet contract selection is configured on the manager:
68
+ ```javascript
69
+ const { DiodeClientManager } = require('diodejs');
70
+
71
+ async function main() {
72
+ const client = new DiodeClientManager({
73
+ keyLocation: './db/keys.json',
74
+ fleetContract: '0x1111111111111111111111111111111111111111',
75
+ });
76
+
77
+ await client.connect();
78
+
79
+ // Applies on the next ticket generated by each active relay connection.
80
+ client.setFleetContract('0x2222222222222222222222222222222222222222');
81
+ }
82
+
83
+ main();
84
+ ```
85
+
86
+ If `fleetContract` is omitted, tickets continue using the default contract `0x6000000000000000000000000000000000000000`.
87
+
53
88
  ### Multi-Relay Connections (Recommended)
54
89
 
55
90
  You can connect to multiple Diode relays and automatically route binds to the relay where the target device is connected.
56
91
 
92
+ `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.
93
+
94
+ 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.
95
+
57
96
  ```javascript
58
97
  const { DiodeClientManager, BindPort } = require('diodejs');
59
98
 
@@ -69,6 +108,66 @@ async function main() {
69
108
  }
70
109
  ```
71
110
 
111
+ You can tune relay selection if needed:
112
+
113
+ ```javascript
114
+ const client = new DiodeClientManager({
115
+ keyLocation: './db/keys.json',
116
+ relaySelection: {
117
+ startupConcurrency: 2,
118
+ minReadyConnections: 2,
119
+ probeTimeoutMs: 1200,
120
+ warmConnectionBudget: 3,
121
+ probeAllInitialCandidates: true,
122
+ continueProbingUntestedSeeds: true,
123
+ regionDiverseSeedOrdering: true,
124
+ discoveryProviderTimeoutMs: 1500,
125
+ backgroundProbeIntervalMs: 300000,
126
+ slowRelayThresholdMs: 250,
127
+ slowDeviceRetryTtlMs: 5000,
128
+ scoreCachePath: './db/relay-scores.json'
129
+ }
130
+ });
131
+ ```
132
+
133
+ 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.
134
+
135
+ 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`.
136
+
137
+ Example discovery provider:
138
+
139
+ ```javascript
140
+ const fs = require('fs/promises');
141
+
142
+ const client = new DiodeClientManager({
143
+ keyLocation: './db/keys.json',
144
+ relaySelection: {
145
+ discoveryProvider: async () => {
146
+ const data = JSON.parse(await fs.readFile('./relays.json', 'utf8'));
147
+ return data;
148
+ },
149
+ networkDiscovery: {
150
+ endpoint: 'wss://prenet.diode.io:8443/ws',
151
+ startupProbeCount: 2,
152
+ backgroundBatchSize: 12
153
+ }
154
+ }
155
+ });
156
+ ```
157
+
158
+ `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:
159
+
160
+ - `defaultPort`
161
+ - `keyLocation`
162
+ - `explicitHost`
163
+ - `explicitHosts`
164
+ - `initialHosts`
165
+ - `knownRelayScores`
166
+
167
+ `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.
168
+
169
+ 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.
170
+
72
171
  If you provide a host, only that relay is used initially (similar to `-diodeaddrs`):
73
172
 
74
173
  ```javascript
@@ -85,25 +184,35 @@ await client.connect();
85
184
  Here's a quick example to get you started with RPC functions using `DiodeRPC` Class
86
185
 
87
186
  ```javascript
88
- const { DiodeConnection, DiodeRPC, makeReadable } = require('diodejs');
187
+ const { DiodeClientManager, DiodeRPC, makeReadable } = require('diodejs');
89
188
 
90
189
  async function main() {
91
190
  const host = 'eu2.prenet.diode.io';
92
191
  const port = 41046;
93
192
  const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
94
193
 
95
- const connection = new DiodeConnection(host, port, keyLocation);
96
-
194
+ const client = new DiodeClientManager({ host, port, keyLocation });
195
+ await client.connect();
196
+
197
+ const connection = client.getConnections()[0];
198
+ if (!connection) {
199
+ throw new Error('No relay connection available');
200
+ }
201
+
97
202
  // Configure reconnection (optional - overrides environment variables)
98
203
  connection.setReconnectOptions({
99
204
  maxRetries: Infinity, // Unlimited reconnection attempts
100
205
  retryDelay: 1000, // Initial delay of 1 second
101
206
  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
207
+ autoReconnect: true // Automatically reconnect on disconnection
105
208
  });
106
-
209
+
210
+ // Configure ticket batching (optional - overrides environment variables)
211
+ connection.setTicketBatchingOptions({
212
+ threshold: 512000, // Bytes threshold for ticket updates
213
+ interval: 30000 // Time interval for ticket updates
214
+ });
215
+
107
216
  // Listen for reconnection events (optional)
108
217
  connection.on('reconnecting', (info) => {
109
218
  console.log(`Reconnecting... Attempt #${info.attempt} in ${info.delay}ms`);
@@ -114,8 +223,6 @@ async function main() {
114
223
  connection.on('reconnect_failed', () => {
115
224
  console.log('Failed to reconnect after maximum attempts');
116
225
  });
117
-
118
- await connection.connect();
119
226
 
120
227
  const rpc = new DiodeRPC(connection);
121
228
 
@@ -130,7 +237,7 @@ async function main() {
130
237
  } catch (error) {
131
238
  console.error('RPC Error:', error);
132
239
  } finally {
133
- connection.close();
240
+ client.close();
134
241
  }
135
242
  }
136
243
 
@@ -143,15 +250,15 @@ Here's a quick example to get you started with port forwarding using the `BindPo
143
250
 
144
251
  #### Port Binding
145
252
  ```javascript
146
- const { DiodeConnection, BindPort } = require('diodejs');
253
+ const { DiodeClientManager, BindPort } = require('diodejs');
147
254
 
148
255
  async function main() {
149
256
  const host = 'eu2.prenet.diode.io';
150
257
  const port = 41046;
151
258
  const keyLocation = './db/keys.json';
152
259
 
153
- const connection = new DiodeConnection(host, port, keyLocation);
154
- await connection.connect();
260
+ const client = new DiodeClientManager({ host, port, keyLocation });
261
+ await client.connect();
155
262
 
156
263
  // Multiple or single port binding with configuration object
157
264
  const portsConfig = {
@@ -168,7 +275,7 @@ async function main() {
168
275
  }
169
276
  };
170
277
 
171
- const portForward = new BindPort(connection, portsConfig);
278
+ const portForward = new BindPort(client, portsConfig);
172
279
  portForward.bind();
173
280
 
174
281
  // You can also dynamically add ports with protocol specification
@@ -181,18 +288,18 @@ main();
181
288
 
182
289
  #### Single Port Binding (Legacy)
183
290
  ```javascript
184
- const { DiodeConnection, BindPort } = require('diodejs');
291
+ const { DiodeClientManager, BindPort } = require('diodejs');
185
292
 
186
293
  async function main() {
187
294
  const host = 'eu2.prenet.diode.io';
188
295
  const port = 41046;
189
296
  const keyLocation = './db/keys.json';
190
297
 
191
- const connection = new DiodeConnection(host, port, keyLocation);
192
- await connection.connect();
298
+ const client = new DiodeClientManager({ host, port, keyLocation });
299
+ await client.connect();
193
300
 
194
301
  // Legacy method - single port binding (defaults to TLS protocol)
195
- const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
302
+ const portForward = new BindPort(client, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
196
303
  portForward.bind();
197
304
  }
198
305
 
@@ -204,30 +311,32 @@ main();
204
311
  Here's a quick example to get you started with publishing ports using the `PublishPort` class:
205
312
 
206
313
  ```javascript
207
- const { DiodeConnection, PublishPort } = require('diodejs');
314
+ const { DiodeClientManager, PublishPort } = require('diodejs');
208
315
 
209
316
  async function main() {
210
317
  const host = 'us2.prenet.diode.io';
211
318
  const port = 41046;
212
319
  const keyLocation = './db/keys.json';
213
320
 
214
- const connection = new DiodeConnection(host, port, keyLocation);
215
- await connection.connect();
321
+ const client = new DiodeClientManager({ host, port, keyLocation });
322
+ await client.connect();
216
323
 
217
324
  // Option 1: Simple array of ports (all public)
218
325
  const publishedPorts = [8080, 3000];
219
326
 
220
327
  // Option 2: Object with port configurations for public/private access control
221
328
  const publishedPortsWithConfig = {
222
- 8080: { mode: 'public' }, // Public port, accessible by any device
329
+ 8080: { mode: 'public' }, // Public port on 127.0.0.1, accessible by any device
330
+ 8081: { mode: 'public', host: '192.168.1.10' }, // Forward to another reachable host
223
331
  3000: {
224
332
  mode: 'private',
333
+ host: 'backend.internal',
225
334
  whitelist: ['0x1234abcd5678...', '0x9876fedc5432...'] // Only these devices can connect
226
335
  }
227
336
  };
228
337
 
229
338
  // certPath parameter is maintained for backward compatibility but not required
230
- const publishPort = new PublishPort(connection, publishedPortsWithConfig);
339
+ const publishPort = new PublishPort(client, publishedPortsWithConfig);
231
340
  }
232
341
 
233
342
  main();
@@ -237,35 +346,6 @@ main();
237
346
 
238
347
  ### Classes and Methods
239
348
 
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
349
  #### `DiodeClientManager`
270
350
 
271
351
  - **Constructor**: `new DiodeClientManager(options)`
@@ -273,18 +353,54 @@ main();
273
353
  - `options.port` (number, optional): Port to use when `options.host` has no port. Defaults to `41046`.
274
354
  - `options.hosts` (string[] or comma-separated string, optional): Explicit relay list.
275
355
  - `options.keyLocation` (string, optional): Key storage path (default: `./db/keys.json`).
356
+ - `options.fleetContract` (string, optional): Fleet contract used for relay tickets. Must be a 20-byte EVM address hex string. Defaults to `0x6000000000000000000000000000000000000000`.
276
357
  - `options.deviceCacheTtlMs` (number, optional): Cache TTL for device relay resolution (default: `30000`).
358
+ - `options.relaySelection` (object, optional): Relay ranking and probing options.
359
+ - `enabled` (boolean, optional): Enables smart relay ranking. Defaults to `true`.
360
+ - `startupConcurrency` (number, optional): Parallel startup probe limit. Defaults to `2`.
361
+ - `minReadyConnections` (number, optional): Minimum target connection count for the startup probe set. Defaults to `2`.
362
+ - `probeTimeoutMs` (number, optional): Ping timeout for relay probes. Defaults to `1200`.
363
+ - `warmConnectionBudget` (number, optional): Maximum number of idle control relays to keep after startup coverage completes. Defaults to `3`.
364
+ - `probeAllInitialCandidates` (boolean, optional): Probes all initial configured/default candidates once before final startup ranking is trusted. Defaults to `true`.
365
+ - `continueProbingUntestedSeeds` (boolean, optional): Continues probing untested candidates after startup coverage completes. Defaults to `true`.
366
+ - `regionDiverseSeedOrdering` (boolean, optional): Interleaves seed regions during first-pass probing and warm retention. Defaults to `true`.
367
+ - `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 }`.
368
+ - `discoveryProviderTimeoutMs` (number, optional): Timeout for `discoveryProvider` results. Defaults to `1500`.
369
+ - `useProviderWithExplicitHost` (boolean, optional): Allows provider candidates when `options.host` is set. Defaults to `false`.
370
+ - `useProviderWithExplicitHosts` (boolean, optional): Allows provider candidates when `options.hosts` is set. Defaults to `false`.
371
+ - `networkDiscovery` (object, optional): Built-in live directory discovery via the JSON-RPC websocket endpoint.
372
+ - `enabled` (boolean, optional): Enables live network discovery when no explicit `host` or `hosts` is configured. Defaults to `true` in default mode.
373
+ - `endpoint` (string, optional): Directory endpoint. Defaults to `wss://prenet.diode.io:8443/ws`.
374
+ - `method` (string, optional): JSON-RPC method name. Defaults to `dio_network`.
375
+ - `timeoutMs` (number, optional): Timeout for the discovery websocket request. Defaults to `1500`.
376
+ - `startupProbeCount` (number, optional): Number of discovered network relays added to the startup coverage probe set. Defaults to `2`.
377
+ - `backgroundBatchSize` (number, optional): Number of additional discovered relays measured in the background queue per run. Defaults to `12`.
378
+ - `includePrivateAddresses` (boolean, optional): Allows private or unroutable discovery results to be used. Defaults to `false`.
379
+ - `backgroundProbeIntervalMs` (number, optional): Minimum interval before background refresh probes are retried for a relay. Defaults to `300000`.
380
+ - `slowRelayThresholdMs` (number, optional): RTT threshold used to shorten target relay cache entries. Defaults to `250`.
381
+ - `slowDeviceRetryTtlMs` (number, optional): TTL used for slow target relays. Defaults to `5000`.
382
+ - `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.
383
+ - `enabled` (boolean, optional): Enables reconciliation. Defaults to `true`.
384
+ - `maxControlRelays` (number, optional): Maximum number of alternate connected control relays queried per reconciliation attempt. Defaults to `2`.
385
+ - `timeoutMs` (number, optional): Per-control-relay timeout for reconciliation lookups. Defaults to `probeTimeoutMs`.
386
+ - `minLatencyDeltaMs` (number, optional): Minimum RTT gap between the target relay and the control relay baseline before reconciliation triggers. Defaults to `150`.
387
+ - `slowdownFactor` (number, optional): Minimum multiple by which the target relay RTT must exceed the control relay baseline before reconciliation triggers. Defaults to `4`.
388
+ - `scoreCachePath` (string|null, optional): Relay score cache file. Defaults to `./db/relay-scores.json` next to `keyLocation`. Set to `null` to disable persistence.
277
389
 
278
390
  - **Methods**:
279
- - `connect()`: Connects to the initial relay pool. Returns a promise.
391
+ - `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.
392
+ - `setFleetContract(address)`: Updates the fleet contract used for future ticket generation on managed connections. Accepts a 20-byte EVM address hex string and returns the manager instance.
393
+ - `getNearestConnection()`: Returns the preferred connected relay. With relay selection enabled, this is the lowest-latency scored relay.
280
394
  - `getConnectionForDevice(deviceId)`: Resolves and returns a relay connection for the device. Returns a promise.
281
395
  - `getConnections()`: Returns a list of active connections.
282
396
  - `close()`: Closes all managed connections.
283
397
 
398
+ Connections returned by `getConnections()` or `getConnectionForDevice()` emit `reconnecting`, `reconnected`, and `reconnect_failed` events, and support `setReconnectOptions(...)` and `setTicketBatchingOptions({ threshold, interval })`.
399
+
284
400
  #### `DiodeRPC`
285
401
 
286
402
  - **Constructor**: `new DiodeRPC(connection)`
287
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
403
+ - `connection` (object): A relay connection from `DiodeClientManager.getConnections()` or `DiodeClientManager.getConnectionForDevice()`.
288
404
 
289
405
  - **Methods**:
290
406
  - `getBlockPeak()`: Retrieves the current block peak. Returns a promise.
@@ -308,14 +424,14 @@ main();
308
424
 
309
425
  Legacy Constructor:
310
426
  - `new BindPort(connection, localPort, targetPort, deviceIdHex)`
311
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
427
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
312
428
  - `localPort` (number): The local port to bind.
313
429
  - `targetPort` (number): The target port on the device.
314
430
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
315
431
 
316
432
  New Constructor:
317
433
  - `new BindPort(connection, portsConfig)`
318
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
434
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
319
435
  - `portsConfig` (object): A configuration object where keys are local ports and values are objects with:
320
436
  - `targetPort` (number): The target port on the device.
321
437
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
@@ -334,16 +450,17 @@ main();
334
450
  #### `PublishPort`
335
451
 
336
452
  - **Constructor**: `new PublishPort(connection, publishedPorts, _certPath)`
337
- - `connection` (DiodeConnection|DiodeClientManager): An instance of `DiodeConnection` or `DiodeClientManager`.
453
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
338
454
  - `publishedPorts` (array|object): Either:
339
455
  - An array of ports to publish (all public mode)
340
- - An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'] } }`
456
+ - An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'], host: '127.0.0.1' } }`
341
457
  - `_certPath` (string): Has no functionality and maintained for backward compatibility.
342
458
 
343
459
  - **Methods**:
344
460
  - `addPort(port, config)`: Adds a new port to publish. Config is optional and defaults to public mode.
345
461
  - `port` (number): The port number to publish.
346
- - `config` (object): Optional configuration with `mode` ('public'|'private') and `whitelist` array.
462
+ - `config` (object): Optional configuration with `mode` ('public'|'private'), `whitelist` array, and `host` string.
463
+ - `host` (string, optional): Target IP or hostname for the published service. Defaults to `127.0.0.1`.
347
464
  - `removePort(port)`: Removes a published port.
348
465
  - `port` (number): The port number to remove.
349
466
  - `addPorts(ports)`: Adds multiple ports at once (equivalent to the constructor's publishedPorts parameter).