@yarkivaev/source-to-sink 1.1.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/source-to-sink",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Generic library for building data streaming pipelines",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,12 +4,35 @@ import pollingSource from './pollingSource.js';
4
4
  /**
5
5
  * Idle state for Modbus connection.
6
6
  *
7
- * @returns {object} State with connected() returning false
7
+ * @returns {object} State with connected() returning false and no retry handle
8
8
  */
9
9
  function idle() {
10
10
  return {
11
11
  connected() {
12
12
  return false;
13
+ },
14
+ retrying() {
15
+ return false;
16
+ }
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Retrying state for Modbus connection.
22
+ *
23
+ * @param {object} handle - Timer handle from setTimeout
24
+ * @returns {object} State with retrying() returning true
25
+ */
26
+ function retrying(handle) {
27
+ return {
28
+ connected() {
29
+ return false;
30
+ },
31
+ retrying() {
32
+ return true;
33
+ },
34
+ cancel() {
35
+ clearTimeout(handle);
13
36
  }
14
37
  };
15
38
  }
@@ -25,6 +48,9 @@ function connected(client) {
25
48
  connected() {
26
49
  return true;
27
50
  },
51
+ retrying() {
52
+ return false;
53
+ },
28
54
  disconnect() {
29
55
  client.close();
30
56
  }
@@ -51,9 +77,10 @@ function connected(client) {
51
77
  * @param {number} interval - Polling interval in seconds
52
78
  * @param {object} collector - Collector with accept() method
53
79
  * @param {object} clk - Clock with millis() method
80
+ * @param {object} [log] - Logger with error() method for connection failures
54
81
  * @returns {object} Source with start() and stop() methods
55
82
  */
56
- export default function modbusSource(host, port, address, count, interval, collector, clk) {
83
+ export default function modbusSource(host, port, address, count, interval, collector, clk, log = console) {
57
84
  if (typeof host !== 'string' || host.length === 0) {
58
85
  throw new Error('Host must be a non-empty string');
59
86
  }
@@ -72,24 +99,40 @@ export default function modbusSource(host, port, address, count, interval, colle
72
99
  const result = await client.readHoldingRegisters(address, count);
73
100
  return [result.data];
74
101
  };
102
+ const maxDelay = 300;
75
103
  const source = pollingSource(fetch, interval, collector, clk);
104
+ let delay = interval;
105
+ const attempt = () => {
106
+ client.connectTCP(host, { port }).then(() => {
107
+ delay = interval;
108
+ state = connected(client);
109
+ source.start();
110
+ }).catch((err) => {
111
+ log.error(`Connection to ${host}:${port} failed, retrying in ${delay}s: ${err.message}`);
112
+ const handle = setTimeout(attempt, delay * 1000);
113
+ state = retrying(handle);
114
+ delay = Math.min(delay * 2, maxDelay);
115
+ });
116
+ };
76
117
  return {
77
118
  /**
78
119
  * Connects to the Modbus device and starts polling.
79
120
  */
80
- async start() {
81
- if (state.connected()) {
121
+ start() {
122
+ if (state.connected() || state.retrying()) {
82
123
  return;
83
124
  }
84
- await client.connectTCP(host, { port });
85
- state = connected(client);
86
- source.start();
125
+ attempt();
87
126
  },
88
127
  /**
89
128
  * Stops polling and disconnects from the Modbus device.
90
129
  */
91
130
  stop() {
92
131
  source.stop();
132
+ if (state.retrying()) {
133
+ state.cancel();
134
+ state = idle();
135
+ }
93
136
  if (state.connected()) {
94
137
  state.disconnect();
95
138
  state = idle();
@@ -93,4 +93,27 @@ describe('modbusSource', () => {
93
93
  source.stop();
94
94
  assert.strictEqual(true, true, 'Should not throw when stopping without start');
95
95
  });
96
+
97
+ it('retries connection on failure', async () => {
98
+ let logged = false;
99
+ const log = { error: () => { logged = true; } };
100
+ const collector = { accept: () => {} };
101
+ const clk = fakeClock(Math.floor(Math.random() * 10000));
102
+ const source = modbusSource(`\u00df${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
103
+ source.start();
104
+ await new Promise(resolve => setTimeout(resolve, 200));
105
+ source.stop();
106
+ assert.strictEqual(logged, true, 'Should log connection failure and retry');
107
+ });
108
+
109
+ it('does not crash process on connection failure', async () => {
110
+ const log = { error: () => {} };
111
+ const collector = { accept: () => {} };
112
+ const clk = fakeClock(Math.floor(Math.random() * 10000));
113
+ const source = modbusSource(`\u0420${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
114
+ source.start();
115
+ await new Promise(resolve => setTimeout(resolve, 200));
116
+ source.stop();
117
+ assert.strictEqual(true, true, 'Should not crash on unreachable host');
118
+ });
96
119
  });