njs-modbus 3.2.0 → 3.3.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 +64 -35
- package/README.zh-CN.md +64 -35
- package/dist/index.cjs +536 -236
- package/dist/index.d.ts +12 -6
- package/dist/index.mjs +536 -236
- package/dist/utils.cjs +251 -154
- package/dist/utils.d.ts +29 -27
- package/dist/utils.mjs +251 -154
- package/package.json +8 -1
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 TABLE = [
|
|
95
|
+
const 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,11 +109,11 @@ 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
|
-
function crc(data, start
|
|
112
|
+
]);
|
|
113
|
+
function crc(data, start, end) {
|
|
79
114
|
let crc = 0xffff;
|
|
80
115
|
for (let index = start; index < end; index++) {
|
|
81
|
-
crc =
|
|
116
|
+
crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
82
117
|
}
|
|
83
118
|
return crc;
|
|
84
119
|
}
|
|
@@ -91,15 +126,15 @@ function crc(data, start = 0, end = data.length) {
|
|
|
91
126
|
* Infinity, and out-of-range values uniformly.
|
|
92
127
|
*/
|
|
93
128
|
function isUint8(n) {
|
|
94
|
-
return
|
|
129
|
+
return (n & 0xff) === n;
|
|
95
130
|
}
|
|
96
131
|
|
|
97
|
-
function lrc(data, start
|
|
132
|
+
function lrc(data, start, end) {
|
|
98
133
|
let sum = 0;
|
|
99
134
|
for (let i = start; i < end; i++) {
|
|
100
135
|
sum += data[i];
|
|
101
136
|
}
|
|
102
|
-
return
|
|
137
|
+
return -sum & 0xff;
|
|
103
138
|
}
|
|
104
139
|
|
|
105
140
|
/**
|
|
@@ -142,103 +177,93 @@ var ConformityLevel;
|
|
|
142
177
|
/** Shared empty Buffer to avoid repeated allocations. */
|
|
143
178
|
Buffer.alloc(0);
|
|
144
179
|
|
|
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
180
|
const PREDICT_NEED_MORE = 0;
|
|
178
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
179
181
|
const PREDICT_UNKNOWN = -1;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
182
|
+
const REQ_TABLE = new Int32Array(256);
|
|
183
|
+
const RES_TABLE = new Int32Array(256);
|
|
184
|
+
(function initTables() {
|
|
185
|
+
REQ_TABLE[FunctionCode.READ_COILS] = 8;
|
|
186
|
+
REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
187
|
+
REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
188
|
+
REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
189
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
190
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
191
|
+
REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
|
|
192
|
+
REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
193
|
+
REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
194
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
195
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
196
|
+
REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
197
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
198
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
199
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
200
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
201
|
+
RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
202
|
+
RES_TABLE[FunctionCode.READ_COILS] = -517;
|
|
203
|
+
RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
204
|
+
RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
205
|
+
RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
206
|
+
RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
|
|
207
|
+
RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
208
|
+
RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
209
|
+
})();
|
|
192
210
|
function predictRtuFrameLength(buffer, start, end, isResponse) {
|
|
193
|
-
|
|
211
|
+
const len = end - start;
|
|
212
|
+
if (len < 2) {
|
|
194
213
|
return PREDICT_NEED_MORE;
|
|
195
214
|
}
|
|
196
215
|
const fc = buffer[start + 1];
|
|
197
|
-
if (isResponse
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
if (isResponse) {
|
|
217
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
218
|
+
return 5;
|
|
219
|
+
}
|
|
220
|
+
const val = RES_TABLE[fc];
|
|
221
|
+
if (val > 0) {
|
|
222
|
+
return val;
|
|
223
|
+
}
|
|
224
|
+
if (val < 0) {
|
|
225
|
+
if (val === -999) {
|
|
226
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
227
|
+
// the framing hot path (even though this FC is uncommon).
|
|
228
|
+
if (end - start < 8) {
|
|
229
|
+
return PREDICT_NEED_MORE;
|
|
230
|
+
}
|
|
231
|
+
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
232
|
+
return PREDICT_UNKNOWN;
|
|
233
|
+
}
|
|
234
|
+
const numObjs = buffer[start + 7];
|
|
235
|
+
let cursor = start + 8;
|
|
236
|
+
for (let i = 0; i < numObjs; i++) {
|
|
237
|
+
if (end < cursor + 2) {
|
|
238
|
+
return PREDICT_NEED_MORE;
|
|
239
|
+
}
|
|
240
|
+
cursor += 2 + buffer[cursor + 1];
|
|
241
|
+
}
|
|
242
|
+
return cursor - start + 2;
|
|
243
|
+
}
|
|
244
|
+
const decode = -val;
|
|
245
|
+
const offset = decode >> 8;
|
|
246
|
+
if (len <= offset) {
|
|
247
|
+
return PREDICT_NEED_MORE;
|
|
248
|
+
}
|
|
249
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
208
250
|
}
|
|
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
251
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
else {
|
|
253
|
+
const val = REQ_TABLE[fc];
|
|
254
|
+
if (val > 0) {
|
|
255
|
+
return val;
|
|
256
|
+
}
|
|
257
|
+
if (val < 0) {
|
|
258
|
+
const decode = -val;
|
|
259
|
+
const offset = decode >> 8;
|
|
260
|
+
if (len <= offset) {
|
|
261
|
+
return PREDICT_NEED_MORE;
|
|
262
|
+
}
|
|
263
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
237
264
|
}
|
|
238
|
-
const objLen = buffer[cursor + 1];
|
|
239
|
-
cursor += 2 + objLen;
|
|
240
265
|
}
|
|
241
|
-
return
|
|
266
|
+
return PREDICT_UNKNOWN;
|
|
242
267
|
}
|
|
243
268
|
|
|
244
269
|
/**
|
|
@@ -247,10 +272,12 @@ function predictFc43_14Response(buffer, start, end) {
|
|
|
247
272
|
function promisifyCb(fn) {
|
|
248
273
|
return new Promise((resolve, reject) => {
|
|
249
274
|
fn((err) => {
|
|
250
|
-
if (err)
|
|
275
|
+
if (err) {
|
|
251
276
|
reject(err);
|
|
252
|
-
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
253
279
|
resolve();
|
|
280
|
+
}
|
|
254
281
|
});
|
|
255
282
|
});
|
|
256
283
|
}
|
|
@@ -303,102 +330,172 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
303
330
|
return { intervalBetweenFrames, interCharTimeout };
|
|
304
331
|
}
|
|
305
332
|
|
|
306
|
-
/**
|
|
307
|
-
*
|
|
333
|
+
/**
|
|
334
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
335
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
336
|
+
*
|
|
337
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
338
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
339
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
340
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
341
|
+
* 10 concurrent: roughly equal
|
|
342
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
343
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
308
344
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
345
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
346
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
347
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
312
348
|
*/
|
|
313
349
|
class TimerHeap {
|
|
314
350
|
_deadlines = [];
|
|
315
351
|
_ids = [];
|
|
352
|
+
_seqs = [];
|
|
353
|
+
_counter = 0;
|
|
316
354
|
_timer = null;
|
|
317
355
|
_onFire;
|
|
318
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
319
356
|
_boundTick;
|
|
320
|
-
|
|
357
|
+
_threshold;
|
|
358
|
+
_mode = 'direct';
|
|
359
|
+
_directTimers = new Map();
|
|
360
|
+
/**
|
|
361
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
362
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
363
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
364
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
365
|
+
*/
|
|
366
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
321
367
|
this._onFire = onFire;
|
|
322
368
|
this._boundTick = this._onTick.bind(this);
|
|
369
|
+
this._threshold = concurrentThreshold;
|
|
323
370
|
}
|
|
324
|
-
/** Number of pending timers in the heap. */
|
|
325
371
|
get size() {
|
|
326
|
-
return this._deadlines.length;
|
|
372
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
327
373
|
}
|
|
328
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
329
374
|
add(id, ms) {
|
|
375
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
376
|
+
const deadline = performance.now() + ms;
|
|
377
|
+
const handle = setTimeout(() => {
|
|
378
|
+
if (this._mode !== 'direct') {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this._directTimers.delete(id);
|
|
382
|
+
this._onFire(id);
|
|
383
|
+
}, ms);
|
|
384
|
+
this._directTimers.set(id, { handle, deadline });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (this._mode === 'direct') {
|
|
388
|
+
this._mode = 'heap';
|
|
389
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
390
|
+
clearTimeout(handle);
|
|
391
|
+
const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
|
|
392
|
+
if (remaining === 0) {
|
|
393
|
+
this._onFire(existingId);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
this._heapAdd(existingId, remaining);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this._directTimers.clear();
|
|
400
|
+
}
|
|
401
|
+
this._heapAdd(id, ms);
|
|
402
|
+
}
|
|
403
|
+
clear() {
|
|
404
|
+
for (const { handle } of this._directTimers.values()) {
|
|
405
|
+
clearTimeout(handle);
|
|
406
|
+
}
|
|
407
|
+
this._directTimers.clear();
|
|
408
|
+
this._mode = 'direct';
|
|
409
|
+
if (this._timer) {
|
|
410
|
+
clearTimeout(this._timer);
|
|
411
|
+
this._timer = null;
|
|
412
|
+
}
|
|
413
|
+
this._deadlines.length = 0;
|
|
414
|
+
this._ids.length = 0;
|
|
415
|
+
this._seqs.length = 0;
|
|
416
|
+
this._counter = 0;
|
|
417
|
+
}
|
|
418
|
+
_heapAdd(id, ms) {
|
|
330
419
|
const deadline = performance.now() + ms;
|
|
420
|
+
const seq = this._counter++;
|
|
331
421
|
let i = this._deadlines.length;
|
|
332
422
|
this._deadlines.push(deadline);
|
|
333
423
|
this._ids.push(id);
|
|
334
|
-
|
|
424
|
+
this._seqs.push(seq);
|
|
335
425
|
while (i > 0) {
|
|
336
426
|
const p = (i - 1) >> 1;
|
|
337
|
-
|
|
427
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
428
|
+
if (parentComesFirst) {
|
|
338
429
|
break;
|
|
430
|
+
}
|
|
339
431
|
this._deadlines[i] = this._deadlines[p];
|
|
340
432
|
this._ids[i] = this._ids[p];
|
|
433
|
+
this._seqs[i] = this._seqs[p];
|
|
341
434
|
i = p;
|
|
342
435
|
}
|
|
343
436
|
this._deadlines[i] = deadline;
|
|
344
437
|
this._ids[i] = id;
|
|
345
|
-
|
|
346
|
-
if (i === 0)
|
|
438
|
+
this._seqs[i] = seq;
|
|
439
|
+
if (i === 0) {
|
|
347
440
|
this._refresh();
|
|
348
|
-
}
|
|
349
|
-
/** Dispose without firing callbacks. */
|
|
350
|
-
clear() {
|
|
351
|
-
if (this._timer) {
|
|
352
|
-
clearTimeout(this._timer);
|
|
353
|
-
this._timer = null;
|
|
354
441
|
}
|
|
355
|
-
this._deadlines.length = 0;
|
|
356
|
-
this._ids.length = 0;
|
|
357
442
|
}
|
|
358
443
|
_refresh() {
|
|
359
444
|
if (this._timer) {
|
|
360
445
|
clearTimeout(this._timer);
|
|
361
446
|
this._timer = null;
|
|
362
447
|
}
|
|
363
|
-
if (this._deadlines.length === 0)
|
|
448
|
+
if (this._deadlines.length === 0) {
|
|
364
449
|
return;
|
|
450
|
+
}
|
|
365
451
|
const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
|
|
366
|
-
|
|
452
|
+
const safeDelay = Math.min(delay, 2147483647);
|
|
453
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
367
454
|
}
|
|
368
455
|
_onTick() {
|
|
369
456
|
this._timer = null;
|
|
370
457
|
const now = performance.now();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
458
|
+
try {
|
|
459
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
460
|
+
const id = this._pop();
|
|
461
|
+
this._onFire(id);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
this._refresh();
|
|
374
466
|
}
|
|
375
|
-
this._refresh();
|
|
376
467
|
}
|
|
377
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
378
468
|
_pop() {
|
|
379
469
|
const topId = this._ids[0];
|
|
380
470
|
const lastId = this._ids.pop();
|
|
381
471
|
const lastDeadline = this._deadlines.pop();
|
|
472
|
+
const lastSeq = this._seqs.pop();
|
|
382
473
|
const n = this._deadlines.length;
|
|
383
474
|
if (n > 0) {
|
|
384
475
|
let i = 0;
|
|
385
|
-
|
|
386
|
-
while (
|
|
387
|
-
let
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
476
|
+
const half = n >> 1;
|
|
477
|
+
while (i < half) {
|
|
478
|
+
let minChild = (i << 1) + 1;
|
|
479
|
+
const rightChild = minChild + 1;
|
|
480
|
+
if (rightChild < n) {
|
|
481
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
482
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
483
|
+
if (rightComesFirst) {
|
|
484
|
+
minChild = rightChild;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
488
|
+
if (lastComesFirst) {
|
|
395
489
|
break;
|
|
396
|
-
|
|
397
|
-
this.
|
|
398
|
-
i =
|
|
490
|
+
}
|
|
491
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
492
|
+
this._ids[i] = this._ids[minChild];
|
|
493
|
+
this._seqs[i] = this._seqs[minChild];
|
|
494
|
+
i = minChild;
|
|
399
495
|
}
|
|
400
496
|
this._deadlines[i] = lastDeadline;
|
|
401
497
|
this._ids[i] = lastId;
|
|
498
|
+
this._seqs[i] = lastSeq;
|
|
402
499
|
}
|
|
403
500
|
return topId;
|
|
404
501
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ declare function drainCbs(cbs: (((err?: Error | null) => void) | undefined)[] |
|
|
|
87
87
|
|
|
88
88
|
declare function checkRange(value: number | number[], range?: [number, number] | [number, number][]): boolean;
|
|
89
89
|
|
|
90
|
-
declare function crc(data: Uint8Array, start
|
|
90
|
+
declare function crc(data: Uint8Array, start: number, end: number): number;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Returns true when `n` is an integer in the unsigned-byte range [0, 255].
|
|
@@ -98,24 +98,10 @@ declare function crc(data: Uint8Array, start?: number, end?: number): number;
|
|
|
98
98
|
*/
|
|
99
99
|
declare function isUint8(n: number): boolean;
|
|
100
100
|
|
|
101
|
-
declare function lrc(data: Uint8Array, start
|
|
101
|
+
declare function lrc(data: Uint8Array, start: number, end: number): number;
|
|
102
102
|
|
|
103
|
-
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
104
103
|
declare const PREDICT_NEED_MORE = 0;
|
|
105
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
106
104
|
declare const PREDICT_UNKNOWN = -1;
|
|
107
|
-
/**
|
|
108
|
-
* Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
|
|
109
|
-
*
|
|
110
|
-
* Returns a sentinel-encoded number to avoid per-call object allocation on the
|
|
111
|
-
* RTU decode hot path:
|
|
112
|
-
* - Positive integer (>= 4): total frame length, function code is known.
|
|
113
|
-
* - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
|
|
114
|
-
* required (typically waiting on the byteCount byte).
|
|
115
|
-
* - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
|
|
116
|
-
* the framing layer must defer to a registered `CustomFunctionCode` or treat
|
|
117
|
-
* this as a framing error.
|
|
118
|
-
*/
|
|
119
105
|
declare function predictRtuFrameLength(buffer: Buffer, start: number, end: number, isResponse: boolean): number;
|
|
120
106
|
|
|
121
107
|
/**
|
|
@@ -123,30 +109,46 @@ declare function predictRtuFrameLength(buffer: Buffer, start: number, end: numbe
|
|
|
123
109
|
*/
|
|
124
110
|
declare function promisifyCb(fn: (cb: (err?: Error | null) => void) => void): Promise<void>;
|
|
125
111
|
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
112
|
+
/**
|
|
113
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
114
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
128
115
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
116
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
117
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
118
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
119
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
120
|
+
* 10 concurrent: roughly equal
|
|
121
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
122
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
123
|
+
*
|
|
124
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
125
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
126
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
132
127
|
*/
|
|
133
128
|
declare class TimerHeap {
|
|
134
129
|
private _deadlines;
|
|
135
130
|
private _ids;
|
|
131
|
+
private _seqs;
|
|
132
|
+
private _counter;
|
|
136
133
|
private _timer;
|
|
137
134
|
private _onFire;
|
|
138
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
139
135
|
private _boundTick;
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
private _threshold;
|
|
137
|
+
private _mode;
|
|
138
|
+
private _directTimers;
|
|
139
|
+
/**
|
|
140
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
141
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
142
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
143
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
144
|
+
*/
|
|
145
|
+
constructor(onFire: (id: number) => void, concurrentThreshold?: number);
|
|
142
146
|
get size(): number;
|
|
143
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
144
147
|
add(id: number, ms: number): void;
|
|
145
|
-
/** Dispose without firing callbacks. */
|
|
146
148
|
clear(): void;
|
|
149
|
+
private _heapAdd;
|
|
147
150
|
private _refresh;
|
|
148
151
|
private _onTick;
|
|
149
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
150
152
|
private _pop;
|
|
151
153
|
}
|
|
152
154
|
|