diodejs 0.3.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,16 +38,124 @@ 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
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();
65
+ ```
66
+
67
+ ### Multi-Relay Connections (Recommended)
68
+
69
+ You can connect to multiple Diode relays and automatically route binds to the relay where the target device is connected.
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
+
75
+ ```javascript
76
+ const { DiodeClientManager, BindPort } = require('diodejs');
77
+
78
+ async function main() {
79
+ // Connect to default relay pool (pre-net defaults)
80
+ const client = new DiodeClientManager({ keyLocation: './db/keys.json' });
81
+ await client.connect();
82
+
83
+ const bind = new BindPort(client, {
84
+ 3003: { targetPort: 8080, deviceIdHex: '0x...', protocol: 'tcp' }
85
+ });
86
+ bind.bind();
87
+ }
88
+ ```
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
+
150
+ If you provide a host, only that relay is used initially (similar to `-diodeaddrs`):
151
+
152
+ ```javascript
153
+ const client = new DiodeClientManager({
154
+ host: 'us2.prenet.diode.io',
155
+ port: 41046,
156
+ keyLocation: './db/keys.json'
50
157
  });
158
+ await client.connect();
51
159
  ```
52
160
 
53
161
  ### Test RPC
@@ -55,25 +163,35 @@ connection.setReconnectOptions({
55
163
  Here's a quick example to get you started with RPC functions using `DiodeRPC` Class
56
164
 
57
165
  ```javascript
58
- const { DiodeConnection, DiodeRPC, makeReadable } = require('diodejs');
166
+ const { DiodeClientManager, DiodeRPC, makeReadable } = require('diodejs');
59
167
 
60
168
  async function main() {
61
169
  const host = 'eu2.prenet.diode.io';
62
170
  const port = 41046;
63
171
  const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
64
172
 
65
- const connection = new DiodeConnection(host, port, keyLocation);
66
-
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
+
67
181
  // Configure reconnection (optional - overrides environment variables)
68
182
  connection.setReconnectOptions({
69
183
  maxRetries: Infinity, // Unlimited reconnection attempts
70
184
  retryDelay: 1000, // Initial delay of 1 second
71
185
  maxRetryDelay: 30000, // Maximum delay of 30 seconds
72
- autoReconnect: true, // Automatically reconnect on disconnection
73
- ticketBytesThreshold: 512000, // Bytes threshold for ticket updates
74
- ticketUpdateInterval: 30000 // Time interval for ticket updates
186
+ autoReconnect: true // Automatically reconnect on disconnection
75
187
  });
76
-
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
+
77
195
  // Listen for reconnection events (optional)
78
196
  connection.on('reconnecting', (info) => {
79
197
  console.log(`Reconnecting... Attempt #${info.attempt} in ${info.delay}ms`);
@@ -84,8 +202,6 @@ async function main() {
84
202
  connection.on('reconnect_failed', () => {
85
203
  console.log('Failed to reconnect after maximum attempts');
86
204
  });
87
-
88
- await connection.connect();
89
205
 
90
206
  const rpc = new DiodeRPC(connection);
91
207
 
@@ -100,7 +216,7 @@ async function main() {
100
216
  } catch (error) {
101
217
  console.error('RPC Error:', error);
102
218
  } finally {
103
- connection.close();
219
+ client.close();
104
220
  }
105
221
  }
106
222
 
@@ -113,15 +229,15 @@ Here's a quick example to get you started with port forwarding using the `BindPo
113
229
 
114
230
  #### Port Binding
115
231
  ```javascript
116
- const { DiodeConnection, BindPort } = require('diodejs');
232
+ const { DiodeClientManager, BindPort } = require('diodejs');
117
233
 
118
234
  async function main() {
119
235
  const host = 'eu2.prenet.diode.io';
120
236
  const port = 41046;
121
237
  const keyLocation = './db/keys.json';
122
238
 
123
- const connection = new DiodeConnection(host, port, keyLocation);
124
- await connection.connect();
239
+ const client = new DiodeClientManager({ host, port, keyLocation });
240
+ await client.connect();
125
241
 
126
242
  // Multiple or single port binding with configuration object
127
243
  const portsConfig = {
@@ -133,11 +249,12 @@ async function main() {
133
249
  3003: {
134
250
  targetPort: 443,
135
251
  deviceIdHex: "0x5365baf29cb7ab58de588dfc448913cb609283e2",
136
- protocol: "tcp" // Can be "tls", "tcp", or "udp"
252
+ protocol: "tcp", // Can be "tls", "tcp", or "udp"
253
+ transport: "native" // Optional - "api" (default) or "native" for portopen2 (tcp/udp only)
137
254
  }
138
255
  };
139
256
 
140
- const portForward = new BindPort(connection, portsConfig);
257
+ const portForward = new BindPort(client, portsConfig);
141
258
  portForward.bind();
142
259
 
143
260
  // You can also dynamically add ports with protocol specification
@@ -150,18 +267,18 @@ main();
150
267
 
151
268
  #### Single Port Binding (Legacy)
152
269
  ```javascript
153
- const { DiodeConnection, BindPort } = require('diodejs');
270
+ const { DiodeClientManager, BindPort } = require('diodejs');
154
271
 
155
272
  async function main() {
156
273
  const host = 'eu2.prenet.diode.io';
157
274
  const port = 41046;
158
275
  const keyLocation = './db/keys.json';
159
276
 
160
- const connection = new DiodeConnection(host, port, keyLocation);
161
- await connection.connect();
277
+ const client = new DiodeClientManager({ host, port, keyLocation });
278
+ await client.connect();
162
279
 
163
280
  // Legacy method - single port binding (defaults to TLS protocol)
164
- const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
281
+ const portForward = new BindPort(client, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
165
282
  portForward.bind();
166
283
  }
167
284
 
@@ -173,15 +290,15 @@ main();
173
290
  Here's a quick example to get you started with publishing ports using the `PublishPort` class:
174
291
 
175
292
  ```javascript
176
- const { DiodeConnection, PublishPort } = require('diodejs');
293
+ const { DiodeClientManager, PublishPort } = require('diodejs');
177
294
 
178
295
  async function main() {
179
296
  const host = 'us2.prenet.diode.io';
180
297
  const port = 41046;
181
298
  const keyLocation = './db/keys.json';
182
299
 
183
- const connection = new DiodeConnection(host, port, keyLocation);
184
- await connection.connect();
300
+ const client = new DiodeClientManager({ host, port, keyLocation });
301
+ await client.connect();
185
302
 
186
303
  // Option 1: Simple array of ports (all public)
187
304
  const publishedPorts = [8080, 3000];
@@ -196,7 +313,7 @@ async function main() {
196
313
  };
197
314
 
198
315
  // certPath parameter is maintained for backward compatibility but not required
199
- const publishPort = new PublishPort(connection, publishedPortsWithConfig);
316
+ const publishPort = new PublishPort(client, publishedPortsWithConfig);
200
317
  }
201
318
 
202
319
  main();
@@ -206,46 +323,69 @@ main();
206
323
 
207
324
  ### Classes and Methods
208
325
 
209
- #### `DiodeConnection`
210
-
211
- - **Constructor**: `new DiodeConnection(host, port, keyLocation)`
212
- - `host` (string): The host address of the Diode server.
213
- - `port` (number): The port number of the Diode server.
214
- - `keyLocation` (string)(default: './db/keys.json'): The path to the key storage file. If the file doesn't exist, keys are generated automatically.
326
+ #### `DiodeClientManager`
327
+
328
+ - **Constructor**: `new DiodeClientManager(options)`
329
+ - `options.host` (string, optional): Single relay host (with or without port). If provided, only this relay is used initially.
330
+ - `options.port` (number, optional): Port to use when `options.host` has no port. Defaults to `41046`.
331
+ - `options.hosts` (string[] or comma-separated string, optional): Explicit relay list.
332
+ - `options.keyLocation` (string, optional): Key storage path (default: `./db/keys.json`).
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.
215
365
 
216
366
  - **Methods**:
217
- - `connect()`: Connects to the Diode server. Returns a promise.
218
- - `sendCommand(commandArray)`: Sends a command to the Diode server. Returns a promise.
219
- - `sendCommandWithSessionId(commandArray, sessionId)`: Sends a command with a session ID. Returns a promise.
220
- - `getEthereumAddress()`: Returns the Ethereum address derived from the device keys.
221
- - `getServerEthereumAddress()`: Returns the Ethereum address of the server.
222
- - `createTicketCommand()`: Creates a ticket command for authentication. Returns a promise.
223
- - `close()`: Closes the connection to the Diode server.
224
- - `getDeviceCertificate()`: Returns the generated certificate PEM.
225
- - `setReconnectOptions(options)`: Configures reconnection behavior with the following options:
226
- - `maxRetries` (number): Maximum reconnection attempts (default: Infinity)
227
- - `retryDelay` (number): Initial delay between retries in ms (default: 1000)
228
- - `maxRetryDelay` (number): Maximum delay between retries in ms (default: 30000)
229
- - `autoReconnect` (boolean): Whether to automatically reconnect on disconnection (default: true)
230
- - `ticketBytesThreshold` (number): Bytes threshold for ticket updates (default: 512000)
231
- - `ticketUpdateInterval` (number): Time interval for ticket updates in ms (default: 30000)
232
-
233
- - **Events**:
234
- - `reconnecting`: Emitted when a reconnection attempt is about to start, with `attempt` and `delay` information
235
- - `reconnected`: Emitted when reconnection is successful
236
- - `reconnect_failed`: Emitted when all reconnection attempts have failed
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.
369
+ - `getConnectionForDevice(deviceId)`: Resolves and returns a relay connection for the device. Returns a promise.
370
+ - `getConnections()`: Returns a list of active connections.
371
+ - `close()`: Closes all managed connections.
372
+
373
+ Connections returned by `getConnections()` or `getConnectionForDevice()` emit `reconnecting`, `reconnected`, and `reconnect_failed` events, and support `setReconnectOptions(...)` and `setTicketBatchingOptions({ threshold, interval })`.
237
374
 
238
375
  #### `DiodeRPC`
239
376
 
240
377
  - **Constructor**: `new DiodeRPC(connection)`
241
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
378
+ - `connection` (object): A relay connection from `DiodeClientManager.getConnections()` or `DiodeClientManager.getConnectionForDevice()`.
242
379
 
243
380
  - **Methods**:
244
381
  - `getBlockPeak()`: Retrieves the current block peak. Returns a promise.
245
382
  - `getBlockHeader(index)`: Retrieves the block header for a given index. Returns a promise.
246
383
  - `getBlock(index)`: Retrieves the block for a given index. Returns a promise.
384
+ - `getObject(deviceId)`: Retrieves a device ticket object. Returns a promise.
385
+ - `getNode(nodeId)`: Retrieves relay node information. Returns a promise.
247
386
  - `ping()`: Sends a ping command. Returns a promise.
248
387
  - `portOpen(deviceId, port, flags)`: Opens a port on the device. Returns a promise.
388
+ - `portOpen2(deviceId, port, flags)`: Opens a native relay port on the device (TCP/UDP). Returns the server relay port.
249
389
  - `portSend(ref, data)`: Sends data to the device. Returns a promise.
250
390
  - `portClose(ref)`: Closes a port on the device. Returns a promise.
251
391
  - `sendError(sessionId, ref, error)`: Sends an error response. Returns a promise.
@@ -259,23 +399,25 @@ main();
259
399
 
260
400
  Legacy Constructor:
261
401
  - `new BindPort(connection, localPort, targetPort, deviceIdHex)`
262
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
402
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
263
403
  - `localPort` (number): The local port to bind.
264
404
  - `targetPort` (number): The target port on the device.
265
405
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
266
406
 
267
407
  New Constructor:
268
408
  - `new BindPort(connection, portsConfig)`
269
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
409
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
270
410
  - `portsConfig` (object): A configuration object where keys are local ports and values are objects with:
271
411
  - `targetPort` (number): The target port on the device.
272
412
  - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
273
413
  - `protocol` (string, optional): The protocol to use ("tls", "tcp", or "udp"). Defaults to "tls".
414
+ - `transport` (string, optional): The relay transport to use ("api" or "native"). Defaults to "api". Native uses `portopen2` and supports TCP/UDP only.
274
415
 
275
416
  - **Methods**:
276
417
  - `bind()`: Binds all configured local ports to their target ports on the devices.
277
- - `addPort(localPort, targetPort, deviceIdHex, protocol)`: Adds a new port binding configuration.
418
+ - `addPort(localPort, targetPort, deviceIdHex, protocol, transport)`: Adds a new port binding configuration.
278
419
  - `protocol` (string, optional): The protocol to use. Can be "tls", "tcp", or "udp". Defaults to "tls".
420
+ - `transport` (string, optional): The relay transport to use ("api" or "native"). Defaults to "api".
279
421
  - `removePort(localPort)`: Removes a port binding configuration.
280
422
  - `bindSinglePort(localPort)`: Binds a single local port to its target.
281
423
  - `closeAllServers()`: Closes all active server instances.
@@ -283,7 +425,7 @@ main();
283
425
  #### `PublishPort`
284
426
 
285
427
  - **Constructor**: `new PublishPort(connection, publishedPorts, _certPath)`
286
- - `connection` (DiodeConnection): An instance of `DiodeConnection`.
428
+ - `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
287
429
  - `publishedPorts` (array|object): Either:
288
430
  - An array of ports to publish (all public mode)
289
431
  - An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'] } }`