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/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 values = Array.isArray(value) ? value : [value];
45
- if (isRangeArray(range)) {
46
- for (const r of range) {
47
- const [min, max] = r;
48
- const [lo, hi] = min <= max ? [min, max] : [max, min];
49
- if (values.every((n) => inRange(n, [lo, hi]))) {
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 [min, max] = range;
56
- const [lo, hi] = min <= max ? [min, max] : [max, min];
57
- return values.every((n) => inRange(n, [lo, hi]));
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 = 0, end = data.length) {
112
+ ]);
113
+ function crc(data, start, end) {
79
114
  let crc = 0xffff;
80
115
  for (let index = start; index < end; index++) {
81
- crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
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 Number.isInteger(n) && n >= 0 && n <= 255;
129
+ return (n & 0xff) === n;
95
130
  }
96
131
 
97
- function lrc(data, start = 0, end = data.length) {
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 (~sum + 1) & 0xff;
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
- * Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
182
- *
183
- * Returns a sentinel-encoded number to avoid per-call object allocation on the
184
- * RTU decode hot path:
185
- * - Positive integer (>= 4): total frame length, function code is known.
186
- * - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
187
- * required (typically waiting on the byteCount byte).
188
- * - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
189
- * the framing layer must defer to a registered `CustomFunctionCode` or treat
190
- * this as a framing error.
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
- if (end - start < 2) {
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 && (fc & EXCEPTION_OFFSET) !== 0) {
198
- return 5;
199
- }
200
- const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
201
- if (fixed !== undefined) {
202
- return fixed;
203
- }
204
- const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
205
- if (bc !== undefined) {
206
- if (end - start <= bc.offset) {
207
- return PREDICT_NEED_MORE;
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
- const numObjs = buffer[start + 7];
233
- let cursor = start + 8;
234
- for (let i = 0; i < numObjs; i++) {
235
- if (end < cursor + 2) {
236
- return PREDICT_NEED_MORE;
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 cursor - start + 2;
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
- else
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
- /** @internal
307
- * Zero-allocation binary min-heap for coalescing per-request timeouts.
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
- * Uses two parallel numeric arrays (no object allocation per entry).
310
- * Lazy deletion: callers never remove from the heap; expired entries
311
- * are silently dropped when they surface at the top.
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
- constructor(onFire) {
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
- // sift up
424
+ this._seqs.push(seq);
335
425
  while (i > 0) {
336
426
  const p = (i - 1) >> 1;
337
- if (this._deadlines[p] <= deadline)
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
- // Only reschedule when the new entry became the heap top.
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
- this._timer = setTimeout(this._boundTick, delay);
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
- while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
372
- const id = this._pop();
373
- this._onFire(id);
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
- // sift down
386
- while (true) {
387
- let min = i;
388
- const l = i * 2 + 1;
389
- const r = l + 1;
390
- if (l < n && this._deadlines[l] < this._deadlines[min])
391
- min = l;
392
- if (r < n && this._deadlines[r] < this._deadlines[min])
393
- min = r;
394
- if (min === i)
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
- this._deadlines[i] = this._deadlines[min];
397
- this._ids[i] = this._ids[min];
398
- i = min;
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?: number, end?: number): number;
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?: number, end?: number): number;
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
- /** @internal
127
- * Zero-allocation binary min-heap for coalescing per-request timeouts.
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
- * Uses two parallel numeric arrays (no object allocation per entry).
130
- * Lazy deletion: callers never remove from the heap; expired entries
131
- * are silently dropped when they surface at the top.
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
- constructor(onFire: (id: number) => void);
141
- /** Number of pending timers in the heap. */
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