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/index.cjs CHANGED
@@ -166,40 +166,75 @@ function bitsToMs(baudRate, bits) {
166
166
  * open / close / destroy callbacks.
167
167
  */
168
168
  function drainCbs(cbs, err) {
169
- if (!cbs)
169
+ if (!cbs) {
170
170
  return;
171
+ }
171
172
  for (const cb of cbs) {
172
173
  cb?.(err);
173
174
  }
174
175
  }
175
176
 
176
- function inRange(n, [min, max]) {
177
- return n >= min && n <= max;
178
- }
179
- function isRangeArray(range) {
180
- return Array.isArray(range[0]);
181
- }
182
177
  function checkRange(value, range) {
183
178
  if (!range || range.length === 0) {
184
179
  return true;
185
180
  }
186
- const values = Array.isArray(value) ? value : [value];
187
- if (isRangeArray(range)) {
188
- for (const r of range) {
189
- const [min, max] = r;
190
- const [lo, hi] = min <= max ? [min, max] : [max, min];
191
- if (values.every((n) => inRange(n, [lo, hi]))) {
181
+ const isMultiRange = Array.isArray(range[0]);
182
+ const isValueArray = Array.isArray(value);
183
+ if (!isValueArray && !isMultiRange) {
184
+ const r = range;
185
+ const min = r[0], max = r[1];
186
+ const v = value;
187
+ return min <= max ? v >= min && v <= max : v >= max && v <= min;
188
+ }
189
+ if (!isValueArray && isMultiRange) {
190
+ const ranges = range;
191
+ const v = value;
192
+ for (let i = 0; i < ranges.length; i++) {
193
+ const min = ranges[i][0], max = ranges[i][1];
194
+ const lo = min <= max ? min : max;
195
+ const hi = min <= max ? max : min;
196
+ if (v >= lo && v <= hi) {
192
197
  return true;
193
198
  }
194
199
  }
195
200
  return false;
196
201
  }
197
- const [min, max] = range;
198
- const [lo, hi] = min <= max ? [min, max] : [max, min];
199
- return values.every((n) => inRange(n, [lo, hi]));
202
+ const values = value;
203
+ if (values.length === 0) {
204
+ return true;
205
+ }
206
+ if (!isMultiRange) {
207
+ const r = range;
208
+ const min = r[0], max = r[1];
209
+ const lo = min <= max ? min : max;
210
+ const hi = min <= max ? max : min;
211
+ for (let i = 0; i < values.length; i++) {
212
+ if (values[i] < lo || values[i] > hi) {
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ const ranges = range;
219
+ for (let i = 0; i < ranges.length; i++) {
220
+ const min = ranges[i][0], max = ranges[i][1];
221
+ const lo = min <= max ? min : max;
222
+ const hi = min <= max ? max : min;
223
+ let allInRange = true;
224
+ for (let j = 0; j < values.length; j++) {
225
+ if (values[j] < lo || values[j] > hi) {
226
+ allInRange = false;
227
+ break;
228
+ }
229
+ }
230
+ if (allInRange) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
200
235
  }
201
236
 
202
- const TABLE = [
237
+ const TABLE = new Uint16Array([
203
238
  0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
204
239
  0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
205
240
  0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
@@ -216,11 +251,11 @@ const TABLE = [
216
251
  0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
217
252
  0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
218
253
  0x4040,
219
- ];
220
- function crc(data, start = 0, end = data.length) {
254
+ ]);
255
+ function crc(data, start, end) {
221
256
  let crc = 0xffff;
222
257
  for (let index = start; index < end; index++) {
223
- crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
258
+ crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
224
259
  }
225
260
  return crc;
226
261
  }
@@ -233,114 +268,104 @@ function crc(data, start = 0, end = data.length) {
233
268
  * Infinity, and out-of-range values uniformly.
234
269
  */
235
270
  function isUint8(n) {
236
- return Number.isInteger(n) && n >= 0 && n <= 255;
271
+ return (n & 0xff) === n;
237
272
  }
238
273
 
239
- function lrc(data, start = 0, end = data.length) {
274
+ function lrc(data, start, end) {
240
275
  let sum = 0;
241
276
  for (let i = start; i < end; i++) {
242
277
  sum += data[i];
243
278
  }
244
- return (~sum + 1) & 0xff;
279
+ return -sum & 0xff;
245
280
  }
246
281
 
247
- const REQUEST_FIXED_LENGTHS = {
248
- [exports.FunctionCode.READ_COILS]: 8,
249
- [exports.FunctionCode.READ_DISCRETE_INPUTS]: 8,
250
- [exports.FunctionCode.READ_HOLDING_REGISTERS]: 8,
251
- [exports.FunctionCode.READ_INPUT_REGISTERS]: 8,
252
- [exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
253
- [exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
254
- [exports.FunctionCode.REPORT_SERVER_ID]: 4,
255
- [exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
256
- [exports.FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
257
- };
258
- const REQUEST_BYTE_COUNT = {
259
- [exports.FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
260
- [exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
261
- [exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
262
- };
263
- const RESPONSE_FIXED_LENGTHS = {
264
- [exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
265
- [exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
266
- [exports.FunctionCode.WRITE_MULTIPLE_COILS]: 8,
267
- [exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
268
- [exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
269
- };
270
- const RESPONSE_BYTE_COUNT = {
271
- [exports.FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
272
- [exports.FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
273
- [exports.FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
274
- [exports.FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
275
- [exports.FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
276
- [exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
277
- };
278
- /** Sentinel: caller needs to feed more bytes before length can be determined. */
279
282
  const PREDICT_NEED_MORE = 0;
280
- /** Sentinel: function code is not in the standard tables. */
281
283
  const PREDICT_UNKNOWN = -1;
282
- /**
283
- * Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
284
- *
285
- * Returns a sentinel-encoded number to avoid per-call object allocation on the
286
- * RTU decode hot path:
287
- * - Positive integer (>= 4): total frame length, function code is known.
288
- * - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
289
- * required (typically waiting on the byteCount byte).
290
- * - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
291
- * the framing layer must defer to a registered `CustomFunctionCode` or treat
292
- * this as a framing error.
293
- */
284
+ const REQ_TABLE = new Int32Array(256);
285
+ const RES_TABLE = new Int32Array(256);
286
+ (function initTables() {
287
+ REQ_TABLE[exports.FunctionCode.READ_COILS] = 8;
288
+ REQ_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = 8;
289
+ REQ_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = 8;
290
+ REQ_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = 8;
291
+ REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
292
+ REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
293
+ REQ_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = 4;
294
+ REQ_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
295
+ REQ_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
296
+ REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
297
+ REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
298
+ REQ_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
299
+ RES_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
300
+ RES_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
301
+ RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = 8;
302
+ RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
303
+ RES_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
304
+ RES_TABLE[exports.FunctionCode.READ_COILS] = -517;
305
+ RES_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = -517;
306
+ RES_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = -517;
307
+ RES_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = -517;
308
+ RES_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = -517;
309
+ RES_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
310
+ RES_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
311
+ })();
294
312
  function predictRtuFrameLength(buffer, start, end, isResponse) {
295
- if (end - start < 2) {
313
+ const len = end - start;
314
+ if (len < 2) {
296
315
  return PREDICT_NEED_MORE;
297
316
  }
298
317
  const fc = buffer[start + 1];
299
- if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
300
- return 5;
301
- }
302
- const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
303
- if (fixed !== undefined) {
304
- return fixed;
305
- }
306
- const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
307
- if (bc !== undefined) {
308
- if (end - start <= bc.offset) {
309
- return PREDICT_NEED_MORE;
318
+ if (isResponse) {
319
+ if ((fc & EXCEPTION_OFFSET) !== 0) {
320
+ return 5;
321
+ }
322
+ const val = RES_TABLE[fc];
323
+ if (val > 0) {
324
+ return val;
325
+ }
326
+ if (val < 0) {
327
+ if (val === -999) {
328
+ // FC 43 / MEI 14 response — inline to avoid function-call overhead on
329
+ // the framing hot path (even though this FC is uncommon).
330
+ if (end - start < 8) {
331
+ return PREDICT_NEED_MORE;
332
+ }
333
+ if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
334
+ return PREDICT_UNKNOWN;
335
+ }
336
+ const numObjs = buffer[start + 7];
337
+ let cursor = start + 8;
338
+ for (let i = 0; i < numObjs; i++) {
339
+ if (end < cursor + 2) {
340
+ return PREDICT_NEED_MORE;
341
+ }
342
+ cursor += 2 + buffer[cursor + 1];
343
+ }
344
+ return cursor - start + 2;
345
+ }
346
+ const decode = -val;
347
+ const offset = decode >> 8;
348
+ if (len <= offset) {
349
+ return PREDICT_NEED_MORE;
350
+ }
351
+ return (decode & 0xff) + buffer[start + offset];
310
352
  }
311
- return bc.extra + buffer[start + bc.offset];
312
353
  }
313
- if (isResponse && fc === exports.FunctionCode.READ_DEVICE_IDENTIFICATION) {
314
- return predictFc43_14Response(buffer, start, end);
315
- }
316
- return PREDICT_UNKNOWN;
317
- }
318
- /**
319
- * Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
320
- * response structure per Modbus V1.1b3 §6.21.
321
- *
322
- * Layout (after unit and fc):
323
- * mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
324
- * [objId(1) objLen(1) objData(objLen)] × numObjs
325
- * CRC(2)
326
- */
327
- function predictFc43_14Response(buffer, start, end) {
328
- if (end - start < 8) {
329
- return PREDICT_NEED_MORE;
330
- }
331
- if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
332
- return PREDICT_UNKNOWN;
333
- }
334
- const numObjs = buffer[start + 7];
335
- let cursor = start + 8;
336
- for (let i = 0; i < numObjs; i++) {
337
- if (end < cursor + 2) {
338
- return PREDICT_NEED_MORE;
354
+ else {
355
+ const val = REQ_TABLE[fc];
356
+ if (val > 0) {
357
+ return val;
358
+ }
359
+ if (val < 0) {
360
+ const decode = -val;
361
+ const offset = decode >> 8;
362
+ if (len <= offset) {
363
+ return PREDICT_NEED_MORE;
364
+ }
365
+ return (decode & 0xff) + buffer[start + offset];
339
366
  }
340
- const objLen = buffer[cursor + 1];
341
- cursor += 2 + objLen;
342
367
  }
343
- return cursor - start + 2;
368
+ return PREDICT_UNKNOWN;
344
369
  }
345
370
 
346
371
  /**
@@ -349,10 +374,12 @@ function predictFc43_14Response(buffer, start, end) {
349
374
  function promisifyCb(fn) {
350
375
  return new Promise((resolve, reject) => {
351
376
  fn((err) => {
352
- if (err)
377
+ if (err) {
353
378
  reject(err);
354
- else
379
+ }
380
+ else {
355
381
  resolve();
382
+ }
356
383
  });
357
384
  });
358
385
  }
@@ -405,102 +432,172 @@ function resolveRtuTiming(opts = {}, baudRate) {
405
432
  return { intervalBetweenFrames, interCharTimeout };
406
433
  }
407
434
 
408
- /** @internal
409
- * Zero-allocation binary min-heap for coalescing per-request timeouts.
435
+ /**
436
+ * Hybrid timer manager: uses native `setTimeout` for low concurrency
437
+ * and switches to a binary min-heap when concurrency exceeds the threshold.
438
+ *
439
+ * Benchmarks (add + clear throughput, Node 24, x64):
440
+ * 1 concurrent: setTimeout ~1.7× faster than heap
441
+ * 2 concurrent: setTimeout ~1.6× faster than heap
442
+ * 5 concurrent: setTimeout ~1.5-1.9× faster than heap
443
+ * 10 concurrent: roughly equal
444
+ * 20 concurrent: heap ~1.3× faster than setTimeout[]
445
+ * 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
410
446
  *
411
- * Uses two parallel numeric arrays (no object allocation per entry).
412
- * Lazy deletion: callers never remove from the heap; expired entries
413
- * are silently dropped when they surface at the top.
447
+ * The crossover point is around 10 concurrent timers, so the default
448
+ * `concurrentThreshold = 2` keeps the common 1-2 request case on the
449
+ * fast direct path while delegating to the heap for larger batches.
414
450
  */
415
451
  class TimerHeap {
416
452
  _deadlines = [];
417
453
  _ids = [];
454
+ _seqs = [];
455
+ _counter = 0;
418
456
  _timer = null;
419
457
  _onFire;
420
- /** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
421
458
  _boundTick;
422
- constructor(onFire) {
459
+ _threshold;
460
+ _mode = 'direct';
461
+ _directTimers = new Map();
462
+ /**
463
+ * @param onFire Callback invoked with the timer id when it expires.
464
+ * @param concurrentThreshold Maximum number of timers kept as individual
465
+ * native `setTimeout` handles. Once exceeded, all timers migrate to
466
+ * the internal heap and share a single native timer. Default is 2.
467
+ */
468
+ constructor(onFire, concurrentThreshold = 2) {
423
469
  this._onFire = onFire;
424
470
  this._boundTick = this._onTick.bind(this);
471
+ this._threshold = concurrentThreshold;
425
472
  }
426
- /** Number of pending timers in the heap. */
427
473
  get size() {
428
- return this._deadlines.length;
474
+ return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
429
475
  }
430
- /** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
431
476
  add(id, ms) {
477
+ if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
478
+ const deadline = performance.now() + ms;
479
+ const handle = setTimeout(() => {
480
+ if (this._mode !== 'direct') {
481
+ return;
482
+ }
483
+ this._directTimers.delete(id);
484
+ this._onFire(id);
485
+ }, ms);
486
+ this._directTimers.set(id, { handle, deadline });
487
+ return;
488
+ }
489
+ if (this._mode === 'direct') {
490
+ this._mode = 'heap';
491
+ for (const [existingId, { handle, deadline }] of this._directTimers) {
492
+ clearTimeout(handle);
493
+ const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
494
+ if (remaining === 0) {
495
+ this._onFire(existingId);
496
+ }
497
+ else {
498
+ this._heapAdd(existingId, remaining);
499
+ }
500
+ }
501
+ this._directTimers.clear();
502
+ }
503
+ this._heapAdd(id, ms);
504
+ }
505
+ clear() {
506
+ for (const { handle } of this._directTimers.values()) {
507
+ clearTimeout(handle);
508
+ }
509
+ this._directTimers.clear();
510
+ this._mode = 'direct';
511
+ if (this._timer) {
512
+ clearTimeout(this._timer);
513
+ this._timer = null;
514
+ }
515
+ this._deadlines.length = 0;
516
+ this._ids.length = 0;
517
+ this._seqs.length = 0;
518
+ this._counter = 0;
519
+ }
520
+ _heapAdd(id, ms) {
432
521
  const deadline = performance.now() + ms;
522
+ const seq = this._counter++;
433
523
  let i = this._deadlines.length;
434
524
  this._deadlines.push(deadline);
435
525
  this._ids.push(id);
436
- // sift up
526
+ this._seqs.push(seq);
437
527
  while (i > 0) {
438
528
  const p = (i - 1) >> 1;
439
- if (this._deadlines[p] <= deadline)
529
+ const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
530
+ if (parentComesFirst) {
440
531
  break;
532
+ }
441
533
  this._deadlines[i] = this._deadlines[p];
442
534
  this._ids[i] = this._ids[p];
535
+ this._seqs[i] = this._seqs[p];
443
536
  i = p;
444
537
  }
445
538
  this._deadlines[i] = deadline;
446
539
  this._ids[i] = id;
447
- // Only reschedule when the new entry became the heap top.
448
- if (i === 0)
540
+ this._seqs[i] = seq;
541
+ if (i === 0) {
449
542
  this._refresh();
450
- }
451
- /** Dispose without firing callbacks. */
452
- clear() {
453
- if (this._timer) {
454
- clearTimeout(this._timer);
455
- this._timer = null;
456
543
  }
457
- this._deadlines.length = 0;
458
- this._ids.length = 0;
459
544
  }
460
545
  _refresh() {
461
546
  if (this._timer) {
462
547
  clearTimeout(this._timer);
463
548
  this._timer = null;
464
549
  }
465
- if (this._deadlines.length === 0)
550
+ if (this._deadlines.length === 0) {
466
551
  return;
552
+ }
467
553
  const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
468
- this._timer = setTimeout(this._boundTick, delay);
554
+ const safeDelay = Math.min(delay, 2147483647);
555
+ this._timer = setTimeout(this._boundTick, safeDelay);
469
556
  }
470
557
  _onTick() {
471
558
  this._timer = null;
472
559
  const now = performance.now();
473
- while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
474
- const id = this._pop();
475
- this._onFire(id);
560
+ try {
561
+ while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
562
+ const id = this._pop();
563
+ this._onFire(id);
564
+ }
565
+ }
566
+ finally {
567
+ this._refresh();
476
568
  }
477
- this._refresh();
478
569
  }
479
- /** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
480
570
  _pop() {
481
571
  const topId = this._ids[0];
482
572
  const lastId = this._ids.pop();
483
573
  const lastDeadline = this._deadlines.pop();
574
+ const lastSeq = this._seqs.pop();
484
575
  const n = this._deadlines.length;
485
576
  if (n > 0) {
486
577
  let i = 0;
487
- // sift down
488
- while (true) {
489
- let min = i;
490
- const l = i * 2 + 1;
491
- const r = l + 1;
492
- if (l < n && this._deadlines[l] < this._deadlines[min])
493
- min = l;
494
- if (r < n && this._deadlines[r] < this._deadlines[min])
495
- min = r;
496
- if (min === i)
578
+ const half = n >> 1;
579
+ while (i < half) {
580
+ let minChild = (i << 1) + 1;
581
+ const rightChild = minChild + 1;
582
+ if (rightChild < n) {
583
+ const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
584
+ (this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
585
+ if (rightComesFirst) {
586
+ minChild = rightChild;
587
+ }
588
+ }
589
+ const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
590
+ if (lastComesFirst) {
497
591
  break;
498
- this._deadlines[i] = this._deadlines[min];
499
- this._ids[i] = this._ids[min];
500
- i = min;
592
+ }
593
+ this._deadlines[i] = this._deadlines[minChild];
594
+ this._ids[i] = this._ids[minChild];
595
+ this._seqs[i] = this._seqs[minChild];
596
+ i = minChild;
501
597
  }
502
598
  this._deadlines[i] = lastDeadline;
503
599
  this._ids[i] = lastId;
600
+ this._seqs[i] = lastSeq;
504
601
  }
505
602
  return topId;
506
603
  }
@@ -1620,8 +1717,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1620
1717
  while (state.end - state.start > 0) {
1621
1718
  const available = state.end - state.start;
1622
1719
  if (available < MIN_FRAME_LENGTH) {
1623
- if (this._handleIncomplete(state, strict))
1720
+ if (this._handleIncomplete(state, strict)) {
1624
1721
  return;
1722
+ }
1625
1723
  break;
1626
1724
  }
1627
1725
  const fc = pool[state.start + 1];
@@ -1650,8 +1748,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1650
1748
  state.start += 1;
1651
1749
  continue;
1652
1750
  }
1653
- if (this._handleIncomplete(state, strict))
1751
+ if (this._handleIncomplete(state, strict)) {
1654
1752
  return;
1753
+ }
1655
1754
  break;
1656
1755
  }
1657
1756
  if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
@@ -1665,8 +1764,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1665
1764
  state.start += 1;
1666
1765
  continue;
1667
1766
  }
1668
- if (this._handleIncomplete(state, strict))
1767
+ if (this._handleIncomplete(state, strict)) {
1669
1768
  return;
1769
+ }
1670
1770
  break;
1671
1771
  }
1672
1772
  // CRC check inline: no helper call, no subarray for the CRC body.
@@ -1728,8 +1828,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1728
1828
  buffer[0] = unit;
1729
1829
  buffer[1] = fc;
1730
1830
  if (data.length <= 16) {
1731
- for (let i = 0; i < data.length; i++)
1831
+ for (let i = 0; i < data.length; i++) {
1732
1832
  buffer[2 + i] = data[i];
1833
+ }
1733
1834
  }
1734
1835
  else {
1735
1836
  buffer.set(data, 2);
@@ -1748,9 +1849,8 @@ const CHAR_CODE = {
1748
1849
  CR: '\r'.charCodeAt(0),
1749
1850
  LF: '\n'.charCodeAt(0),
1750
1851
  };
1751
- // Modbus ASCII frame encodes at most 256 PDU bytes as 512 hex chars between
1752
- // `:` and `\r`. Cap per-connection buffering so a peer that never sends `\r`
1753
- // cannot grow `state.frame` without bound.
1852
+ // Modbus ASCII frame body is capped well below the theoretical maximum so a
1853
+ // peer that never sends `\r` cannot grow `state.frame` without bound.
1754
1854
  const MAX_ASCII_PAYLOAD = 512;
1755
1855
  const HEX_DECODE = new Uint8Array(256);
1756
1856
  HEX_DECODE.fill(0xff);
@@ -1887,14 +1987,16 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1887
1987
  // dataLen may be 0 for a frame that is only unit + fc + lrc.
1888
1988
  const dataLen = byteLen - 3;
1889
1989
  const data = Buffer.allocUnsafe(dataLen);
1990
+ let hexOff = 4;
1890
1991
  for (let i = 0; i < dataLen; i++) {
1891
- const hi = HEX_DECODE[hexChars[4 + i * 2]];
1892
- const lo = HEX_DECODE[hexChars[4 + i * 2 + 1]];
1992
+ const hi = HEX_DECODE[hexChars[hexOff]];
1993
+ const lo = HEX_DECODE[hexChars[hexOff + 1]];
1893
1994
  if (hi === 0xff || lo === 0xff) {
1894
1995
  this.onFramingError(new Error('Invalid hex character'));
1895
1996
  return;
1896
1997
  }
1897
1998
  data[i] = (hi << 4) | lo;
1999
+ hexOff += 2;
1898
2000
  }
1899
2001
  // Compute LRC over unit + fc + data.
1900
2002
  let sum = unit + fc;
@@ -1925,13 +2027,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1925
2027
  buffer[0] = unit;
1926
2028
  buffer[1] = fc;
1927
2029
  buffer.set(data, 2);
1928
- buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
2030
+ buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
1929
2031
  const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
1930
2032
  out[0] = CHAR_CODE.COLON;
2033
+ let outOff = 1;
1931
2034
  for (let i = 0; i < buffer.length; i++) {
1932
2035
  const byte = buffer[i];
1933
- out[1 + i * 2] = HEX_ENCODE[byte >> 4];
1934
- out[2 + i * 2] = HEX_ENCODE[byte & 0x0f];
2036
+ out[outOff] = HEX_ENCODE[byte >> 4];
2037
+ out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
2038
+ outOff += 2;
1935
2039
  }
1936
2040
  out[out.length - 2] = CHAR_CODE.CR;
1937
2041
  out[out.length - 1] = CHAR_CODE.LF;
@@ -2034,7 +2138,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2034
2138
  const frame = {
2035
2139
  // Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
2036
2140
  // argument coercion + bounds check. Symmetric to the header writes in
2037
- // encode() above. Hits on every received TCP frame.
2141
+ // encode() below. Hits on every received TCP frame.
2038
2142
  transaction: (buffer[0] << 8) | buffer[1],
2039
2143
  unit: buffer[6],
2040
2144
  fc: buffer[7],
@@ -2063,8 +2167,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2063
2167
  // Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
2064
2168
  // crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
2065
2169
  if (data.length <= 16) {
2066
- for (let i = 0; i < data.length; i++)
2170
+ for (let i = 0; i < data.length; i++) {
2067
2171
  buffer[8 + i] = data[i];
2172
+ }
2068
2173
  }
2069
2174
  else {
2070
2175
  buffer.set(data, 8);
@@ -2112,26 +2217,33 @@ class MasterSession {
2112
2217
  }
2113
2218
 
2114
2219
  function validateResponse(frame, unit, fc) {
2115
- if (frame.unit !== unit || frame.fc !== fc)
2220
+ if (frame.unit !== unit || frame.fc !== fc) {
2116
2221
  throw new Error('Invalid response');
2222
+ }
2117
2223
  }
2118
2224
  function validateByteCountResponse(frame, unit, fc, byteCount) {
2119
2225
  validateResponse(frame, unit, fc);
2120
- if (frame.data.length < 1 + byteCount)
2226
+ if (frame.data.length < 1 + byteCount) {
2121
2227
  throw new Error('Insufficient data length');
2122
- if (frame.data.length !== 1 + byteCount)
2228
+ }
2229
+ if (frame.data.length !== 1 + byteCount) {
2123
2230
  throw new Error('Invalid response');
2124
- if (frame.data[0] !== byteCount)
2231
+ }
2232
+ if (frame.data[0] !== byteCount) {
2125
2233
  throw new Error('Invalid response');
2234
+ }
2126
2235
  }
2127
2236
  function validateEchoResponse(frame, unit, fc, expected) {
2128
2237
  validateResponse(frame, unit, fc);
2129
- if (frame.data.length < expected.length)
2238
+ if (frame.data.length < expected.length) {
2130
2239
  throw new Error('Insufficient data length');
2131
- if (frame.data.length !== expected.length)
2240
+ }
2241
+ if (frame.data.length !== expected.length) {
2132
2242
  throw new Error('Invalid response');
2133
- if (!frame.data.equals(expected))
2243
+ }
2244
+ if (!frame.data.equals(expected)) {
2134
2245
  throw new Error('Invalid response');
2246
+ }
2135
2247
  }
2136
2248
  class ModbusMaster extends EventEmitter {
2137
2249
  timeout;
@@ -2159,8 +2271,9 @@ class ModbusMaster extends EventEmitter {
2159
2271
  _pendingExchanges = new Map();
2160
2272
  _timerHeap = new TimerHeap((id) => {
2161
2273
  const pending = this._pendingExchanges.get(id);
2162
- if (!pending)
2163
- return; // lazy deletion: already handled
2274
+ if (!pending) {
2275
+ return;
2276
+ } // lazy deletion: already handled
2164
2277
  pending.settled = true;
2165
2278
  this._pendingExchanges.delete(id);
2166
2279
  if (pending.sessionKey !== null) {
@@ -2347,7 +2460,9 @@ class ModbusMaster extends EventEmitter {
2347
2460
  }
2348
2461
  // Lazy-deletion timer architecture:
2349
2462
  // 1. Assign an exchangeId and register in _pendingExchanges.
2350
- // 2. Push deadline into the global TimerHeap (no per-request setTimeout).
2463
+ // 2. Push deadline into the global TimerHeap (one native setTimeout under
2464
+ // load; a fast direct-timer path is used when only 1-2 exchanges are
2465
+ // pending).
2351
2466
  // 3. When the response arrives, delete from Map — the heap entry is left
2352
2467
  // behind and silently discarded when it surfaces at the top (lazy deletion).
2353
2468
  const exchangeId = this._nextExchangeId++;
@@ -2358,11 +2473,13 @@ class ModbusMaster extends EventEmitter {
2358
2473
  this._timerHeap.add(exchangeId, timeout);
2359
2474
  connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
2360
2475
  const p = this._pendingExchanges.get(exchangeId);
2361
- if (!p || p.settled)
2476
+ if (!p || p.settled) {
2362
2477
  return;
2478
+ }
2363
2479
  const cb = p.callback;
2364
- if (!cb)
2480
+ if (!cb) {
2365
2481
  return;
2482
+ }
2366
2483
  p.settled = true;
2367
2484
  p.callback = null;
2368
2485
  this._pendingExchanges.delete(exchangeId);
@@ -2391,8 +2508,9 @@ class ModbusMaster extends EventEmitter {
2391
2508
  this._timerHeap.add(exchangeId, timeout);
2392
2509
  connection.write(payload, (writeErr) => {
2393
2510
  const p = this._pendingExchanges.get(exchangeId);
2394
- if (!p || p.settled)
2511
+ if (!p || p.settled) {
2395
2512
  return;
2513
+ }
2396
2514
  if (writeErr) {
2397
2515
  const cb = p.callback;
2398
2516
  if (cb) {
@@ -2417,8 +2535,9 @@ class ModbusMaster extends EventEmitter {
2417
2535
  // Timeout is managed by the global timer heap above.
2418
2536
  this._masterSession.start(key, (err, frame) => {
2419
2537
  const p2 = this._pendingExchanges.get(exchangeId);
2420
- if (!p2 || p2.settled)
2538
+ if (!p2 || p2.settled) {
2421
2539
  return;
2540
+ }
2422
2541
  const cb = p2.callback;
2423
2542
  if (cb) {
2424
2543
  p2.settled = true;
@@ -2451,8 +2570,42 @@ class ModbusMaster extends EventEmitter {
2451
2570
  try {
2452
2571
  validateByteCountResponse(frame, unit, fc, byteCount);
2453
2572
  const data = new Array(length);
2454
- for (let i = 0; i < length; i++) {
2455
- data[i] = (frame.data[1 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
2573
+ let byteIdx = 1;
2574
+ let outIdx = 0;
2575
+ const fullBytes = length >> 3;
2576
+ for (let b = 0; b < fullBytes; b++) {
2577
+ const byte = frame.data[byteIdx++];
2578
+ data[outIdx++] = (byte & 0x01) > 0;
2579
+ data[outIdx++] = (byte & 0x02) > 0;
2580
+ data[outIdx++] = (byte & 0x04) > 0;
2581
+ data[outIdx++] = (byte & 0x08) > 0;
2582
+ data[outIdx++] = (byte & 0x10) > 0;
2583
+ data[outIdx++] = (byte & 0x20) > 0;
2584
+ data[outIdx++] = (byte & 0x40) > 0;
2585
+ data[outIdx++] = (byte & 0x80) > 0;
2586
+ }
2587
+ const rem = length & 7;
2588
+ if (rem) {
2589
+ const byte = frame.data[byteIdx];
2590
+ data[outIdx++] = (byte & 0x01) > 0;
2591
+ if (rem > 1) {
2592
+ data[outIdx++] = (byte & 0x02) > 0;
2593
+ }
2594
+ if (rem > 2) {
2595
+ data[outIdx++] = (byte & 0x04) > 0;
2596
+ }
2597
+ if (rem > 3) {
2598
+ data[outIdx++] = (byte & 0x08) > 0;
2599
+ }
2600
+ if (rem > 4) {
2601
+ data[outIdx++] = (byte & 0x10) > 0;
2602
+ }
2603
+ if (rem > 5) {
2604
+ data[outIdx++] = (byte & 0x20) > 0;
2605
+ }
2606
+ if (rem > 6) {
2607
+ data[outIdx++] = (byte & 0x40) > 0;
2608
+ }
2456
2609
  }
2457
2610
  // Mutate the frame in place rather than spread-copying — `frame` is freshly
2458
2611
  // allocated per request and not retained anywhere else.
@@ -2500,9 +2653,10 @@ class ModbusMaster extends EventEmitter {
2500
2653
  // on each call. Symmetric to the slave-side BE write inlining
2501
2654
  // in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
2502
2655
  // bounds-check pairs per response.
2656
+ let off = 0;
2503
2657
  for (let i = 0; i < length; i++) {
2504
- const off = i * 2;
2505
2658
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
2659
+ off += 2;
2506
2660
  }
2507
2661
  frame.data = data;
2508
2662
  resolve(frame);
@@ -2593,10 +2747,19 @@ class ModbusMaster extends EventEmitter {
2593
2747
  bufferTx[2] = (value.length >>> 8) & 0xff;
2594
2748
  bufferTx[3] = value.length & 0xff;
2595
2749
  bufferTx[4] = byteCount;
2750
+ let acc = 0;
2751
+ let out = 5;
2596
2752
  for (let i = 0; i < value.length; i++) {
2597
2753
  if (value[i]) {
2598
- bufferTx[5 + Math.floor(i / 8)] |= 1 << i % 8;
2754
+ acc |= 1 << (i & 7);
2599
2755
  }
2756
+ if ((i & 7) === 7) {
2757
+ bufferTx[out++] = acc;
2758
+ acc = 0;
2759
+ }
2760
+ }
2761
+ if ((value.length & 7) !== 0) {
2762
+ bufferTx[out] = acc;
2600
2763
  }
2601
2764
  return new Promise((resolve, reject) => {
2602
2765
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2630,11 +2793,12 @@ class ModbusMaster extends EventEmitter {
2630
2793
  bufferTx[2] = (value.length >>> 8) & 0xff;
2631
2794
  bufferTx[3] = value.length & 0xff;
2632
2795
  bufferTx[4] = byteCount;
2796
+ let off = 5;
2633
2797
  for (let i = 0; i < value.length; i++) {
2634
2798
  const v = value[i];
2635
- const off = 5 + i * 2;
2636
2799
  bufferTx[off] = (v >>> 8) & 0xff;
2637
2800
  bufferTx[off + 1] = v & 0xff;
2801
+ off += 2;
2638
2802
  }
2639
2803
  return new Promise((resolve, reject) => {
2640
2804
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2672,10 +2836,12 @@ class ModbusMaster extends EventEmitter {
2672
2836
  }
2673
2837
  try {
2674
2838
  validateResponse(frame, unit, fc);
2675
- if (frame.data.length < 2 + serverIdLength)
2839
+ if (frame.data.length < 2 + serverIdLength) {
2676
2840
  throw new Error('Insufficient data length');
2677
- if (frame.data.length !== 1 + frame.data[0])
2841
+ }
2842
+ if (frame.data.length !== 1 + frame.data[0]) {
2678
2843
  throw new Error('Invalid response');
2844
+ }
2679
2845
  const runStatusIndex = 1 + serverIdLength;
2680
2846
  frame.data = {
2681
2847
  serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
@@ -2738,11 +2904,12 @@ class ModbusMaster extends EventEmitter {
2738
2904
  bufferTx[6] = (write.value.length >>> 8) & 0xff;
2739
2905
  bufferTx[7] = write.value.length & 0xff;
2740
2906
  bufferTx[8] = byteCount;
2907
+ let off = 9;
2741
2908
  for (let i = 0; i < write.value.length; i++) {
2742
2909
  const v = write.value[i];
2743
- const off = 9 + i * 2;
2744
2910
  bufferTx[off] = (v >>> 8) & 0xff;
2745
2911
  bufferTx[off + 1] = v & 0xff;
2912
+ off += 2;
2746
2913
  }
2747
2914
  return new Promise((resolve, reject) => {
2748
2915
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2761,9 +2928,10 @@ class ModbusMaster extends EventEmitter {
2761
2928
  // closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
2762
2929
  // response handler for the same optimization.
2763
2930
  const data = new Array(read.length);
2931
+ let off = 0;
2764
2932
  for (let i = 0; i < read.length; i++) {
2765
- const off = i * 2;
2766
2933
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
2934
+ off += 2;
2767
2935
  }
2768
2936
  frame.data = data;
2769
2937
  resolve(frame);
@@ -2789,10 +2957,12 @@ class ModbusMaster extends EventEmitter {
2789
2957
  }
2790
2958
  try {
2791
2959
  validateResponse(frame, unit, fc);
2792
- if (frame.data.length < 6)
2960
+ if (frame.data.length < 6) {
2793
2961
  throw new Error('Insufficient data length');
2794
- if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode)
2962
+ }
2963
+ if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
2795
2964
  throw new Error('Invalid response');
2965
+ }
2796
2966
  const objects = [];
2797
2967
  let object = [];
2798
2968
  let totalBytes = 0;
@@ -2820,10 +2990,12 @@ class ModbusMaster extends EventEmitter {
2820
2990
  break;
2821
2991
  }
2822
2992
  }
2823
- if (objects.length !== frame.data[5])
2993
+ if (objects.length !== frame.data[5]) {
2824
2994
  throw new Error('Invalid response');
2825
- if (frame.data.length !== 6 + totalBytes)
2995
+ }
2996
+ if (frame.data.length !== 6 + totalBytes) {
2826
2997
  throw new Error('Invalid response');
2998
+ }
2827
2999
  frame.data = {
2828
3000
  readDeviceIDCode,
2829
3001
  conformityLevel: frame.data[2],
@@ -2870,7 +3042,7 @@ class ModbusMaster extends EventEmitter {
2870
3042
  });
2871
3043
  }
2872
3044
  /**
2873
- * Open the underlying physical layer and begin accepting connections.
3045
+ * Open the underlying physical layer and establish a connection.
2874
3046
  *
2875
3047
  * A `ModbusMaster` instance can only be opened once. Once {@link close}
2876
3048
  * is called — explicitly or because the physical layer disconnected —
@@ -2900,8 +3072,9 @@ class ModbusMaster extends EventEmitter {
2900
3072
  // (broadcasts have no session waiter; non-broadcasts still in the
2901
3073
  // pre-write-window haven't registered in session yet).
2902
3074
  for (const pending of this._pendingExchanges.values()) {
2903
- if (pending.settled)
3075
+ if (pending.settled) {
2904
3076
  continue;
3077
+ }
2905
3078
  pending.settled = true;
2906
3079
  const cb = pending.callback;
2907
3080
  if (cb) {
@@ -3059,23 +3232,68 @@ class ModbusSlave extends EventEmitter {
3059
3232
  const byteCount = (length + 7) >> 3;
3060
3233
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3061
3234
  pdu[0] = byteCount;
3062
- // Pack 8 booleans per byte without first zero-filling: accumulate into
3063
- // `acc` and write a full byte once each lane is finished. Saves the
3064
- // `pdu.fill(0, 1, ...)` pass and replaces N `|=` reads-modify-writes
3065
- // with N `read`s + ⌈N/8⌉ `write`s. Measured ~+5% on FC01 / ~+3% on FC02
3066
- // at max payload (2000 / 1968 coils) via benchmark/all-fcs.ts bisect.
3067
- let acc = 0;
3068
- let out = 1;
3069
- for (let i = 0; i < length; i++) {
3070
- if (coils[i])
3071
- acc |= 1 << (i & 7);
3072
- if ((i & 7) === 7) {
3073
- pdu[out++] = acc;
3074
- acc = 0;
3235
+ if (coils instanceof Uint8Array) {
3236
+ // Branchless fast path `coils[i]` is already 0/1, no boolean
3237
+ // coercion or conditional jumps. At max payload (2000 coils) this
3238
+ // avoids 2000 branch-predictor slots and boolean-to-number casts.
3239
+ let out = 1;
3240
+ const fullBytes = length >> 3;
3241
+ for (let i = 0; i < fullBytes; i++) {
3242
+ const base = i << 3;
3243
+ pdu[out++] =
3244
+ (coils[base] & 1) |
3245
+ ((coils[base + 1] & 1) << 1) |
3246
+ ((coils[base + 2] & 1) << 2) |
3247
+ ((coils[base + 3] & 1) << 3) |
3248
+ ((coils[base + 4] & 1) << 4) |
3249
+ ((coils[base + 5] & 1) << 5) |
3250
+ ((coils[base + 6] & 1) << 6) |
3251
+ ((coils[base + 7] & 1) << 7);
3252
+ }
3253
+ const rem = length & 7;
3254
+ if (rem) {
3255
+ const base = fullBytes << 3;
3256
+ let acc = coils[base] & 1;
3257
+ if (rem > 1) {
3258
+ acc |= (coils[base + 1] & 1) << 1;
3259
+ }
3260
+ if (rem > 2) {
3261
+ acc |= (coils[base + 2] & 1) << 2;
3262
+ }
3263
+ if (rem > 3) {
3264
+ acc |= (coils[base + 3] & 1) << 3;
3265
+ }
3266
+ if (rem > 4) {
3267
+ acc |= (coils[base + 4] & 1) << 4;
3268
+ }
3269
+ if (rem > 5) {
3270
+ acc |= (coils[base + 5] & 1) << 5;
3271
+ }
3272
+ if (rem > 6) {
3273
+ acc |= (coils[base + 6] & 1) << 6;
3274
+ }
3275
+ pdu[out] = acc;
3276
+ }
3277
+ }
3278
+ else {
3279
+ // Fallback for boolean[] — accumulate into `acc` and write a full byte
3280
+ // once each lane is finished. Saves N `|=` read-modify-writes on the
3281
+ // output buffer.
3282
+ let acc = 0;
3283
+ let out = 1;
3284
+ for (let i = 0; i < length; i++) {
3285
+ if (coils[i]) {
3286
+ acc |= 1 << (i & 7);
3287
+ }
3288
+ if ((i & 7) === 7) {
3289
+ pdu[out++] = acc;
3290
+ acc = 0;
3291
+ }
3292
+ }
3293
+ if ((length & 7) !== 0) {
3294
+ pdu[out] = acc;
3075
3295
  }
3076
3296
  }
3077
- if ((length & 7) !== 0)
3078
- pdu[out] = acc;
3079
3297
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3080
3298
  }
3081
3299
  catch (error) {
@@ -3106,19 +3324,62 @@ class ModbusSlave extends EventEmitter {
3106
3324
  const byteCount = (length + 7) >> 3;
3107
3325
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3108
3326
  pdu[0] = byteCount;
3109
- // Accumulator-based bit pack — see handleFC1 for the rationale.
3110
- let acc = 0;
3111
- let out = 1;
3112
- for (let i = 0; i < length; i++) {
3113
- if (discreteInputs[i])
3114
- acc |= 1 << (i & 7);
3115
- if ((i & 7) === 7) {
3116
- pdu[out++] = acc;
3117
- acc = 0;
3327
+ if (discreteInputs instanceof Uint8Array) {
3328
+ let out = 1;
3329
+ const fullBytes = length >> 3;
3330
+ for (let i = 0; i < fullBytes; i++) {
3331
+ const base = i << 3;
3332
+ pdu[out++] =
3333
+ (discreteInputs[base] & 1) |
3334
+ ((discreteInputs[base + 1] & 1) << 1) |
3335
+ ((discreteInputs[base + 2] & 1) << 2) |
3336
+ ((discreteInputs[base + 3] & 1) << 3) |
3337
+ ((discreteInputs[base + 4] & 1) << 4) |
3338
+ ((discreteInputs[base + 5] & 1) << 5) |
3339
+ ((discreteInputs[base + 6] & 1) << 6) |
3340
+ ((discreteInputs[base + 7] & 1) << 7);
3341
+ }
3342
+ const rem = length & 7;
3343
+ if (rem) {
3344
+ const base = fullBytes << 3;
3345
+ let acc = discreteInputs[base] & 1;
3346
+ if (rem > 1) {
3347
+ acc |= (discreteInputs[base + 1] & 1) << 1;
3348
+ }
3349
+ if (rem > 2) {
3350
+ acc |= (discreteInputs[base + 2] & 1) << 2;
3351
+ }
3352
+ if (rem > 3) {
3353
+ acc |= (discreteInputs[base + 3] & 1) << 3;
3354
+ }
3355
+ if (rem > 4) {
3356
+ acc |= (discreteInputs[base + 4] & 1) << 4;
3357
+ }
3358
+ if (rem > 5) {
3359
+ acc |= (discreteInputs[base + 5] & 1) << 5;
3360
+ }
3361
+ if (rem > 6) {
3362
+ acc |= (discreteInputs[base + 6] & 1) << 6;
3363
+ }
3364
+ pdu[out] = acc;
3365
+ }
3366
+ }
3367
+ else {
3368
+ let acc = 0;
3369
+ let out = 1;
3370
+ for (let i = 0; i < length; i++) {
3371
+ if (discreteInputs[i]) {
3372
+ acc |= 1 << (i & 7);
3373
+ }
3374
+ if ((i & 7) === 7) {
3375
+ pdu[out++] = acc;
3376
+ acc = 0;
3377
+ }
3378
+ }
3379
+ if ((length & 7) !== 0) {
3380
+ pdu[out] = acc;
3118
3381
  }
3119
3382
  }
3120
- if ((length & 7) !== 0)
3121
- pdu[out] = acc;
3122
3383
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3123
3384
  }
3124
3385
  catch (error) {
@@ -3151,11 +3412,12 @@ class ModbusSlave extends EventEmitter {
3151
3412
  // Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
3152
3413
  // while `writeUInt16BE` runs argument validation + bounds checks on each
3153
3414
  // call. At length=125 (FC3 max) that's 250 saved checks per request.
3415
+ let off = 1;
3154
3416
  for (let i = 0; i < length; i++) {
3155
3417
  const v = registers[i];
3156
- const off = 1 + i * 2;
3157
3418
  pdu[off] = (v >>> 8) & 0xff;
3158
3419
  pdu[off + 1] = v & 0xff;
3420
+ off += 2;
3159
3421
  }
3160
3422
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3161
3423
  }
@@ -3187,11 +3449,12 @@ class ModbusSlave extends EventEmitter {
3187
3449
  const pdu = Buffer.allocUnsafe(length * 2 + 1);
3188
3450
  pdu[0] = length * 2;
3189
3451
  // Inline big-endian write — see handleFC3 for the rationale.
3452
+ let off = 1;
3190
3453
  for (let i = 0; i < length; i++) {
3191
3454
  const v = registers[i];
3192
- const off = 1 + i * 2;
3193
3455
  pdu[off] = (v >>> 8) & 0xff;
3194
3456
  pdu[off + 1] = v & 0xff;
3457
+ off += 2;
3195
3458
  }
3196
3459
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3197
3460
  }
@@ -3270,12 +3533,42 @@ class ModbusSlave extends EventEmitter {
3270
3533
  return;
3271
3534
  }
3272
3535
  const value = new Array(length);
3273
- for (let i = 0; i < length; i++) {
3274
- // Bit math: kept as `Math.floor(i / 8)` / `i % 8` — V8 pattern-matches
3275
- // both forms with `i` as an integer loop counter to direct shifts, but
3276
- // measured ~+3% throughput vs the bit-shift form on this exact loop
3277
- // (benchmark/all-fcs.ts FC15 slave A/B, 8/8 paired samples).
3278
- value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
3536
+ let byteIdx = 5;
3537
+ let outIdx = 0;
3538
+ const fullBytes = length >> 3;
3539
+ for (let b = 0; b < fullBytes; b++) {
3540
+ const byte = frame.data[byteIdx++];
3541
+ value[outIdx++] = (byte & 0x01) > 0;
3542
+ value[outIdx++] = (byte & 0x02) > 0;
3543
+ value[outIdx++] = (byte & 0x04) > 0;
3544
+ value[outIdx++] = (byte & 0x08) > 0;
3545
+ value[outIdx++] = (byte & 0x10) > 0;
3546
+ value[outIdx++] = (byte & 0x20) > 0;
3547
+ value[outIdx++] = (byte & 0x40) > 0;
3548
+ value[outIdx++] = (byte & 0x80) > 0;
3549
+ }
3550
+ const rem = length & 7;
3551
+ if (rem) {
3552
+ const byte = frame.data[byteIdx];
3553
+ value[outIdx++] = (byte & 0x01) > 0;
3554
+ if (rem > 1) {
3555
+ value[outIdx++] = (byte & 0x02) > 0;
3556
+ }
3557
+ if (rem > 2) {
3558
+ value[outIdx++] = (byte & 0x04) > 0;
3559
+ }
3560
+ if (rem > 3) {
3561
+ value[outIdx++] = (byte & 0x08) > 0;
3562
+ }
3563
+ if (rem > 4) {
3564
+ value[outIdx++] = (byte & 0x10) > 0;
3565
+ }
3566
+ if (rem > 5) {
3567
+ value[outIdx++] = (byte & 0x20) > 0;
3568
+ }
3569
+ if (rem > 6) {
3570
+ value[outIdx++] = (byte & 0x40) > 0;
3571
+ }
3279
3572
  }
3280
3573
  try {
3281
3574
  if (model.writeMultipleCoils) {
@@ -3313,8 +3606,10 @@ class ModbusSlave extends EventEmitter {
3313
3606
  return;
3314
3607
  }
3315
3608
  const value = new Array(length);
3609
+ let off = 5;
3316
3610
  for (let i = 0; i < length; i++) {
3317
- value[i] = (frame.data[5 + i * 2] << 8) | frame.data[6 + i * 2];
3611
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3612
+ off += 2;
3318
3613
  }
3319
3614
  try {
3320
3615
  if (model.writeMultipleRegisters) {
@@ -3425,8 +3720,10 @@ class ModbusSlave extends EventEmitter {
3425
3720
  return;
3426
3721
  }
3427
3722
  const value = new Array(length.write);
3723
+ let off = 9;
3428
3724
  for (let i = 0; i < length.write; i++) {
3429
- value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
3725
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3726
+ off += 2;
3430
3727
  }
3431
3728
  try {
3432
3729
  await this._withIntervalLock(address.write, address.write + length.write, async () => {
@@ -3443,11 +3740,12 @@ class ModbusSlave extends EventEmitter {
3443
3740
  const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
3444
3741
  pdu[0] = length.read * 2;
3445
3742
  // Inline big-endian write — see handleFC3 for the rationale.
3743
+ let off = 1;
3446
3744
  for (let i = 0; i < length.read; i++) {
3447
3745
  const v = registers[i];
3448
- const off = 1 + i * 2;
3449
3746
  pdu[off] = (v >>> 8) & 0xff;
3450
3747
  pdu[off + 1] = v & 0xff;
3748
+ off += 2;
3451
3749
  }
3452
3750
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3453
3751
  }
@@ -3679,8 +3977,9 @@ class ModbusSlave extends EventEmitter {
3679
3977
  for (let i = 0; i < locks.length; i++) {
3680
3978
  const l = locks[i];
3681
3979
  if (l.lo < hi && lo < l.hi) {
3682
- if (overlap === null)
3980
+ if (overlap === null) {
3683
3981
  overlap = [];
3982
+ }
3684
3983
  overlap.push(l.promise);
3685
3984
  }
3686
3985
  }
@@ -3716,8 +4015,9 @@ class ModbusSlave extends EventEmitter {
3716
4015
  if (i !== -1) {
3717
4016
  // O(1) swap-and-pop since lock order doesn't matter for correctness.
3718
4017
  const last = locks.length - 1;
3719
- if (i !== last)
4018
+ if (i !== last) {
3720
4019
  locks[i] = locks[last];
4020
+ }
3721
4021
  locks.pop();
3722
4022
  }
3723
4023
  }