njs-modbus 3.2.0 → 3.4.0

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