@yarkivaev/source-to-sink 1.3.6 → 1.3.7

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.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "Generic library for building data streaming pipelines",
5
5
  "repository": {
6
6
  "type": "git",
@@ -85,6 +85,28 @@ function connected(client) {
85
85
  };
86
86
  }
87
87
 
88
+ /**
89
+ * Destroys a Modbus client without throwing if already closed.
90
+ *
91
+ * @param {object} client - ModbusRTU client
92
+ */
93
+ function drop(client) {
94
+ if (client && typeof client.destroy === 'function') {
95
+ client.destroy(() => {
96
+ return undefined;
97
+ });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Builds a ModbusRTU client for one connect attempt.
103
+ *
104
+ * @returns {object} Fresh ModbusRTU client
105
+ */
106
+ function create() {
107
+ return new ModbusRTU();
108
+ }
109
+
88
110
  /**
89
111
  * Starts one connect attempt and ignores results after stop().
90
112
  *
@@ -93,9 +115,11 @@ function connected(client) {
93
115
  function attempt(bag) {
94
116
  const token = bag.epoch;
95
117
  bag.state = opening();
118
+ drop(bag.client);
119
+ bag.client = bag.create();
96
120
  bag.connect(bag.client).then(() => {
97
121
  if (token !== bag.epoch) {
98
- bag.client.close();
122
+ drop(bag.client);
99
123
  return;
100
124
  }
101
125
  bag.delay = bag.interval;
@@ -105,6 +129,7 @@ function attempt(bag) {
105
129
  if (token !== bag.epoch) {
106
130
  return;
107
131
  }
132
+ drop(bag.client);
108
133
  bag.log.error(`Connection to ${bag.target} failed, retrying in ${bag.delay}s: ${err.message}`);
109
134
  const handle = setTimeout(() => {
110
135
  attempt(bag);
@@ -121,19 +146,20 @@ function attempt(bag) {
121
146
  * @returns {object} Source with start() and stop() methods
122
147
  */
123
148
  function modbusPollingSource(options) {
124
- const { address, count, interval, collector, clk, client } = options;
125
- async function fetch() {
126
- const result = await client.readHoldingRegisters(address, count);
127
- return [result.data];
128
- }
149
+ const { address, count, interval, collector, clk } = options;
129
150
  const bag = {
130
151
  ...options,
152
+ client: undefined,
131
153
  state: idle(),
132
154
  epoch: 0,
133
155
  maxDelay: 300,
134
- source: pollingSource(fetch, interval, collector, clk),
135
156
  delay: interval
136
157
  };
158
+ async function fetch() {
159
+ const result = await bag.client.readHoldingRegisters(address, count);
160
+ return [result.data];
161
+ }
162
+ bag.source = pollingSource(fetch, interval, collector, clk);
137
163
  return {
138
164
  /**
139
165
  * Connects to the Modbus device and starts polling.
@@ -153,9 +179,8 @@ function modbusPollingSource(options) {
153
179
  if (bag.state.retrying()) {
154
180
  bag.state.cancel();
155
181
  }
156
- if (bag.state.connected() || bag.state.opening()) {
157
- bag.client.close();
158
- }
182
+ drop(bag.client);
183
+ bag.client = undefined;
159
184
  bag.state = idle();
160
185
  }
161
186
  };
@@ -195,11 +220,11 @@ export default function modbusSource(host, port, address, count, interval, colle
195
220
  if (typeof count !== 'number' || count <= 0) {
196
221
  throw new Error(`Count must be a positive number, got: ${count}`);
197
222
  }
198
- const client = new ModbusRTU();
199
223
  function connect(modbusClient) {
200
224
  return modbusClient.connectTCP(host, { port });
201
225
  }
202
226
  return modbusPollingSource({
227
+ create,
203
228
  connect,
204
229
  target: `${host}:${port}`,
205
230
  address,
@@ -207,7 +232,6 @@ export default function modbusSource(host, port, address, count, interval, colle
207
232
  interval,
208
233
  collector,
209
234
  clk,
210
- client,
211
235
  log
212
236
  });
213
237
  }
@@ -248,7 +272,6 @@ export function modbusRtuSource(path, serial, address, count, interval, collecto
248
272
  if (typeof count !== 'number' || count <= 0) {
249
273
  throw new Error(`Count must be a positive number, got: ${count}`);
250
274
  }
251
- const client = new ModbusRTU();
252
275
  const slaveId = serial.slaveId || 1;
253
276
  const dataBits = serial.dataBits || 8;
254
277
  const stopBits = serial.stopBits || 1;
@@ -264,6 +287,7 @@ export function modbusRtuSource(path, serial, address, count, interval, collecto
264
287
  });
265
288
  }
266
289
  return modbusPollingSource({
290
+ create,
267
291
  connect,
268
292
  target: path,
269
293
  address,
@@ -271,7 +295,6 @@ export function modbusRtuSource(path, serial, address, count, interval, collecto
271
295
  interval,
272
296
  collector,
273
297
  clk,
274
- client,
275
298
  log
276
299
  });
277
300
  }
@@ -64,14 +64,17 @@ export default function pollingSource(fetch, interval, collector, clk) {
64
64
  }
65
65
  let state = idle();
66
66
  let since = clk.millis();
67
- async function poll() {
67
+ function poll() {
68
68
  const from = since;
69
69
  const until = clk.millis();
70
70
  since = until;
71
- const result = await fetch(from, until);
72
- for (const entry of result) {
73
- collector.accept(entry);
74
- }
71
+ Promise.resolve(fetch(from, until)).then((result) => {
72
+ for (const entry of result) {
73
+ collector.accept(entry);
74
+ }
75
+ }).catch(() => {
76
+ return undefined;
77
+ });
75
78
  }
76
79
  return {
77
80
  /**
@@ -59,6 +59,27 @@ function settle(ms) {
59
59
  });
60
60
  }
61
61
 
62
+ /**
63
+ * Collects unhandledRejection events until stop().
64
+ *
65
+ * @returns {object} Collector with seen() and stop()
66
+ */
67
+ function watchRejects() {
68
+ const seen = [];
69
+ function onReject(err) {
70
+ seen.push(err);
71
+ }
72
+ process.on('unhandledRejection', onReject);
73
+ return {
74
+ seen() {
75
+ return seen.length;
76
+ },
77
+ stop() {
78
+ process.off('unhandledRejection', onReject);
79
+ }
80
+ };
81
+ }
82
+
62
83
  /**
63
84
  * Waits until probe is true or the timeout elapses.
64
85
  *
@@ -200,6 +221,28 @@ describe('modbusSource', () => {
200
221
  assert.strictEqual(true, true, 'Should not crash on unreachable host');
201
222
  });
202
223
 
224
+ it('does not emit unhandled rejection after connection retry', async function() {
225
+ this.timeout(4000);
226
+ const watch = watchRejects();
227
+ const log = { error() {} };
228
+ const port = 65000 + Math.floor(Math.random() * 500);
229
+ const source = modbusSource(
230
+ '127.0.0.1',
231
+ port,
232
+ 4000,
233
+ 14,
234
+ 0.05,
235
+ { accept() {} },
236
+ fakeClock(1 + Math.floor(Math.random() * 100)),
237
+ log
238
+ );
239
+ source.start();
240
+ await settle(400);
241
+ source.stop();
242
+ watch.stop();
243
+ assert.strictEqual(watch.seen(), 0, 'retry leaked an unhandled rejection');
244
+ });
245
+
203
246
  it('does not open a second TCP socket when start is called twice', async function() {
204
247
  const gate = await countingListen();
205
248
  const source = modbusSource(
@@ -91,6 +91,26 @@ describe('pollingSource', () => {
91
91
  assert.strictEqual(count, before, 'Should stop polling after stop');
92
92
  });
93
93
 
94
+ it('does not emit unhandled rejection when fetch rejects', async () => {
95
+ const seen = [];
96
+ function onReject(err) {
97
+ seen.push(err);
98
+ }
99
+ process.on('unhandledRejection', onReject);
100
+ const label = `\u00e9${Math.random()}`;
101
+ const fetch = () => {
102
+ return Promise.reject(new Error(label));
103
+ };
104
+ const source = pollingSource(fetch, 0.03, { accept() {} }, fakeClock(1 + Math.floor(Math.random() * 100)));
105
+ source.start();
106
+ await new Promise((resolve) => {
107
+ setTimeout(resolve, 80);
108
+ });
109
+ source.stop();
110
+ process.off('unhandledRejection', onReject);
111
+ assert.strictEqual(seen.length, 0, 'poll rejection leaked as unhandled');
112
+ });
113
+
94
114
  it('throws on missing fetch', () => {
95
115
  const collector = { accept: () => {} };
96
116
  const clk = fakeClock(0);