njs-modbus 3.2.0 → 3.4.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 +185 -110
- package/README.zh-CN.md +185 -109
- package/dist/index.cjs +1084 -588
- package/dist/index.d.ts +92 -61
- package/dist/index.mjs +1084 -588
- package/dist/utils.cjs +293 -168
- package/dist/utils.d.ts +43 -40
- package/dist/utils.mjs +289 -167
- package/package.json +4 -3
package/dist/utils.cjs
CHANGED
|
@@ -24,40 +24,75 @@ function bitsToMs(baudRate, bits) {
|
|
|
24
24
|
* open / close / destroy callbacks.
|
|
25
25
|
*/
|
|
26
26
|
function drainCbs(cbs, err) {
|
|
27
|
-
if (!cbs)
|
|
27
|
+
if (!cbs) {
|
|
28
28
|
return;
|
|
29
|
+
}
|
|
29
30
|
for (const cb of cbs) {
|
|
30
31
|
cb?.(err);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function inRange(n, [min, max]) {
|
|
35
|
-
return n >= min && n <= max;
|
|
36
|
-
}
|
|
37
|
-
function isRangeArray(range) {
|
|
38
|
-
return Array.isArray(range[0]);
|
|
39
|
-
}
|
|
40
35
|
function checkRange(value, range) {
|
|
41
36
|
if (!range || range.length === 0) {
|
|
42
37
|
return true;
|
|
43
38
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
const isMultiRange = Array.isArray(range[0]);
|
|
40
|
+
const isValueArray = Array.isArray(value);
|
|
41
|
+
if (!isValueArray && !isMultiRange) {
|
|
42
|
+
const r = range;
|
|
43
|
+
const min = r[0], max = r[1];
|
|
44
|
+
const v = value;
|
|
45
|
+
return min <= max ? v >= min && v <= max : v >= max && v <= min;
|
|
46
|
+
}
|
|
47
|
+
if (!isValueArray && isMultiRange) {
|
|
48
|
+
const ranges = range;
|
|
49
|
+
const v = value;
|
|
50
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
51
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
52
|
+
const lo = min <= max ? min : max;
|
|
53
|
+
const hi = min <= max ? max : min;
|
|
54
|
+
if (v >= lo && v <= hi) {
|
|
50
55
|
return true;
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const values = value;
|
|
61
|
+
if (values.length === 0) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!isMultiRange) {
|
|
65
|
+
const r = range;
|
|
66
|
+
const min = r[0], max = r[1];
|
|
67
|
+
const lo = min <= max ? min : max;
|
|
68
|
+
const hi = min <= max ? max : min;
|
|
69
|
+
for (let i = 0; i < values.length; i++) {
|
|
70
|
+
if (values[i] < lo || values[i] > hi) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const ranges = range;
|
|
77
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
78
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
79
|
+
const lo = min <= max ? min : max;
|
|
80
|
+
const hi = min <= max ? max : min;
|
|
81
|
+
let allInRange = true;
|
|
82
|
+
for (let j = 0; j < values.length; j++) {
|
|
83
|
+
if (values[j] < lo || values[j] > hi) {
|
|
84
|
+
allInRange = false;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (allInRange) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
58
93
|
}
|
|
59
94
|
|
|
60
|
-
const
|
|
95
|
+
const CRC_TABLE = new Uint16Array([
|
|
61
96
|
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
62
97
|
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
63
98
|
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
@@ -74,32 +109,38 @@ const TABLE = [
|
|
|
74
109
|
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
75
110
|
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
76
111
|
0x4040,
|
|
77
|
-
];
|
|
78
|
-
|
|
112
|
+
]);
|
|
113
|
+
/** CRC-16 (Modbus) over a single contiguous buffer. */
|
|
114
|
+
function crcFixed(data, start, end) {
|
|
79
115
|
let crc = 0xffff;
|
|
80
116
|
for (let index = start; index < end; index++) {
|
|
81
|
-
crc =
|
|
117
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
82
118
|
}
|
|
83
119
|
return crc;
|
|
84
120
|
}
|
|
85
|
-
|
|
86
121
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
90
|
-
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
91
|
-
* Infinity, and out-of-range values uniformly.
|
|
122
|
+
* CRC-16 (Modbus) over two contiguous buffer segments.
|
|
123
|
+
* Computes CRC(head[headOff:headOff+headLen]) followed by CRC(tail[tailOff:tailOff+tailLen]).
|
|
92
124
|
*/
|
|
93
|
-
function
|
|
94
|
-
|
|
125
|
+
function crcDual(head, headOff, headLen, tail, tailOff, tailLen) {
|
|
126
|
+
let crc = 0xffff;
|
|
127
|
+
const headEnd = headOff + headLen;
|
|
128
|
+
for (let i = headOff; i < headEnd; i++) {
|
|
129
|
+
crc = CRC_TABLE[(crc ^ head[i]) & 0xff] ^ (crc >> 8);
|
|
130
|
+
}
|
|
131
|
+
const tailEnd = tailOff + tailLen;
|
|
132
|
+
for (let i = tailOff; i < tailEnd; i++) {
|
|
133
|
+
crc = CRC_TABLE[(crc ^ tail[i]) & 0xff] ^ (crc >> 8);
|
|
134
|
+
}
|
|
135
|
+
return crc;
|
|
95
136
|
}
|
|
96
137
|
|
|
97
|
-
function lrc(data, start
|
|
138
|
+
function lrc(data, start, end) {
|
|
98
139
|
let sum = 0;
|
|
99
140
|
for (let i = start; i < end; i++) {
|
|
100
141
|
sum += data[i];
|
|
101
142
|
}
|
|
102
|
-
return
|
|
143
|
+
return -sum & 0xff;
|
|
103
144
|
}
|
|
104
145
|
|
|
105
146
|
/**
|
|
@@ -142,103 +183,93 @@ var ConformityLevel;
|
|
|
142
183
|
/** Shared empty Buffer to avoid repeated allocations. */
|
|
143
184
|
Buffer.alloc(0);
|
|
144
185
|
|
|
145
|
-
const REQUEST_FIXED_LENGTHS = {
|
|
146
|
-
[FunctionCode.READ_COILS]: 8,
|
|
147
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
148
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
149
|
-
[FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
150
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
151
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
152
|
-
[FunctionCode.REPORT_SERVER_ID]: 4,
|
|
153
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
154
|
-
[FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
155
|
-
};
|
|
156
|
-
const REQUEST_BYTE_COUNT = {
|
|
157
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
158
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
159
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
160
|
-
};
|
|
161
|
-
const RESPONSE_FIXED_LENGTHS = {
|
|
162
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
163
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
164
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
165
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
166
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
167
|
-
};
|
|
168
|
-
const RESPONSE_BYTE_COUNT = {
|
|
169
|
-
[FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
170
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
171
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
172
|
-
[FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
173
|
-
[FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
174
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
175
|
-
};
|
|
176
|
-
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
177
186
|
const PREDICT_NEED_MORE = 0;
|
|
178
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
179
187
|
const PREDICT_UNKNOWN = -1;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
const REQ_TABLE = new Int32Array(256);
|
|
189
|
+
const RES_TABLE = new Int32Array(256);
|
|
190
|
+
(function initTables() {
|
|
191
|
+
REQ_TABLE[FunctionCode.READ_COILS] = 8;
|
|
192
|
+
REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
193
|
+
REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
194
|
+
REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
195
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
196
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
197
|
+
REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
|
|
198
|
+
REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
199
|
+
REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
200
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
201
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
202
|
+
REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
203
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
204
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
205
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
206
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
207
|
+
RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
208
|
+
RES_TABLE[FunctionCode.READ_COILS] = -517;
|
|
209
|
+
RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
210
|
+
RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
211
|
+
RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
212
|
+
RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
|
|
213
|
+
RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
214
|
+
RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
215
|
+
})();
|
|
216
|
+
function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
|
|
217
|
+
const len = end - start;
|
|
218
|
+
if (len < 2) {
|
|
194
219
|
return PREDICT_NEED_MORE;
|
|
195
220
|
}
|
|
196
|
-
const fc =
|
|
197
|
-
if (isResponse
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
221
|
+
const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
|
|
222
|
+
if (isResponse) {
|
|
223
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
224
|
+
return 5;
|
|
225
|
+
}
|
|
226
|
+
const val = RES_TABLE[fc];
|
|
227
|
+
if (val > 0) {
|
|
228
|
+
return val;
|
|
229
|
+
}
|
|
230
|
+
if (val < 0) {
|
|
231
|
+
if (val === -999) {
|
|
232
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
233
|
+
// the framing hot path (even though this FC is uncommon).
|
|
234
|
+
if (end - start < 8) {
|
|
235
|
+
return PREDICT_NEED_MORE;
|
|
236
|
+
}
|
|
237
|
+
if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
|
|
238
|
+
return PREDICT_UNKNOWN;
|
|
239
|
+
}
|
|
240
|
+
const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
|
|
241
|
+
let cursor = start + 8;
|
|
242
|
+
for (let i = 0; i < numObjs; i++) {
|
|
243
|
+
if (end < cursor + 2) {
|
|
244
|
+
return PREDICT_NEED_MORE;
|
|
245
|
+
}
|
|
246
|
+
cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
|
|
247
|
+
}
|
|
248
|
+
return cursor - start + 2;
|
|
249
|
+
}
|
|
250
|
+
const decode = -val;
|
|
251
|
+
const offset = decode >> 8;
|
|
252
|
+
if (len <= offset) {
|
|
253
|
+
return PREDICT_NEED_MORE;
|
|
254
|
+
}
|
|
255
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
208
256
|
}
|
|
209
|
-
return bc.extra + buffer[start + bc.offset];
|
|
210
|
-
}
|
|
211
|
-
if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
212
|
-
return predictFc43_14Response(buffer, start, end);
|
|
213
|
-
}
|
|
214
|
-
return PREDICT_UNKNOWN;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
|
|
218
|
-
* response structure per Modbus V1.1b3 §6.21.
|
|
219
|
-
*
|
|
220
|
-
* Layout (after unit and fc):
|
|
221
|
-
* mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
|
|
222
|
-
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
223
|
-
* CRC(2)
|
|
224
|
-
*/
|
|
225
|
-
function predictFc43_14Response(buffer, start, end) {
|
|
226
|
-
if (end - start < 8) {
|
|
227
|
-
return PREDICT_NEED_MORE;
|
|
228
|
-
}
|
|
229
|
-
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
230
|
-
return PREDICT_UNKNOWN;
|
|
231
257
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
258
|
+
else {
|
|
259
|
+
const val = REQ_TABLE[fc];
|
|
260
|
+
if (val > 0) {
|
|
261
|
+
return val;
|
|
262
|
+
}
|
|
263
|
+
if (val < 0) {
|
|
264
|
+
const decode = -val;
|
|
265
|
+
const offset = decode >> 8;
|
|
266
|
+
if (len <= offset) {
|
|
267
|
+
return PREDICT_NEED_MORE;
|
|
268
|
+
}
|
|
269
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
237
270
|
}
|
|
238
|
-
const objLen = buffer[cursor + 1];
|
|
239
|
-
cursor += 2 + objLen;
|
|
240
271
|
}
|
|
241
|
-
return
|
|
272
|
+
return PREDICT_UNKNOWN;
|
|
242
273
|
}
|
|
243
274
|
|
|
244
275
|
/**
|
|
@@ -247,10 +278,12 @@ function predictFc43_14Response(buffer, start, end) {
|
|
|
247
278
|
function promisifyCb(fn) {
|
|
248
279
|
return new Promise((resolve, reject) => {
|
|
249
280
|
fn((err) => {
|
|
250
|
-
if (err)
|
|
281
|
+
if (err) {
|
|
251
282
|
reject(err);
|
|
252
|
-
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
253
285
|
resolve();
|
|
286
|
+
}
|
|
254
287
|
});
|
|
255
288
|
});
|
|
256
289
|
}
|
|
@@ -276,7 +309,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
|
|
|
276
309
|
if (baudRate === undefined) {
|
|
277
310
|
return undefined;
|
|
278
311
|
}
|
|
279
|
-
|
|
312
|
+
if (baudRate > 19200) {
|
|
313
|
+
return fastBaudMs;
|
|
314
|
+
}
|
|
315
|
+
const ms = bitsToMs(baudRate, value.value);
|
|
316
|
+
const trunc = ms | 0;
|
|
317
|
+
return trunc + (ms > trunc ? 1 : 0);
|
|
280
318
|
}
|
|
281
319
|
/**
|
|
282
320
|
* Resolve Modbus RTU timing parameters from user options into milliseconds.
|
|
@@ -293,7 +331,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
293
331
|
if (intervalBetweenFrames === undefined) {
|
|
294
332
|
// Spec default: t3.5 derived from baudRate, or 0 when neither option nor
|
|
295
333
|
// baudRate were supplied.
|
|
296
|
-
|
|
334
|
+
if (baudRate === undefined) {
|
|
335
|
+
intervalBetweenFrames = 0;
|
|
336
|
+
}
|
|
337
|
+
else if (baudRate > 19200) {
|
|
338
|
+
intervalBetweenFrames = 1.75;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const ms = bitsToMs(baudRate, 38.5);
|
|
342
|
+
const trunc = ms | 0;
|
|
343
|
+
intervalBetweenFrames = trunc + (ms > trunc ? 1 : 0);
|
|
344
|
+
}
|
|
297
345
|
}
|
|
298
346
|
let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
|
|
299
347
|
if (interCharTimeout === undefined) {
|
|
@@ -303,102 +351,176 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
303
351
|
return { intervalBetweenFrames, interCharTimeout };
|
|
304
352
|
}
|
|
305
353
|
|
|
306
|
-
/**
|
|
307
|
-
*
|
|
354
|
+
/**
|
|
355
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
356
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
357
|
+
*
|
|
358
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
359
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
360
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
361
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
362
|
+
* 10 concurrent: roughly equal
|
|
363
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
364
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
308
365
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
366
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
367
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
368
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
312
369
|
*/
|
|
313
370
|
class TimerHeap {
|
|
314
371
|
_deadlines = [];
|
|
315
372
|
_ids = [];
|
|
373
|
+
_seqs = [];
|
|
374
|
+
_counter = 0;
|
|
316
375
|
_timer = null;
|
|
317
376
|
_onFire;
|
|
318
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
319
377
|
_boundTick;
|
|
320
|
-
|
|
378
|
+
_threshold;
|
|
379
|
+
_mode = 'direct';
|
|
380
|
+
_directTimers = new Map();
|
|
381
|
+
/**
|
|
382
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
383
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
384
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
385
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
386
|
+
*/
|
|
387
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
321
388
|
this._onFire = onFire;
|
|
322
389
|
this._boundTick = this._onTick.bind(this);
|
|
390
|
+
this._threshold = concurrentThreshold;
|
|
323
391
|
}
|
|
324
|
-
/** Number of pending timers in the heap. */
|
|
325
392
|
get size() {
|
|
326
|
-
return this._deadlines.length;
|
|
393
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
327
394
|
}
|
|
328
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
329
395
|
add(id, ms) {
|
|
396
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
397
|
+
const deadline = performance.now() + ms;
|
|
398
|
+
const handle = setTimeout(() => {
|
|
399
|
+
if (this._mode !== 'direct') {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this._directTimers.delete(id);
|
|
403
|
+
this._onFire(id);
|
|
404
|
+
}, ms);
|
|
405
|
+
this._directTimers.set(id, { handle, deadline });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (this._mode === 'direct') {
|
|
409
|
+
this._mode = 'heap';
|
|
410
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
411
|
+
clearTimeout(handle);
|
|
412
|
+
const diff = deadline - performance.now();
|
|
413
|
+
const trunc = diff | 0;
|
|
414
|
+
const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
415
|
+
if (remaining === 0) {
|
|
416
|
+
this._onFire(existingId);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
this._heapAdd(existingId, remaining);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this._directTimers.clear();
|
|
423
|
+
}
|
|
424
|
+
this._heapAdd(id, ms);
|
|
425
|
+
}
|
|
426
|
+
clear() {
|
|
427
|
+
for (const { handle } of this._directTimers.values()) {
|
|
428
|
+
clearTimeout(handle);
|
|
429
|
+
}
|
|
430
|
+
this._directTimers.clear();
|
|
431
|
+
this._mode = 'direct';
|
|
432
|
+
if (this._timer) {
|
|
433
|
+
clearTimeout(this._timer);
|
|
434
|
+
this._timer = null;
|
|
435
|
+
}
|
|
436
|
+
this._deadlines.length = 0;
|
|
437
|
+
this._ids.length = 0;
|
|
438
|
+
this._seqs.length = 0;
|
|
439
|
+
this._counter = 0;
|
|
440
|
+
}
|
|
441
|
+
_heapAdd(id, ms) {
|
|
330
442
|
const deadline = performance.now() + ms;
|
|
443
|
+
const seq = this._counter++;
|
|
331
444
|
let i = this._deadlines.length;
|
|
332
445
|
this._deadlines.push(deadline);
|
|
333
446
|
this._ids.push(id);
|
|
334
|
-
|
|
447
|
+
this._seqs.push(seq);
|
|
335
448
|
while (i > 0) {
|
|
336
449
|
const p = (i - 1) >> 1;
|
|
337
|
-
|
|
450
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
451
|
+
if (parentComesFirst) {
|
|
338
452
|
break;
|
|
453
|
+
}
|
|
339
454
|
this._deadlines[i] = this._deadlines[p];
|
|
340
455
|
this._ids[i] = this._ids[p];
|
|
456
|
+
this._seqs[i] = this._seqs[p];
|
|
341
457
|
i = p;
|
|
342
458
|
}
|
|
343
459
|
this._deadlines[i] = deadline;
|
|
344
460
|
this._ids[i] = id;
|
|
345
|
-
|
|
346
|
-
if (i === 0)
|
|
461
|
+
this._seqs[i] = seq;
|
|
462
|
+
if (i === 0) {
|
|
347
463
|
this._refresh();
|
|
348
|
-
}
|
|
349
|
-
/** Dispose without firing callbacks. */
|
|
350
|
-
clear() {
|
|
351
|
-
if (this._timer) {
|
|
352
|
-
clearTimeout(this._timer);
|
|
353
|
-
this._timer = null;
|
|
354
464
|
}
|
|
355
|
-
this._deadlines.length = 0;
|
|
356
|
-
this._ids.length = 0;
|
|
357
465
|
}
|
|
358
466
|
_refresh() {
|
|
359
467
|
if (this._timer) {
|
|
360
468
|
clearTimeout(this._timer);
|
|
361
469
|
this._timer = null;
|
|
362
470
|
}
|
|
363
|
-
if (this._deadlines.length === 0)
|
|
471
|
+
if (this._deadlines.length === 0) {
|
|
364
472
|
return;
|
|
365
|
-
|
|
366
|
-
|
|
473
|
+
}
|
|
474
|
+
const diff = this._deadlines[0] - performance.now();
|
|
475
|
+
const trunc = diff | 0;
|
|
476
|
+
const delay = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
477
|
+
const safeDelay = delay < 2147483647 ? delay : 2147483647;
|
|
478
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
367
479
|
}
|
|
368
480
|
_onTick() {
|
|
369
481
|
this._timer = null;
|
|
370
482
|
const now = performance.now();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
483
|
+
try {
|
|
484
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
485
|
+
const id = this._pop();
|
|
486
|
+
this._onFire(id);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
this._refresh();
|
|
374
491
|
}
|
|
375
|
-
this._refresh();
|
|
376
492
|
}
|
|
377
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
378
493
|
_pop() {
|
|
379
494
|
const topId = this._ids[0];
|
|
380
495
|
const lastId = this._ids.pop();
|
|
381
496
|
const lastDeadline = this._deadlines.pop();
|
|
497
|
+
const lastSeq = this._seqs.pop();
|
|
382
498
|
const n = this._deadlines.length;
|
|
383
499
|
if (n > 0) {
|
|
384
500
|
let i = 0;
|
|
385
|
-
|
|
386
|
-
while (
|
|
387
|
-
let
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
501
|
+
const half = n >> 1;
|
|
502
|
+
while (i < half) {
|
|
503
|
+
let minChild = (i << 1) + 1;
|
|
504
|
+
const rightChild = minChild + 1;
|
|
505
|
+
if (rightChild < n) {
|
|
506
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
507
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
508
|
+
if (rightComesFirst) {
|
|
509
|
+
minChild = rightChild;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
513
|
+
if (lastComesFirst) {
|
|
395
514
|
break;
|
|
396
|
-
|
|
397
|
-
this.
|
|
398
|
-
i =
|
|
515
|
+
}
|
|
516
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
517
|
+
this._ids[i] = this._ids[minChild];
|
|
518
|
+
this._seqs[i] = this._seqs[minChild];
|
|
519
|
+
i = minChild;
|
|
399
520
|
}
|
|
400
521
|
this._deadlines[i] = lastDeadline;
|
|
401
522
|
this._ids[i] = lastId;
|
|
523
|
+
this._seqs[i] = lastSeq;
|
|
402
524
|
}
|
|
403
525
|
return topId;
|
|
404
526
|
}
|
|
@@ -424,14 +546,17 @@ function isWhitelisted(address, whitelist) {
|
|
|
424
546
|
return whitelist.includes(normalized);
|
|
425
547
|
}
|
|
426
548
|
|
|
549
|
+
exports.CRC_TABLE = CRC_TABLE;
|
|
427
550
|
exports.PREDICT_NEED_MORE = PREDICT_NEED_MORE;
|
|
428
551
|
exports.PREDICT_UNKNOWN = PREDICT_UNKNOWN;
|
|
552
|
+
exports.REQ_TABLE = REQ_TABLE;
|
|
553
|
+
exports.RES_TABLE = RES_TABLE;
|
|
429
554
|
exports.TimerHeap = TimerHeap;
|
|
430
555
|
exports.bitsToMs = bitsToMs;
|
|
431
556
|
exports.checkRange = checkRange;
|
|
432
|
-
exports.
|
|
557
|
+
exports.crcDual = crcDual;
|
|
558
|
+
exports.crcFixed = crcFixed;
|
|
433
559
|
exports.drainCbs = drainCbs;
|
|
434
|
-
exports.isUint8 = isUint8;
|
|
435
560
|
exports.isWhitelisted = isWhitelisted;
|
|
436
561
|
exports.lrc = lrc;
|
|
437
562
|
exports.predictRtuFrameLength = predictRtuFrameLength;
|