diodejs 0.1.5 → 0.2.0

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
@@ -15,6 +15,35 @@ If you want to enable debug logs, set environment variable DEBUG to true.
15
15
 
16
16
  Can also use .env files
17
17
 
18
+ ### Connection Settings
19
+
20
+ Connection retry behavior can be configured via environment variables:
21
+
22
+ | Environment Variable | Description | Default |
23
+ |----------------------|-------------|---------|
24
+ | DIODE_MAX_RETRIES | Maximum number of reconnection attempts | Infinity |
25
+ | DIODE_RETRY_DELAY | Initial delay between retries (ms) | 1000 |
26
+ | DIODE_MAX_RETRY_DELAY | Maximum delay between retries (ms) | 30000 |
27
+ | DIODE_AUTO_RECONNECT | Whether to automatically reconnect | true |
28
+
29
+ Example `.env` file:
30
+ ```
31
+ DIODE_MAX_RETRIES=10
32
+ DIODE_RETRY_DELAY=2000
33
+ DIODE_MAX_RETRY_DELAY=20000
34
+ DIODE_AUTO_RECONNECT=true
35
+ ```
36
+
37
+ These settings can also be configured programmatically:
38
+ ```javascript
39
+ connection.setReconnectOptions({
40
+ maxRetries: 10,
41
+ retryDelay: 2000,
42
+ maxRetryDelay: 20000,
43
+ autoReconnect: true
44
+ });
45
+ ```
46
+
18
47
  ### Test RPC
19
48
 
20
49
  Here's a quick example to get you started with RPC functions using `DiodeRPC` Class
@@ -28,6 +57,26 @@ async function main() {
28
57
  const keyLocation = './db/keys.json'; // Optional, defaults to './db/keys.json'
29
58
 
30
59
  const connection = new DiodeConnection(host, port, keyLocation);
60
+
61
+ // Configure reconnection (optional - overrides environment variables)
62
+ connection.setReconnectOptions({
63
+ maxRetries: Infinity, // Unlimited reconnection attempts
64
+ retryDelay: 1000, // Initial delay of 1 second
65
+ maxRetryDelay: 30000, // Maximum delay of 30 seconds
66
+ autoReconnect: true // Automatically reconnect on disconnection
67
+ });
68
+
69
+ // Listen for reconnection events (optional)
70
+ connection.on('reconnecting', (info) => {
71
+ console.log(`Reconnecting... Attempt #${info.attempt} in ${info.delay}ms`);
72
+ });
73
+ connection.on('reconnected', () => {
74
+ console.log('Successfully reconnected!');
75
+ });
76
+ connection.on('reconnect_failed', () => {
77
+ console.log('Failed to reconnect after maximum attempts');
78
+ });
79
+
31
80
  await connection.connect();
32
81
 
33
82
  const rpc = new DiodeRPC(connection);
@@ -68,15 +117,23 @@ async function main() {
68
117
 
69
118
  // Multiple or single port binding with configuration object
70
119
  const portsConfig = {
71
- 3002: { targetPort: 80, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" },
72
- 3003: { targetPort: 443, deviceIdHex: "0x5365baf29cb7ab58de588dfc448913cb609283e2" } // Works with or without 0x prefix
120
+ 3002: {
121
+ targetPort: 80,
122
+ deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2",
123
+ protocol: "tls" // Optional - defaults to TLS if not specified
124
+ },
125
+ 3003: {
126
+ targetPort: 443,
127
+ deviceIdHex: "0x5365baf29cb7ab58de588dfc448913cb609283e2",
128
+ protocol: "tcp" // Can be "tls", "tcp", or "udp"
129
+ }
73
130
  };
74
131
 
75
132
  const portForward = new BindPort(connection, portsConfig);
76
133
  portForward.bind();
77
134
 
78
- // You can also dynamically add and remove ports
79
- portForward.addPort(3004, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
135
+ // You can also dynamically add ports with protocol specification
136
+ portForward.addPort(3004, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2", "udp");
80
137
  portForward.removePort(3003);
81
138
  }
82
139
 
@@ -95,7 +152,7 @@ async function main() {
95
152
  const connection = new DiodeConnection(host, port, keyLocation);
96
153
  await connection.connect();
97
154
 
98
- // Legacy method - single port binding
155
+ // Legacy method - single port binding (defaults to TLS protocol)
99
156
  const portForward = new BindPort(connection, 3002, 80, "5365baf29cb7ab58de588dfc448913cb609283e2");
100
157
  portForward.bind();
101
158
  }
@@ -157,6 +214,16 @@ main();
157
214
  - `createTicketCommand()`: Creates a ticket command for authentication. Returns a promise.
158
215
  - `close()`: Closes the connection to the Diode server.
159
216
  - `getDeviceCertificate()`: Returns the generated certificate PEM.
217
+ - `setReconnectOptions(options)`: Configures reconnection behavior with the following options:
218
+ - `maxRetries` (number): Maximum reconnection attempts (default: Infinity)
219
+ - `retryDelay` (number): Initial delay between retries in ms (default: 1000)
220
+ - `maxRetryDelay` (number): Maximum delay between retries in ms (default: 30000)
221
+ - `autoReconnect` (boolean): Whether to automatically reconnect on disconnection (default: true)
222
+
223
+ - **Events**:
224
+ - `reconnecting`: Emitted when a reconnection attempt is about to start, with `attempt` and `delay` information
225
+ - `reconnected`: Emitted when reconnection is successful
226
+ - `reconnect_failed`: Emitted when all reconnection attempts have failed
160
227
 
161
228
  #### `DiodeRPC`
162
229
 
@@ -190,13 +257,15 @@ main();
190
257
  New Constructor:
191
258
  - `new BindPort(connection, portsConfig)`
192
259
  - `connection` (DiodeConnection): An instance of `DiodeConnection`.
193
- - `portsConfig` (object): A configuration object where keys are local ports and values are objects with `targetPort` and `deviceIdHex`.
194
- Example: `{ 3002: { targetPort: 80, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" } }`
195
- Note: deviceIdHex can be provided with or without the '0x' prefix.
260
+ - `portsConfig` (object): A configuration object where keys are local ports and values are objects with:
261
+ - `targetPort` (number): The target port on the device.
262
+ - `deviceIdHex` (string): The device ID in hexadecimal format (with or without '0x' prefix).
263
+ - `protocol` (string, optional): The protocol to use ("tls", "tcp", or "udp"). Defaults to "tls".
196
264
 
197
265
  - **Methods**:
198
266
  - `bind()`: Binds all configured local ports to their target ports on the devices.
199
- - `addPort(localPort, targetPort, deviceIdHex)`: Adds a new port binding configuration. deviceIdHex can include '0x' prefix.
267
+ - `addPort(localPort, targetPort, deviceIdHex, protocol)`: Adds a new port binding configuration.
268
+ - `protocol` (string, optional): The protocol to use. Can be "tls", "tcp", or "udp". Defaults to "tls".
200
269
  - `removePort(localPort)`: Removes a port binding configuration.
201
270
  - `bindSinglePort(localPort)`: Binds a single local port to its target.
202
271
  - `closeAllServers()`: Closes all active server instances.
package/bindPort.js CHANGED
@@ -1,8 +1,45 @@
1
1
  const net = require('net');
2
+ const tls = require('tls');
3
+ const dgram = require('dgram');
2
4
  const { Buffer } = require('buffer');
5
+ const { Duplex } = require('stream');
3
6
  const DiodeRPC = require('./rpc');
4
7
  const logger = require('./logger');
5
8
 
9
+ // Custom Duplex stream to handle the Diode connection
10
+ class DiodeSocket extends Duplex {
11
+ constructor(ref, rpc) {
12
+ super();
13
+ this.ref = ref;
14
+ this.rpc = rpc;
15
+ this.destroyed = false;
16
+ }
17
+
18
+ _write(chunk, encoding, callback) {
19
+ // Send data to the remote device via portSend
20
+ this.rpc.portSend(this.ref, chunk)
21
+ .then(() => callback())
22
+ .catch((err) => callback(err));
23
+ }
24
+
25
+ _read(size) {
26
+ // No need to implement this method for our use case
27
+ }
28
+
29
+ // Method to push data received from the remote device
30
+ pushData(data) {
31
+ if (!this.destroyed) {
32
+ this.push(data);
33
+ }
34
+ }
35
+
36
+ _destroy(err, callback) {
37
+ this.destroyed = true;
38
+ this.push(null);
39
+ callback(err);
40
+ }
41
+ }
42
+
6
43
  class BindPort {
7
44
  constructor(connection, localPortOrPortsConfig, targetPort, deviceIdHex) {
8
45
  this.connection = connection;
@@ -12,7 +49,8 @@ class BindPort {
12
49
  this.portsConfig = {
13
50
  [localPortOrPortsConfig]: {
14
51
  targetPort,
15
- deviceIdHex: this._stripHexPrefix(deviceIdHex)
52
+ deviceIdHex: this._stripHexPrefix(deviceIdHex),
53
+ protocol: 'tls' // Default protocol is tls
16
54
  }
17
55
  };
18
56
  } else {
@@ -20,10 +58,17 @@ class BindPort {
20
58
  this.portsConfig = localPortOrPortsConfig || {};
21
59
 
22
60
  // Strip 0x prefix from all deviceIdHex values in portsConfig
61
+ // And ensure protocol is specified (default to tls)
23
62
  for (const port in this.portsConfig) {
24
63
  if (this.portsConfig[port].deviceIdHex) {
25
64
  this.portsConfig[port].deviceIdHex = this._stripHexPrefix(this.portsConfig[port].deviceIdHex);
26
65
  }
66
+ // Set default protocol if not provided
67
+ if (!this.portsConfig[port].protocol) {
68
+ this.portsConfig[port].protocol = 'tls';
69
+ }
70
+ // Ensure protocol is uppercase
71
+ this.portsConfig[port].protocol = this.portsConfig[port].protocol.toLowerCase();
27
72
  }
28
73
  }
29
74
 
@@ -45,7 +90,6 @@ class BindPort {
45
90
  _setupMessageListener() {
46
91
  // Listen for data events from the device
47
92
  this.connection.on('unsolicited', (message) => {
48
- // message is [messageId, [messageType, ...]]
49
93
  const [messageIdRaw, messageContent] = message;
50
94
  const messageTypeRaw = messageContent[0];
51
95
  const messageType = Buffer.from(messageTypeRaw).toString('utf8');
@@ -60,7 +104,13 @@ class BindPort {
60
104
  // Find the associated client socket from connection
61
105
  const clientSocket = this.connection.getClientSocket(dataRef);
62
106
  if (clientSocket) {
63
- clientSocket.write(data);
107
+ if (clientSocket.diodeSocket) {
108
+ // If it's a DiodeSocket, push data to it so tls can process
109
+ clientSocket.diodeSocket.pushData(data);
110
+ } else {
111
+ // Otherwise write directly to the socket
112
+ clientSocket.write(data);
113
+ }
64
114
  } else {
65
115
  const connectionInfo = this.connection.getConnection(dataRef);
66
116
  if (connectionInfo) {
@@ -76,6 +126,9 @@ class BindPort {
76
126
  // Close the associated client socket
77
127
  const clientSocket = this.connection.getClientSocket(dataRef);
78
128
  if (clientSocket) {
129
+ if (clientSocket.diodeSocket) {
130
+ clientSocket.diodeSocket._destroy(null, () => {});
131
+ }
79
132
  clientSocket.end();
80
133
  this.connection.deleteClientSocket(dataRef);
81
134
  logger.info(`Port closed for ref: ${dataRef.toString('hex')}`);
@@ -100,7 +153,7 @@ class BindPort {
100
153
  });
101
154
  }
102
155
 
103
- addPort(localPort, targetPort, deviceIdHex) {
156
+ addPort(localPort, targetPort, deviceIdHex, protocol = 'tls') {
104
157
  if (this.servers.has(localPort)) {
105
158
  logger.warn(`Port ${localPort} is already bound`);
106
159
  return false;
@@ -108,7 +161,8 @@ class BindPort {
108
161
 
109
162
  this.portsConfig[localPort] = {
110
163
  targetPort,
111
- deviceIdHex: this._stripHexPrefix(deviceIdHex)
164
+ deviceIdHex: this._stripHexPrefix(deviceIdHex),
165
+ protocol: protocol.toLowerCase()
112
166
  };
113
167
 
114
168
  this.bindSinglePort(localPort);
@@ -151,70 +205,191 @@ class BindPort {
151
205
  return false;
152
206
  }
153
207
 
154
- const { targetPort, deviceIdHex } = config;
208
+ const { targetPort, deviceIdHex, protocol = 'tls' } = config;
155
209
  const deviceId = Buffer.from(deviceIdHex, 'hex');
156
210
 
157
- // Set up local server
158
- const server = net.createServer(async (clientSocket) => {
159
- logger.info(`Client connected to local server on port ${localPort}`);
160
-
161
- // Open a new port on the device for this client
162
- let ref;
163
- try {
164
- ref = await this.rpc.portOpen(deviceId, targetPort, 'rw');
211
+ // Format the target port with protocol prefix for the remote connection
212
+ const formattedTargetPort = `${protocol}:${targetPort}`;
213
+ logger.info(`Binding local port ${localPort} to remote ${formattedTargetPort}`);
214
+
215
+ // For udp protocol, use udp server
216
+ if (protocol === 'udp') {
217
+ const server = dgram.createSocket('udp4');
218
+
219
+ server.on('listening', () => {
220
+ logger.info(`udp server listening on port ${localPort} forwarding to device port ${targetPort}`);
221
+ });
222
+
223
+ server.on('message', async (data, rinfo) => {
224
+ // Open a new port on the device if this is a new client
225
+ const clientKey = `${rinfo.address}:${rinfo.port}`;
226
+ let ref = server.clientRefs && server.clientRefs[clientKey];
227
+
165
228
  if (!ref) {
166
- logger.error(`Error opening port ${targetPort} on deviceId: ${deviceIdHex}`);
167
- clientSocket.destroy();
168
- return;
169
- } else {
170
- logger.info(`Port ${targetPort} opened on device with ref: ${ref.toString('hex')} for client`);
229
+ try {
230
+ ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
231
+ if (!ref) {
232
+ logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
233
+ return;
234
+ } else {
235
+ logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for udp client ${clientKey}`);
236
+ if (!server.clientRefs) server.clientRefs = {};
237
+ server.clientRefs[clientKey] = ref;
238
+
239
+ // Store the client info
240
+ this.connection.addClientSocket(ref, {
241
+ address: rinfo.address,
242
+ port: rinfo.port,
243
+ protocol: 'udp',
244
+ write: (data) => {
245
+ server.send(data, rinfo.port, rinfo.address);
246
+ }
247
+ });
248
+ }
249
+ } catch (error) {
250
+ logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
251
+ return;
252
+ }
171
253
  }
172
- } catch (error) {
173
- logger.error(`Error opening port ${targetPort} on device: ${error}`);
174
- clientSocket.destroy();
175
- return;
176
- }
177
-
178
- // Store the client socket with the ref using connection's method
179
- this.connection.addClientSocket(ref, clientSocket);
180
-
181
- // When data is received from the client, send it to the device
182
- clientSocket.on('data', async (data) => {
254
+
255
+ // Send data to the device
183
256
  try {
184
257
  await this.rpc.portSend(ref, data);
185
258
  } catch (error) {
186
- logger.error(`Error sending data to device: ${error}`);
187
- clientSocket.destroy();
259
+ logger.error(`Error sending udp data to device: ${error}`);
188
260
  }
189
261
  });
262
+
263
+ server.on('error', (err) => {
264
+ logger.error(`udp Server error: ${err}`);
265
+ });
266
+
267
+ server.bind(localPort);
268
+ this.servers.set(parseInt(localPort), server);
269
+ } else {
270
+ // For TCP and tls protocols, use TCP server locally
271
+ const server = net.createServer(async (clientSocket) => {
272
+ logger.info(`Client connected to local server on port ${localPort}`);
190
273
 
191
- // Handle client socket closure
192
- clientSocket.on('end', async () => {
193
- logger.info('Client disconnected');
194
- if (ref && this.connection.hasClientSocket(ref)) {
274
+ // Open a new port on the device for this client
275
+ let ref;
276
+ try {
277
+ ref = await this.rpc.portOpen(deviceId, formattedTargetPort, 'rw');
278
+ if (!ref) {
279
+ logger.error(`Error opening port ${formattedTargetPort} on deviceId: ${deviceIdHex}`);
280
+ clientSocket.destroy();
281
+ return;
282
+ } else {
283
+ logger.info(`Port ${formattedTargetPort} opened on device with ref: ${ref.toString('hex')} for client`);
284
+ }
285
+ } catch (error) {
286
+ logger.error(`Error opening port ${formattedTargetPort} on device: ${error}`);
287
+ clientSocket.destroy();
288
+ return;
289
+ }
290
+
291
+ if (protocol === 'tls') {
292
+ // For tls protocol, create a proper tls connection
195
293
  try {
196
- await this.rpc.portClose(ref);
197
- logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
198
- this.connection.deleteClientSocket(ref);
294
+ // Create a DiodeSocket to handle communication with the device
295
+ const diodeSocket = new DiodeSocket(ref, this.rpc);
296
+
297
+ // Get the device certificate for tls
298
+ const certPem = this.connection.getDeviceCertificate();
299
+ if (!certPem) {
300
+ throw new Error('No device certificate available');
301
+ }
302
+
303
+ // Setup tls options for client mode
304
+ const tlsOptions = {
305
+ cert: certPem,
306
+ key: certPem,
307
+ rejectUnauthorized: false,
308
+ ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
309
+ ecdhCurve: 'secp256k1',
310
+ minVersion: 'TLSv1.2',
311
+ maxVersion: 'TLSv1.2',
312
+ };
313
+
314
+ // Create tls socket as client (not server)
315
+ const tlsSocket = tls.connect({
316
+ socket: diodeSocket,
317
+ ...tlsOptions
318
+ }, () => {
319
+ logger.info(`tls connection established to device ${deviceIdHex}`);
320
+ });
321
+
322
+ // Pipe data between the client socket and the tls socket
323
+ tlsSocket.pipe(clientSocket).pipe(tlsSocket);
324
+
325
+ // Handle tls socket errors
326
+ tlsSocket.on('error', (err) => {
327
+ logger.error(`tls Socket error: ${err}`);
328
+ clientSocket.destroy();
329
+ });
330
+
331
+ // Store reference to the diodeSocket so we can push data to it
332
+ const socketWrapper = {
333
+ diodeSocket,
334
+ tlsSocket,
335
+ end: () => {
336
+ tlsSocket.end();
337
+ diodeSocket._destroy(null, () => {});
338
+ }
339
+ };
340
+
341
+ // Store the socket wrapper
342
+ this.connection.addClientSocket(ref, socketWrapper);
343
+
199
344
  } catch (error) {
200
- logger.error(`Error closing port on device: ${error}`);
345
+ logger.error(`Error setting up tls connection: ${error}`);
346
+ clientSocket.destroy();
347
+ return;
201
348
  }
202
349
  } else {
203
- logger.warn('Ref is invalid or no longer in clientSockets.');
350
+ // For TCP protocol, just use the raw socket
351
+ this.connection.addClientSocket(ref, clientSocket);
352
+
353
+ // Handle data from client to device
354
+ clientSocket.on('data', async (data) => {
355
+ try {
356
+ await this.rpc.portSend(ref, data);
357
+ } catch (error) {
358
+ logger.error(`Error sending data to device: ${error}`);
359
+ clientSocket.destroy();
360
+ }
361
+ });
204
362
  }
205
- });
206
363
 
207
- // Handle client socket errors
208
- clientSocket.on('error', (err) => {
209
- logger.error('Client socket error:', err);
364
+ // Handle client socket closure (common for all protocols)
365
+ clientSocket.on('end', async () => {
366
+ logger.info('Client disconnected');
367
+ if (ref && this.connection.hasClientSocket(ref)) {
368
+ try {
369
+ await this.rpc.portClose(ref);
370
+ logger.info(`Port closed on device for ref: ${ref.toString('hex')}`);
371
+ this.connection.deleteClientSocket(ref);
372
+ } catch (error) {
373
+ logger.error(`Error closing port on device: ${error}`);
374
+ }
375
+ } else {
376
+ logger.warn('Ref is invalid or no longer in clientSockets.');
377
+ }
378
+ });
379
+
380
+ // Handle client socket errors
381
+ clientSocket.on('error', (err) => {
382
+ logger.error(`Client socket error: ${err}`);
383
+ });
210
384
  });
211
- });
212
385
 
213
- server.listen(localPort, () => {
214
- logger.info(`Local server listening on port ${localPort} forwarding to device port ${targetPort}`);
215
- });
386
+ server.listen(localPort, () => {
387
+ logger.info(`Local server listening on port ${localPort} forwarding to device ${protocol} port ${targetPort}`);
388
+ });
389
+
390
+ this.servers.set(parseInt(localPort), server);
391
+ }
216
392
 
217
- this.servers.set(parseInt(localPort), server);
218
393
  return true;
219
394
  }
220
395
 
package/connection.js CHANGED
@@ -13,6 +13,8 @@ const DiodeRPC = require('./rpc');
13
13
  const abi = require('ethereumjs-abi');
14
14
  const logger = require('./logger');
15
15
  const path = require('path');
16
+ // Add dotenv for environment variables
17
+ require('dotenv').config();
16
18
 
17
19
  class DiodeConnection extends EventEmitter {
18
20
  constructor(host, port, keyLocation = './db/keys.json') {
@@ -37,9 +39,38 @@ class DiodeConnection extends EventEmitter {
37
39
  this.certPem = null;
38
40
  // Load or generate keypair
39
41
  this.keyPair = loadOrGenerateKeyPair(this.keyLocation);
42
+
43
+ // Load reconnection properties from environment variables with defaults
44
+ const envMaxRetries = process.env.DIODE_MAX_RETRIES;
45
+ this.maxRetries = envMaxRetries !== undefined ?
46
+ (envMaxRetries.toLowerCase() === 'infinity' ? Infinity : parseInt(envMaxRetries, 10)) :
47
+ Infinity;
48
+
49
+ this.retryDelay = parseInt(process.env.DIODE_RETRY_DELAY, 10) || 1000; // Default: 1 second
50
+ this.maxRetryDelay = parseInt(process.env.DIODE_MAX_RETRY_DELAY, 10) || 30000; // Default: 30 seconds
51
+
52
+ // Parse boolean from string ('true'/'false')
53
+ const envAutoReconnect = process.env.DIODE_AUTO_RECONNECT;
54
+ this.autoReconnect = envAutoReconnect !== undefined ?
55
+ (envAutoReconnect.toLowerCase() === 'true') :
56
+ true;
57
+
58
+ this.retryCount = 0;
59
+ this.retryTimeoutId = null;
60
+
61
+ // Log the reconnection settings
62
+ logger.info(`Connection settings - Auto Reconnect: ${this.autoReconnect}, Max Retries: ${
63
+ this.maxRetries === Infinity ? 'Infinity' : this.maxRetries
64
+ }, Retry Delay: ${this.retryDelay}ms, Max Retry Delay: ${this.maxRetryDelay}ms`);
40
65
  }
41
66
 
42
67
  connect() {
68
+ // Clear any existing retry timeout
69
+ if (this.retryTimeoutId) {
70
+ clearTimeout(this.retryTimeoutId);
71
+ this.retryTimeoutId = null;
72
+ }
73
+
43
74
  return new Promise((resolve, reject) => {
44
75
  // Generate a temporary certificate valid for 1 month
45
76
  this.certPem = generateCert(this.keyPair.prvKeyObj, this.keyPair.pubKeyObj);
@@ -56,8 +87,10 @@ class DiodeConnection extends EventEmitter {
56
87
 
57
88
  this.socket = tls.connect(this.port, this.host, options, async () => {
58
89
  logger.info('Connected to Diode.io server');
90
+ // Reset retry counter on successful connection
91
+ this.retryCount = 0;
59
92
  // Set keep-alive to prevent connection timeout forever
60
- this.socket.setKeepAlive(true, 0);
93
+ this.socket.setKeepAlive(true, 1500);
61
94
 
62
95
  // Send the ticketv2 command
63
96
  try {
@@ -79,14 +112,87 @@ class DiodeConnection extends EventEmitter {
79
112
  logger.error(`Error handling data: ${error}`);
80
113
  }
81
114
  });
115
+
82
116
  this.socket.on('error', (err) => {
83
117
  logger.error(`Connection error: ${err}`);
84
- reject(err);
118
+ reject(err);
119
+ });
120
+
121
+ this.socket.on('end', () => {
122
+ logger.info('Disconnected from server');
123
+ this._handleDisconnect();
124
+ });
125
+
126
+ this.socket.on('close', (hadError) => {
127
+ logger.info(`Connection closed${hadError ? ' due to error' : ''}`);
128
+ this._handleDisconnect();
129
+ });
130
+
131
+ this.socket.on('timeout', () => {
132
+ logger.warn('Connection timeout');
133
+ this._handleDisconnect();
85
134
  });
86
- this.socket.on('end', () => logger.info('Disconnected from server'));
87
135
  });
88
136
  }
89
137
 
138
+ // New method to handle reconnection with exponential backoff
139
+ _reconnect() {
140
+ if (!this.autoReconnect || this.isReconnecting) return;
141
+
142
+ this.retryCount++;
143
+
144
+ if (this.maxRetries !== Infinity && this.retryCount > this.maxRetries) {
145
+ logger.error(`Maximum reconnection attempts (${this.maxRetries}) reached. Giving up.`);
146
+ this.emit('reconnect_failed');
147
+ return;
148
+ }
149
+
150
+ // Calculate delay with exponential backoff
151
+ const delay = Math.min(this.retryDelay * Math.pow(1.5, this.retryCount - 1), this.maxRetryDelay);
152
+
153
+ logger.info(`Reconnecting in ${delay}ms... (Attempt ${this.retryCount})`);
154
+ this.emit('reconnecting', { attempt: this.retryCount, delay });
155
+
156
+ this.retryTimeoutId = setTimeout(() => {
157
+ this.isReconnecting = true;
158
+
159
+ // Clear existing socket if any
160
+ if (this.socket) {
161
+ this.socket.removeAllListeners();
162
+ if (!this.socket.destroyed) {
163
+ this.socket.destroy();
164
+ }
165
+ this.socket = null;
166
+ }
167
+
168
+ // Connect again
169
+ this.connect()
170
+ .then(() => {
171
+ this.isReconnecting = false;
172
+ this.emit('reconnected');
173
+ logger.info('Successfully reconnected to Diode.io server');
174
+ })
175
+ .catch((err) => {
176
+ this.isReconnecting = false;
177
+ logger.error(`Reconnection attempt failed: ${err}`);
178
+ });
179
+ }, delay);
180
+ }
181
+
182
+ // Helper method to handle disconnection events
183
+ _handleDisconnect() {
184
+ // Reset socket to null to ensure we don't try to use it
185
+ if (this.socket && !this.socket.destroyed) {
186
+ this.socket.destroy();
187
+ }
188
+ this.socket = null;
189
+
190
+ // Don't try to reconnect if we're intentionally closing
191
+ if (this.autoReconnect && !this.isReconnecting) {
192
+ this._reconnect();
193
+ }
194
+ }
195
+
90
196
  _ensureConnected() {
91
197
  if (this.socket && !this.socket.destroyed) {
92
198
  return Promise.resolve();
@@ -94,20 +200,58 @@ class DiodeConnection extends EventEmitter {
94
200
  if (this.connectPromise) {
95
201
  return this.connectPromise;
96
202
  }
97
- this.isReconnecting = true;
98
- this.connectPromise = this.connect()
99
- .then(() => {
100
- this.isReconnecting = false;
101
- this.connectPromise = null;
102
- })
103
- .catch((err) => {
104
- this.isReconnecting = false;
105
- this.connectPromise = null;
106
- throw err;
107
- });
203
+
204
+ this.connectPromise = new Promise((resolve, reject) => {
205
+
206
+ if (this.isReconnecting) {
207
+ // If we're already reconnecting, wait for the reconnection to complete
208
+ this.once('reconnected', resolve);
209
+ //wait for max retry delay
210
+ setTimeout(() => {
211
+ reject(new Error('Reconnection timed out'));
212
+ }, this.maxRetryDelay);
213
+ } else {
214
+ this._reconnect();
215
+ this.once('reconnected', resolve);
216
+ //wait for max retry delay
217
+ setTimeout(() => {
218
+ reject(new Error('Reconnection timed out'));
219
+ }, this.maxRetryDelay);
220
+ }
221
+ });
222
+
108
223
  return this.connectPromise;
109
224
  }
110
225
 
226
+ // Method to set reconnection options
227
+ setReconnectOptions(options = {}) {
228
+ if (typeof options.maxRetries === 'number') {
229
+ this.maxRetries = options.maxRetries;
230
+ }
231
+ if (typeof options.retryDelay === 'number') {
232
+ this.retryDelay = options.retryDelay;
233
+ }
234
+ if (typeof options.maxRetryDelay === 'number') {
235
+ this.maxRetryDelay = options.maxRetryDelay;
236
+ }
237
+ if (typeof options.autoReconnect === 'boolean') {
238
+ this.autoReconnect = options.autoReconnect;
239
+ }
240
+ return this;
241
+ }
242
+
243
+ // Update close method to prevent reconnection when intentionally closing
244
+ close() {
245
+ this.autoReconnect = false;
246
+ if (this.retryTimeoutId) {
247
+ clearTimeout(this.retryTimeoutId);
248
+ this.retryTimeoutId = null;
249
+ }
250
+ if (this.socket) {
251
+ this.socket.end();
252
+ }
253
+ }
254
+
111
255
  _handleData(data) {
112
256
  // Append new data to the receive buffer
113
257
  this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]);
@@ -428,10 +572,6 @@ class DiodeConnection extends EventEmitter {
428
572
  return this.requestId;
429
573
  }
430
574
 
431
- close() {
432
- this.socket.end();
433
- }
434
-
435
575
  // Client sockets management methods (for BindPort)
436
576
  addClientSocket(ref, socket) {
437
577
  this.clientSockets.set(ref.toString('hex'), socket);
@@ -9,8 +9,8 @@ async function main() {
9
9
  await connection.connect();
10
10
 
11
11
  const portForward = new BindPort(connection, {
12
- 3003: { targetPort: 8080, deviceIdHex: "0xca1e71d8105a598810578fb6042fa8cbc1e7f039" },
13
- 3004: { targetPort: 443, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2" }
12
+ 3003: { targetPort: 8080, deviceIdHex: "0xca1e71d8105a598810578fb6042fa8cbc1e7f039", protocol: "tcp" },
13
+ 3004: { targetPort: 8081, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2", protocol: "tls" }
14
14
  });
15
15
  portForward.bind();
16
16
 
@@ -23,7 +23,7 @@ async function main() {
23
23
  // after 10 seconds, add port 3003 back
24
24
  setTimeout(() => {
25
25
  console.log("Adding port 3003 back");
26
- portForward.addPort(3003, 8080, "0xca1e71d8105a598810578fb6042fa8cbc1e7f039");
26
+ portForward.addPort(3003, 8080, "0xca1e71d8105a598810578fb6042fa8cbc1e7f039", "tcp");
27
27
  }, 10000);
28
28
 
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diodejs",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/publishPort.js CHANGED
@@ -254,7 +254,7 @@ class PublishPort extends EventEmitter {
254
254
  // Create a DiodeSocket instance
255
255
  const diodeSocket = new DiodeSocket(ref, this.rpc);
256
256
 
257
- certPem = this.connection.getDeviceCertificate();
257
+ const certPem = this.connection.getDeviceCertificate();
258
258
 
259
259
  // TLS options with your server's certificate and key
260
260
  const tlsOptions = {