diodejs 0.4.3 → 0.4.4

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/bindPort.js CHANGED
@@ -660,6 +660,46 @@ class BindPort {
660
660
  let rpc = null;
661
661
 
662
662
  let ref;
663
+ let clientClosed = false;
664
+ let remoteCleanupStarted = false;
665
+ let tlsSocketWrapper = null;
666
+
667
+ const closeRemoteRef = async () => {
668
+ if (remoteCleanupStarted) {
669
+ return;
670
+ }
671
+
672
+ if (!ref || !connection || !rpc) {
673
+ return;
674
+ }
675
+ remoteCleanupStarted = true;
676
+
677
+ const storedSocket = typeof connection.getClientSocket === 'function'
678
+ ? connection.getClientSocket(ref)
679
+ : null;
680
+ if (storedSocket && storedSocket !== clientSocket && typeof storedSocket.end === 'function') {
681
+ try { storedSocket.end(); } catch {}
682
+ }
683
+ if (typeof connection.deleteClientSocket === 'function') {
684
+ try { connection.deleteClientSocket(ref); } catch {}
685
+ }
686
+
687
+ try {
688
+ await rpc.portClose(ref);
689
+ logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
690
+ } catch (error) {
691
+ logger.error(() => `Error closing port on device: ${error}`);
692
+ }
693
+ };
694
+
695
+ clientSocket.once('close', () => {
696
+ clientClosed = true;
697
+ closeRemoteRef();
698
+ });
699
+ clientSocket.once('error', (err) => {
700
+ logger.error(() => `Client socket error: ${err}`);
701
+ });
702
+
663
703
  if (useNative) {
664
704
  try {
665
705
  connection = await this._resolveConnectionForDevice(deviceId);
@@ -783,6 +823,12 @@ class BindPort {
783
823
  return;
784
824
  }
785
825
 
826
+ if (clientClosed || clientSocket.destroyed) {
827
+ logger.warn(() => `Local client disconnected before port ${formattedTargetPort} opened; closing ref ${ref.toString('hex')}`);
828
+ await closeRemoteRef();
829
+ return;
830
+ }
831
+
786
832
  if (protocol === 'tls') {
787
833
  // For tls protocol, create a proper tls connection
788
834
  try {
@@ -829,16 +875,27 @@ class BindPort {
829
875
  diodeSocket,
830
876
  tlsSocket,
831
877
  end: () => {
832
- tlsSocket.end();
833
- diodeSocket._destroy(null, () => {});
878
+ try { tlsSocket.end(); } catch {}
879
+ try { diodeSocket._destroy(null, () => {}); } catch {}
834
880
  }
835
881
  };
882
+ tlsSocketWrapper = socketWrapper;
836
883
 
837
884
  // Store the socket wrapper
838
885
  connection.addClientSocket(ref, socketWrapper);
886
+ tlsSocket.once('close', () => {
887
+ if (!clientSocket.destroyed) {
888
+ clientSocket.end();
889
+ }
890
+ closeRemoteRef();
891
+ });
839
892
 
840
893
  } catch (error) {
841
894
  logger.error(() => `Error setting up tls connection: ${error}`);
895
+ if (tlsSocketWrapper && typeof tlsSocketWrapper.end === 'function') {
896
+ tlsSocketWrapper.end();
897
+ }
898
+ await closeRemoteRef();
842
899
  clientSocket.destroy();
843
900
  return;
844
901
  }
@@ -858,24 +915,9 @@ class BindPort {
858
915
  }
859
916
 
860
917
  // Handle client socket closure (common for all protocols)
861
- clientSocket.on('end', async () => {
918
+ clientSocket.once('end', () => {
862
919
  logger.info(() => 'Client disconnected');
863
- if (ref && connection.hasClientSocket(ref)) {
864
- try {
865
- await rpc.portClose(ref);
866
- logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
867
- connection.deleteClientSocket(ref);
868
- } catch (error) {
869
- logger.error(() => `Error closing port on device: ${error}`);
870
- }
871
- } else {
872
- logger.warn(() => 'Ref is invalid or no longer in clientSockets.');
873
- }
874
- });
875
-
876
- // Handle client socket errors
877
- clientSocket.on('error', (err) => {
878
- logger.error(() => `Client socket error: ${err}`);
920
+ closeRemoteRef();
879
921
  });
880
922
  });
881
923
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diodejs",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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": {
@@ -1,6 +1,8 @@
1
1
  const test = require('node:test');
2
2
  const assert = require('node:assert/strict');
3
3
  const EventEmitter = require('events');
4
+ const net = require('net');
5
+ const { once } = require('events');
4
6
 
5
7
  const BindPort = require('../bindPort');
6
8
 
@@ -9,21 +11,61 @@ function makeRef(hex) {
9
11
  }
10
12
 
11
13
  function makeRelay(hostKey, result, calls) {
14
+ const clientSockets = new Map();
15
+ const portCloseCalls = [];
12
16
  return {
13
17
  _managerHostKey: hostKey,
14
18
  socket: { destroyed: false },
19
+ clientSockets,
20
+ portCloseCalls,
15
21
  RPC: {
16
22
  portOpen: async (_deviceId, port, flags) => {
17
23
  calls.push({ hostKey, port, flags });
18
24
  if (result instanceof Error) {
19
25
  throw result;
20
26
  }
21
- return result;
27
+ return typeof result === 'function' ? result() : result;
22
28
  },
29
+ portClose: async (ref) => {
30
+ portCloseCalls.push(ref);
31
+ },
32
+ },
33
+ addClientSocket(ref, socket) {
34
+ clientSockets.set(ref.toString('hex'), socket);
35
+ },
36
+ getClientSocket(ref) {
37
+ return clientSockets.get(ref.toString('hex'));
38
+ },
39
+ deleteClientSocket(ref) {
40
+ return clientSockets.delete(ref.toString('hex'));
41
+ },
42
+ hasClientSocket(ref) {
43
+ return clientSockets.has(ref.toString('hex'));
23
44
  },
24
45
  };
25
46
  }
26
47
 
48
+ function deferred() {
49
+ let resolve;
50
+ let reject;
51
+ const promise = new Promise((res, rej) => {
52
+ resolve = res;
53
+ reject = rej;
54
+ });
55
+ return { promise, resolve, reject };
56
+ }
57
+
58
+ async function waitFor(predicate, timeoutMs = 1000) {
59
+ const startedAt = Date.now();
60
+ while (Date.now() - startedAt < timeoutMs) {
61
+ if (predicate()) {
62
+ return;
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, 10));
65
+ }
66
+ assert.equal(predicate(), true);
67
+ }
68
+
27
69
  class FakeManager extends EventEmitter {
28
70
  constructor({ relays, resolvedRelay, nearestRelay, resolveError = null }) {
29
71
  super();
@@ -123,3 +165,78 @@ test('multiple BindPort instances share one manager listener set', () => {
123
165
  assert.equal(manager.listenerCount('end'), 1);
124
166
  assert.equal(manager.listenerCount('error'), 1);
125
167
  });
168
+
169
+ test('API bind closes ref when local TCP client disconnects before portopen returns', async () => {
170
+ const calls = [];
171
+ const opened = deferred();
172
+ const ref = makeRef('0a0b0c0d');
173
+ const relay = makeRelay('relay.example:41046', () => opened.promise, calls);
174
+ const manager = new FakeManager({
175
+ relays: [relay],
176
+ resolvedRelay: relay,
177
+ nearestRelay: relay,
178
+ });
179
+
180
+ const bind = new BindPort(manager, {
181
+ 0: {
182
+ targetPort: 8088,
183
+ deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
184
+ protocol: 'tcp',
185
+ }
186
+ });
187
+ bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
188
+ const server = bind.servers.get(0);
189
+
190
+ try {
191
+ await once(server, 'listening');
192
+ const client = net.connect(server.address().port, '127.0.0.1');
193
+ await once(client, 'connect');
194
+ client.destroy();
195
+ await once(client, 'close');
196
+
197
+ opened.resolve(ref);
198
+ await waitFor(() => relay.portCloseCalls.length === 1);
199
+
200
+ assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
201
+ assert.equal(relay.hasClientSocket(ref), false);
202
+ } finally {
203
+ server.close();
204
+ }
205
+ });
206
+
207
+ test('API bind closes ref when local TCP client disconnects after portopen returns', async () => {
208
+ const calls = [];
209
+ const ref = makeRef('0e0f1011');
210
+ const relay = makeRelay('relay.example:41046', ref, calls);
211
+ const manager = new FakeManager({
212
+ relays: [relay],
213
+ resolvedRelay: relay,
214
+ nearestRelay: relay,
215
+ });
216
+
217
+ const bind = new BindPort(manager, {
218
+ 0: {
219
+ targetPort: 8088,
220
+ deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
221
+ protocol: 'tcp',
222
+ }
223
+ });
224
+ bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
225
+ const server = bind.servers.get(0);
226
+
227
+ try {
228
+ await once(server, 'listening');
229
+ const client = net.connect(server.address().port, '127.0.0.1');
230
+ await once(client, 'connect');
231
+ await waitFor(() => relay.hasClientSocket(ref));
232
+
233
+ client.end();
234
+ await once(client, 'close');
235
+ await waitFor(() => relay.portCloseCalls.length === 1);
236
+
237
+ assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
238
+ assert.equal(relay.hasClientSocket(ref), false);
239
+ } finally {
240
+ server.close();
241
+ }
242
+ });