@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 +1 -1
- package/src/modbusSource.js +37 -14
- package/src/pollingSource.js +8 -5
- package/test/test_modbusSource.js +43 -0
- package/test/test_pollingSource.js +20 -0
package/package.json
CHANGED
package/src/modbusSource.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
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
|
}
|
package/src/pollingSource.js
CHANGED
|
@@ -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
|
-
|
|
67
|
+
function poll() {
|
|
68
68
|
const from = since;
|
|
69
69
|
const until = clk.millis();
|
|
70
70
|
since = until;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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);
|