@flux-control/effect-modbus-rs 0.4.1 → 0.6.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.
Files changed (36) hide show
  1. package/README.md +343 -20
  2. package/dist/index.d.ts +62 -1
  3. package/dist/index.js +1046 -216
  4. package/dist/src/AsciiTransportService.d.ts +14 -10
  5. package/dist/src/RtuTransportService.d.ts +14 -10
  6. package/dist/src/TcpTransportService.d.ts +14 -10
  7. package/dist/src/WasmAsciiTransportService.d.ts +15 -11
  8. package/dist/src/WasmRtuTransportService.d.ts +15 -11
  9. package/dist/src/WasmWsTransportService.d.ts +14 -10
  10. package/dist/src/batching-client.d.ts +197 -0
  11. package/dist/src/connection.d.ts +6 -3
  12. package/dist/src/errors.d.ts +3 -0
  13. package/dist/src/mocks.d.ts +14 -8
  14. package/dist/src/modbus-client.d.ts +3 -1
  15. package/dist/src/read-debouncer.d.ts +100 -0
  16. package/dist/src/register-cache.d.ts +90 -0
  17. package/dist/src/register-plan.d.ts +212 -0
  18. package/dist/src/retry.d.ts +4 -4
  19. package/dist/src/shared-transport.d.ts +79 -5
  20. package/dist/src/span-attributes.d.ts +34 -0
  21. package/dist/src/write-debouncer.d.ts +134 -0
  22. package/dist/test/retry.test.d.ts +1 -0
  23. package/dist/test/shared-transport.test.d.ts +1 -0
  24. package/dist/test/span-attributes.test.d.ts +1 -0
  25. package/dist/test/upstream-options.test.d.ts +1 -0
  26. package/dist/test/wasm-mocks.test.d.ts +1 -0
  27. package/dist/test/write-debouncer.test.d.ts +1 -0
  28. package/package.json +5 -4
  29. /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
  30. /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
  31. /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
  32. /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
  33. /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
  34. /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
  35. /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
  36. /package/dist/{src/wasm-mocks.test.d.ts → test/resilience.test.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
-
4
1
  // src/errors.ts
5
2
  import { Data } from "effect";
6
3
  import { getModbusErrorCode, ModbusErrorCode } from "modbus-rs";
@@ -67,10 +64,19 @@ var defaultRetryableTags = {
67
64
  ModbusCircuitOpenError: true,
68
65
  ModbusInternalError: false
69
66
  };
70
- var allTags = Object.keys(defaultRetryableTags);
67
+ var allTags = [
68
+ "ModbusTimeoutError",
69
+ "ModbusTransportError",
70
+ "ModbusConnectionClosedError",
71
+ "ModbusExceptionError",
72
+ "ModbusInvalidArgumentError",
73
+ "ModbusNotConnectedError",
74
+ "ModbusCircuitOpenError",
75
+ "ModbusInternalError"
76
+ ];
71
77
  var toMillis = (input) => Duration.toMillis(Duration.fromInputUnsafe(input));
72
78
  var resolveRetryableTags = (options) => {
73
- const retryable = {};
79
+ const retryable = { ...defaultRetryableTags };
74
80
  for (const tag of allTags) {
75
81
  const entry = options.errors?.[tag];
76
82
  retryable[tag] = entry === undefined ? defaultRetryableTags[tag] : entry !== false;
@@ -85,19 +91,25 @@ var resolveCurve = (overrides, fallback) => ({
85
91
  });
86
92
  var resolveDelays = (options) => {
87
93
  const policyCurve = resolveCurve(options, defaultCurve);
88
- const delays = {};
94
+ const delays = {
95
+ ModbusTimeoutError: policyCurve,
96
+ ModbusTransportError: policyCurve,
97
+ ModbusConnectionClosedError: policyCurve,
98
+ ModbusExceptionError: policyCurve,
99
+ ModbusInvalidArgumentError: policyCurve,
100
+ ModbusNotConnectedError: policyCurve,
101
+ ModbusCircuitOpenError: policyCurve,
102
+ ModbusInternalError: policyCurve
103
+ };
89
104
  for (const tag of allTags) {
90
105
  const entry = options.errors?.[tag];
91
- delays[tag] = resolveCurve(typeof entry === "object" ? entry : {}, policyCurve);
106
+ const overrides = entry === undefined || entry === true || entry === false ? {} : entry;
107
+ delays[tag] = resolveCurve(overrides, policyCurve);
92
108
  }
93
109
  return delays;
94
110
  };
95
- var mergeOptions = (base, overrides) => overrides === undefined ? base : {
96
- ...base,
97
- ...overrides,
98
- ...base.errors || overrides.errors ? { errors: { ...base.errors, ...overrides.errors } } : {}
99
- };
100
- var makeRetryPolicy = (options = {}) => {
111
+ var mergeOptions = (base, overrides) => overrides === undefined ? base : base.errors || overrides.errors ? { ...base, ...overrides, errors: { ...base.errors, ...overrides.errors } } : { ...base, ...overrides };
112
+ var createRetryPolicy = (options = {}) => {
101
113
  const retryable = resolveRetryableTags(options);
102
114
  const delays = resolveDelays(options);
103
115
  const exceptions = options.retryableExceptions ?? retryableExceptionCodes;
@@ -123,15 +135,15 @@ var makeRetryPolicy = (options = {}) => {
123
135
  return { schedule, isRetryable };
124
136
  };
125
137
  var RetryPolicies = {
126
- none: (overrides) => makeRetryPolicy(mergeOptions({ maxRetries: 0 }, overrides)),
127
- serial: (overrides) => makeRetryPolicy(mergeOptions({
138
+ none: (overrides) => createRetryPolicy(mergeOptions({ maxRetries: 0 }, overrides)),
139
+ serial: (overrides) => createRetryPolicy(mergeOptions({
128
140
  maxRetries: 3,
129
141
  baseDelay: "50 millis",
130
142
  factor: 2,
131
143
  maxDelay: "1 second",
132
144
  errors: { ModbusTimeoutError: { baseDelay: "100 millis" } }
133
145
  }, overrides)),
134
- tcp: (overrides) => makeRetryPolicy(mergeOptions({
146
+ tcp: (overrides) => createRetryPolicy(mergeOptions({
135
147
  maxRetries: 4,
136
148
  baseDelay: "100 millis",
137
149
  factor: 2,
@@ -140,7 +152,7 @@ var RetryPolicies = {
140
152
  ModbusConnectionClosedError: { baseDelay: "250 millis", maxDelay: "10 seconds" }
141
153
  }
142
154
  }, overrides)),
143
- persistent: (overrides) => makeRetryPolicy(mergeOptions({
155
+ persistent: (overrides) => createRetryPolicy(mergeOptions({
144
156
  maxRetries: 10,
145
157
  baseDelay: "250 millis",
146
158
  factor: 2,
@@ -152,7 +164,7 @@ var retryModbus = (policy) => (self) => Effect.retry(self, policy.schedule);
152
164
  // src/connection.ts
153
165
  import { Data as Data2, Duration as Duration2, Effect as Effect2, Result, SubscriptionRef } from "effect";
154
166
  var ConnectionState = Data2.taggedEnum();
155
- var defaultReconnectPolicy = makeRetryPolicy({
167
+ var defaultReconnectPolicy = createRetryPolicy({
156
168
  maxRetries: 5,
157
169
  baseDelay: "250 millis",
158
170
  factor: 2,
@@ -206,22 +218,708 @@ var claimReconnect = (reconnect, state, resolved, scope, onClaim) => Effect2.gen
206
218
  yield* onClaim;
207
219
  yield* Effect2.forkIn(superviseReconnect(reconnect, state, resolved), scope);
208
220
  });
221
+ // src/register-plan.ts
222
+ var MODBUS_MAX_WRITE_REGISTERS = 123;
223
+ var MODBUS_MAX_READ_REGISTERS = 125;
224
+ var MAX_REGISTER_VALUE = 65535;
225
+ var MAX_REGISTER_ADDRESS = 65535;
226
+ var MIN_REGISTER_VALUE = -32768;
227
+ var assertPositiveInteger = (name, value) => {
228
+ if (!Number.isInteger(value) || value < 1) {
229
+ throw new RangeError(`${name} must be an integer of 1 or more, got ${value}`);
230
+ }
231
+ };
232
+ var assertTransactionLimit = (name, value, maximum) => {
233
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
234
+ throw new RangeError(`${name} must be an integer from 1 to ${maximum}, got ${value}`);
235
+ }
236
+ };
237
+ var assertAddress = (address) => {
238
+ if (!Number.isInteger(address) || address < 0 || address > MAX_REGISTER_ADDRESS) {
239
+ throw new RangeError(`address must be an integer from 0 to ${MAX_REGISTER_ADDRESS}, got ${address}`);
240
+ }
241
+ };
242
+ var encodeRegisterValue = (value) => {
243
+ if (!Number.isInteger(value) || value < MIN_REGISTER_VALUE || value > MAX_REGISTER_VALUE) {
244
+ throw new RangeError(`value must be an integer from ${MIN_REGISTER_VALUE} to ${MAX_REGISTER_VALUE}, got ${value}`);
245
+ }
246
+ return value & MAX_REGISTER_VALUE;
247
+ };
248
+ var planWrites = (writes, options = {}) => {
249
+ const maxRegistersPerWrite = options.maxRegistersPerWrite ?? MODBUS_MAX_WRITE_REGISTERS;
250
+ const minRunLength = options.minRunLength ?? 2;
251
+ assertTransactionLimit("maxRegistersPerWrite", maxRegistersPerWrite, MODBUS_MAX_WRITE_REGISTERS);
252
+ assertPositiveInteger("minRunLength", minRunLength);
253
+ const latest = new Map;
254
+ for (const write of writes) {
255
+ assertAddress(write.address);
256
+ latest.set(write.address, encodeRegisterValue(write.value));
257
+ }
258
+ if (latest.size === 0)
259
+ return [];
260
+ const addresses = Array.from(latest.keys()).sort((left, right) => left - right);
261
+ const steps = [];
262
+ const emitRun = (start, end) => {
263
+ const length = end - start;
264
+ if (length >= minRunLength) {
265
+ const values = new Uint16Array(length);
266
+ for (let index = 0;index < length; index += 1) {
267
+ values[index] = latest.get(addresses[start + index]);
268
+ }
269
+ steps.push({ kind: "multiple", address: addresses[start], values });
270
+ return;
271
+ }
272
+ for (let index = start;index < end; index += 1) {
273
+ const address = addresses[index];
274
+ steps.push({ kind: "single", address, value: latest.get(address) });
275
+ }
276
+ };
277
+ let runStart = 0;
278
+ for (let index = 1;index <= addresses.length; index += 1) {
279
+ const endsHere = index === addresses.length || addresses[index] !== addresses[index - 1] + 1 || index - runStart >= maxRegistersPerWrite;
280
+ if (endsHere) {
281
+ emitRun(runStart, index);
282
+ runStart = index;
283
+ }
284
+ }
285
+ return steps;
286
+ };
287
+ var planReads = (addresses, options = {}) => {
288
+ const maxRegistersPerRead = options.maxRegistersPerRead ?? MODBUS_MAX_READ_REGISTERS;
289
+ const maxGap = options.maxGap ?? 0;
290
+ assertTransactionLimit("maxRegistersPerRead", maxRegistersPerRead, MODBUS_MAX_READ_REGISTERS);
291
+ if (!Number.isInteger(maxGap) || maxGap < 0) {
292
+ throw new RangeError(`maxGap must be a non-negative integer, got ${maxGap}`);
293
+ }
294
+ for (const address of addresses)
295
+ assertAddress(address);
296
+ const sorted = Array.from(new Set(addresses)).sort((left, right) => left - right);
297
+ const spans = [];
298
+ if (sorted.length > 0) {
299
+ let start = sorted[0];
300
+ let end = sorted[0];
301
+ for (let index = 1;index < sorted.length; index += 1) {
302
+ const address = sorted[index];
303
+ const fitsGap = address - end - 1 <= maxGap;
304
+ const fitsSpan = address - start + 1 <= maxRegistersPerRead;
305
+ if (fitsGap && fitsSpan) {
306
+ end = address;
307
+ continue;
308
+ }
309
+ spans.push({ address: start, quantity: end - start + 1 });
310
+ start = address;
311
+ end = address;
312
+ }
313
+ spans.push({ address: start, quantity: end - start + 1 });
314
+ }
315
+ const locate = (address) => {
316
+ if (!Number.isInteger(address) || address < 0 || address > MAX_REGISTER_ADDRESS) {
317
+ return;
318
+ }
319
+ let low = 0;
320
+ let high = spans.length - 1;
321
+ while (low <= high) {
322
+ const middle = low + high >> 1;
323
+ const span = spans[middle];
324
+ if (address < span.address)
325
+ high = middle - 1;
326
+ else if (address >= span.address + span.quantity)
327
+ low = middle + 1;
328
+ else
329
+ return { span: middle, offset: address - span.address };
330
+ }
331
+ return;
332
+ };
333
+ return { spans, locate };
334
+ };
335
+ // src/register-cache.ts
336
+ var assertRegisterAddress = (address) => {
337
+ if (!Number.isInteger(address) || address < 0 || address > 65535) {
338
+ throw new RangeError(`address must be an integer from 0 to 65535, got ${address}`);
339
+ }
340
+ };
341
+ var createRegisterCache = () => {
342
+ const held = new Map;
343
+ const keyOf = (unitId, address) => `${unitId}:${address}`;
344
+ const unitGenerations = new Map;
345
+ let busGeneration = 0;
346
+ const generationOf = (unitId) => (unitGenerations.get(unitId) ?? 0) + busGeneration;
347
+ return {
348
+ filter: (unitId, writes) => {
349
+ const latest = new Map;
350
+ const suppressed = [];
351
+ for (const write of writes) {
352
+ assertRegisterAddress(write.address);
353
+ const encoded = encodeRegisterValue(write.value);
354
+ const superseded = latest.get(write.address);
355
+ if (superseded !== undefined)
356
+ suppressed.push(superseded.write);
357
+ latest.set(write.address, { write, encoded });
358
+ }
359
+ const pending = [];
360
+ for (const { encoded, write } of latest.values()) {
361
+ if (held.get(keyOf(unitId, write.address)) === encoded) {
362
+ suppressed.push(write);
363
+ } else {
364
+ pending.push(write);
365
+ }
366
+ }
367
+ return { pending, suppressed };
368
+ },
369
+ generationOf,
370
+ observe: (unitId, address, value, generation) => {
371
+ assertRegisterAddress(address);
372
+ if (generation !== undefined && generation !== generationOf(unitId))
373
+ return;
374
+ held.set(keyOf(unitId, address), encodeRegisterValue(value));
375
+ },
376
+ invalidate: (unitId) => {
377
+ if (unitId === undefined) {
378
+ held.clear();
379
+ busGeneration += 1;
380
+ return;
381
+ }
382
+ unitGenerations.set(unitId, (unitGenerations.get(unitId) ?? 0) + 1);
383
+ const prefix = `${unitId}:`;
384
+ for (const key of held.keys()) {
385
+ if (key.startsWith(prefix))
386
+ held.delete(key);
387
+ }
388
+ },
389
+ get size() {
390
+ return held.size;
391
+ }
392
+ };
393
+ };
394
+ // src/write-debouncer.ts
395
+ import { Clock, Deferred, Duration as Duration3, Effect as Effect3, Fiber, Semaphore } from "effect";
396
+ var createWriteDebouncer = (options) => Effect3.gen(function* () {
397
+ const windowMs = Duration3.toMillis(Duration3.fromInputUnsafe(options.window));
398
+ const maxHoldMs = options.maxHold === undefined ? windowMs * 4 : Duration3.toMillis(Duration3.fromInputUnsafe(options.maxHold));
399
+ const scope = yield* Effect3.scope;
400
+ const lock = yield* Semaphore.make(1);
401
+ const flushLock = yield* Semaphore.make(1);
402
+ const held = new Map;
403
+ let timer;
404
+ let nextTimerId = 0;
405
+ let openedAtMs;
406
+ let closed = false;
407
+ const scopeClosedError = () => {
408
+ const message = "The write debouncer scope has been closed";
409
+ return new ModbusNotConnectedError({ cause: new Error(message), message });
410
+ };
411
+ const ensureOpen = Effect3.suspend(() => closed ? Effect3.fail(scopeClosedError()) : Effect3.void);
412
+ const validateWrites = (writes) => Effect3.try({
413
+ try: () => {
414
+ for (const write of writes) {
415
+ if (!Number.isInteger(write.address) || write.address < 0 || write.address > 65535) {
416
+ throw new RangeError(`address must be an integer from 0 to 65535, got ${write.address}`);
417
+ }
418
+ encodeRegisterValue(write.value);
419
+ }
420
+ },
421
+ catch: (cause) => {
422
+ const error = cause instanceof Error ? cause : new Error(String(cause));
423
+ return new ModbusInvalidArgumentError({ cause: error, message: error.message });
424
+ }
425
+ });
426
+ const flushPending = (expectedTimer) => flushLock.withPermits(1)(Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
427
+ const taken = yield* lock.withPermits(1)(Effect3.sync(() => {
428
+ if (expectedTimer !== undefined && timer?.id !== expectedTimer.id)
429
+ return;
430
+ const activeTimer = timer;
431
+ timer = undefined;
432
+ const batch = Array.from(held.values());
433
+ held.clear();
434
+ openedAtMs = undefined;
435
+ return { activeTimer, batch };
436
+ }));
437
+ if (taken === undefined)
438
+ return;
439
+ if (expectedTimer === undefined)
440
+ taken.activeTimer?.fiber?.interruptUnsafe();
441
+ if (taken.batch.length === 0)
442
+ return;
443
+ const exit = yield* Effect3.exit(restore(options.flush(taken.batch.map((entry) => ({
444
+ address: entry.write.address,
445
+ value: entry.write.value,
446
+ attributes: entry.attributes
447
+ })))));
448
+ yield* Effect3.forEach(taken.batch.flatMap((entry) => entry.waiters), (waiter) => Deferred.done(waiter, exit), { discard: true });
449
+ })));
450
+ const enqueue = (writes, attributes, restartTimer) => Effect3.gen(function* () {
451
+ const waiter = yield* Deferred.make();
452
+ yield* lock.withPermits(1)(Effect3.gen(function* () {
453
+ if (closed)
454
+ return yield* scopeClosedError();
455
+ for (const write of writes) {
456
+ const superseded = held.get(write.address);
457
+ held.set(write.address, {
458
+ write,
459
+ attributes,
460
+ waiters: [...superseded?.waiters ?? [], waiter]
461
+ });
462
+ }
463
+ const now = yield* Clock.currentTimeMillis;
464
+ openedAtMs ??= now;
465
+ if (!restartTimer)
466
+ return;
467
+ const delayMs = Math.max(0, Math.min(windowMs, openedAtMs + maxHoldMs - now));
468
+ const previous = timer;
469
+ const state = { id: nextTimerId++ };
470
+ timer = state;
471
+ const forked = yield* Effect3.forkIn(Effect3.sleep(Duration3.millis(delayMs)).pipe(Effect3.andThen(flushPending(state))), scope);
472
+ state.fiber = forked;
473
+ if (timer?.id !== state.id)
474
+ forked.interruptUnsafe();
475
+ previous?.fiber?.interruptUnsafe();
476
+ }));
477
+ return waiter;
478
+ });
479
+ const issueDirectly = (writes, attributes) => flushLock.withPermits(1)(options.flush(writes.map((write) => ({ address: write.address, value: write.value, attributes }))));
480
+ const writeAll = (writes, attributes) => {
481
+ if (writes.length === 0)
482
+ return Effect3.void;
483
+ return Effect3.andThen(ensureOpen, Effect3.andThen(validateWrites(writes), windowMs <= 0 ? issueDirectly(writes, attributes) : Effect3.flatMap(enqueue(writes, attributes, true), Deferred.await)));
484
+ };
485
+ const writeAllNow = (writes, attributes) => {
486
+ if (writes.length === 0)
487
+ return Effect3.void;
488
+ return Effect3.andThen(ensureOpen, Effect3.andThen(validateWrites(writes), windowMs <= 0 ? issueDirectly(writes, attributes) : Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
489
+ const waiter = yield* enqueue(writes, attributes, false);
490
+ yield* Effect3.forkIn(flushPending(), scope);
491
+ return yield* restore(Deferred.await(waiter));
492
+ }))));
493
+ };
494
+ const write = (write, attributes) => writeAll([write], attributes);
495
+ const writeNow = (write, attributes) => writeAllNow([write], attributes);
496
+ const flush = Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
497
+ const fiber = yield* Effect3.forkIn(flushPending(), scope);
498
+ yield* restore(Fiber.await(fiber));
499
+ }));
500
+ yield* Effect3.addFinalizer(() => Effect3.andThen(Effect3.sync(() => {
501
+ closed = true;
502
+ }), lock.withPermits(1)(Effect3.sync(() => {
503
+ const abandoned = Array.from(held.values());
504
+ held.clear();
505
+ openedAtMs = undefined;
506
+ return abandoned;
507
+ })).pipe(Effect3.flatMap((abandoned) => abandoned.length === 0 ? Effect3.void : Effect3.logWarning(`Discarding ${abandoned.length} pending register write(s) at shutdown`).pipe(Effect3.andThen(Effect3.forEach(abandoned.flatMap((entry) => entry.waiters), Deferred.interrupt, { discard: true })))))));
508
+ return {
509
+ write,
510
+ writeNow,
511
+ writeAll,
512
+ writeAllNow,
513
+ flush,
514
+ get pending() {
515
+ return held.size;
516
+ }
517
+ };
518
+ });
519
+ // src/read-debouncer.ts
520
+ import { Deferred as Deferred2, Duration as Duration4, Effect as Effect4, Exit, Fiber as Fiber2, Semaphore as Semaphore2 } from "effect";
521
+ var malformedResponse = (message) => new ModbusTransportError({ cause: new Error(message), message });
522
+ var shortResponse = (address) => malformedResponse(`Response did not carry a value for register ${address}`);
523
+ var settleWith = (value, address) => (waiter) => value === undefined ? Deferred2.fail(waiter, shortResponse(address)) : Deferred2.succeed(waiter, value);
524
+ var createReadDebouncer = (options) => Effect4.gen(function* () {
525
+ const windowMs = Duration4.toMillis(Duration4.fromInputUnsafe(options.window));
526
+ const scope = yield* Effect4.scope;
527
+ const lock = yield* Semaphore2.make(1);
528
+ const collected = new Map;
529
+ let timer;
530
+ let nextTimerId = 0;
531
+ let closed = false;
532
+ const scopeClosedError = () => {
533
+ const message = "The read debouncer scope has been closed";
534
+ return new ModbusNotConnectedError({ cause: new Error(message), message });
535
+ };
536
+ const ensureOpen = Effect4.suspend(() => closed ? Effect4.fail(scopeClosedError()) : Effect4.void);
537
+ const validateAddresses = (addresses) => Effect4.try({
538
+ try: () => {
539
+ for (const address of addresses) {
540
+ if (!Number.isInteger(address) || address < 0 || address > 65535) {
541
+ throw new RangeError(`address must be an integer from 0 to 65535, got ${address}`);
542
+ }
543
+ }
544
+ },
545
+ catch: (cause) => {
546
+ const error = cause instanceof Error ? cause : new Error(String(cause));
547
+ return new ModbusInvalidArgumentError({ cause: error, message: error.message });
548
+ }
549
+ });
550
+ const issue = (addresses) => Effect4.gen(function* () {
551
+ const plan = yield* Effect4.try({
552
+ try: () => planReads(addresses, options.plan),
553
+ catch: (cause) => {
554
+ const error = cause instanceof Error ? cause : new Error(String(cause));
555
+ return new ModbusInvalidArgumentError({ cause: error, message: error.message });
556
+ }
557
+ });
558
+ const responses = yield* options.fetch(plan.spans);
559
+ if (responses.length !== plan.spans.length) {
560
+ return yield* malformedResponse(`Expected ${plan.spans.length} read response(s), received ${responses.length}`);
561
+ }
562
+ for (let index = 0;index < plan.spans.length; index += 1) {
563
+ const span = plan.spans[index];
564
+ const response = responses[index];
565
+ if (response.length !== span.quantity) {
566
+ return yield* malformedResponse(`Expected ${span.quantity} register(s) for span at address ${span.address}, ` + `received ${response.length}`);
567
+ }
568
+ }
569
+ return (address) => {
570
+ const location = plan.locate(address);
571
+ return location === undefined ? undefined : responses[location.span]?.[location.offset];
572
+ };
573
+ });
574
+ const issueDirectly = (addresses) => Effect4.flatMap(issue(addresses), (valueOf) => Effect4.forEach(addresses, (address) => {
575
+ const value = valueOf(address);
576
+ return value === undefined ? Effect4.fail(shortResponse(address)) : Effect4.succeed(value);
577
+ }));
578
+ const flushPending = (expectedTimer) => Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
579
+ const taken = yield* lock.withPermits(1)(Effect4.sync(() => {
580
+ if (expectedTimer !== undefined && timer?.id !== expectedTimer.id)
581
+ return;
582
+ const activeTimer = timer;
583
+ timer = undefined;
584
+ const batch = Array.from(collected.entries());
585
+ collected.clear();
586
+ return { activeTimer, batch };
587
+ }));
588
+ if (taken === undefined)
589
+ return;
590
+ if (expectedTimer === undefined)
591
+ taken.activeTimer?.fiber?.interruptUnsafe();
592
+ if (taken.batch.length === 0)
593
+ return;
594
+ const exit = yield* Effect4.exit(restore(issue(taken.batch.map(([address]) => address))));
595
+ yield* Effect4.forEach(taken.batch, ([address, waiters]) => {
596
+ const settle = Exit.isSuccess(exit) ? settleWith(exit.value(address), address) : (waiter) => Deferred2.failCause(waiter, exit.cause);
597
+ return Effect4.forEach(waiters, settle, { discard: true });
598
+ }, { discard: true });
599
+ }));
600
+ const enqueue = (addresses, startTimer) => Effect4.gen(function* () {
601
+ const waiters = yield* Effect4.forEach(addresses, () => Deferred2.make());
602
+ yield* lock.withPermits(1)(Effect4.gen(function* () {
603
+ if (closed)
604
+ return yield* scopeClosedError();
605
+ addresses.forEach((address, index) => {
606
+ const existing = collected.get(address);
607
+ if (existing === undefined)
608
+ collected.set(address, [waiters[index]]);
609
+ else
610
+ existing.push(waiters[index]);
611
+ });
612
+ }).pipe(Effect4.andThen(Effect4.suspend(() => {
613
+ if (!startTimer || timer !== undefined)
614
+ return Effect4.void;
615
+ const state = { id: nextTimerId++ };
616
+ timer = state;
617
+ return Effect4.map(Effect4.forkIn(Effect4.sleep(Duration4.millis(windowMs)).pipe(Effect4.andThen(flushPending(state))), scope), (forked) => {
618
+ state.fiber = forked;
619
+ if (timer?.id !== state.id)
620
+ forked.interruptUnsafe();
621
+ });
622
+ }))));
623
+ return waiters;
624
+ });
625
+ const readAll = (addresses) => {
626
+ if (addresses.length === 0)
627
+ return Effect4.succeed([]);
628
+ return Effect4.andThen(ensureOpen, Effect4.andThen(validateAddresses(addresses), windowMs <= 0 ? issueDirectly(addresses) : Effect4.flatMap(enqueue(addresses, true), (waiters) => Effect4.forEach(waiters, Deferred2.await))));
629
+ };
630
+ const readAllNow = (addresses) => {
631
+ if (addresses.length === 0)
632
+ return Effect4.succeed([]);
633
+ return Effect4.andThen(ensureOpen, Effect4.andThen(validateAddresses(addresses), windowMs <= 0 ? issueDirectly(addresses) : Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
634
+ const waiters = yield* enqueue(addresses, false);
635
+ yield* Effect4.forkIn(flushPending(), scope);
636
+ return yield* restore(Effect4.forEach(waiters, Deferred2.await));
637
+ }))));
638
+ };
639
+ const read = (address) => Effect4.map(readAll([address]), (values) => values[0]);
640
+ const readNow = (address) => Effect4.map(readAllNow([address]), (values) => values[0]);
641
+ const flush = Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
642
+ const fiber = yield* Effect4.forkIn(flushPending(), scope);
643
+ yield* restore(Fiber2.await(fiber));
644
+ }));
645
+ yield* Effect4.addFinalizer(() => Effect4.andThen(Effect4.sync(() => {
646
+ closed = true;
647
+ }), lock.withPermits(1)(Effect4.sync(() => {
648
+ const abandoned = Array.from(collected.values()).flat();
649
+ collected.clear();
650
+ return abandoned;
651
+ })).pipe(Effect4.flatMap((abandoned) => abandoned.length === 0 ? Effect4.void : Effect4.logWarning(`Discarding ${abandoned.length} pending register read(s) at shutdown`).pipe(Effect4.andThen(Effect4.forEach(abandoned, Deferred2.interrupt, { discard: true })))))));
652
+ return {
653
+ read,
654
+ readNow,
655
+ readAll,
656
+ readAllNow,
657
+ flush,
658
+ get pending() {
659
+ return collected.size;
660
+ }
661
+ };
662
+ });
663
+ // src/batching-client.ts
664
+ import {
665
+ Deferred as Deferred3,
666
+ Effect as Effect5,
667
+ Exit as Exit2,
668
+ Fiber as Fiber3,
669
+ Scope,
670
+ Semaphore as Semaphore3,
671
+ Stream,
672
+ SubscriptionRef as SubscriptionRef2
673
+ } from "effect";
674
+
675
+ // src/span-attributes.ts
676
+ var mergeSpanAttributes = (sources) => {
677
+ const collected = new Map;
678
+ for (const source of sources) {
679
+ if (source === undefined)
680
+ continue;
681
+ for (const [key, value] of Object.entries(source)) {
682
+ const entry = collected.get(key);
683
+ if (entry === undefined)
684
+ collected.set(key, { first: value, values: new Set([String(value)]) });
685
+ else
686
+ entry.values.add(String(value));
687
+ }
688
+ }
689
+ return Object.fromEntries(Array.from(collected, ([key, entry]) => [
690
+ key,
691
+ entry.values.size === 1 ? entry.first : Array.from(entry.values).join(",")
692
+ ]));
693
+ };
694
+
695
+ // src/batching-client.ts
696
+ var makeBatchingClient = (options) => Effect5.gen(function* () {
697
+ const { unitId, client, cache } = options;
698
+ const scope = yield* Effect5.scope;
699
+ const invalidArgument = (cause) => {
700
+ const error = cause instanceof Error ? cause : new Error(String(cause));
701
+ return new ModbusInvalidArgumentError({ cause: error, message: error.message });
702
+ };
703
+ const own = (registerCount, transactionCount, suppressedCount) => ({
704
+ "modbus.unit_ids": String(unitId),
705
+ "modbus.register_count": registerCount,
706
+ "modbus.transaction_count": transactionCount,
707
+ "modbus.suppressed_count": suppressedCount
708
+ });
709
+ const issueWrites = (batch) => Effect5.gen(function* () {
710
+ const proposed = batch.map((entry) => ({ address: entry.address, value: entry.value }));
711
+ const filtered = yield* Effect5.try({
712
+ try: () => cache?.filter(unitId, proposed),
713
+ catch: invalidArgument
714
+ });
715
+ const pending = filtered?.pending ?? proposed;
716
+ if (pending.length === 0)
717
+ return;
718
+ const plan = yield* Effect5.try({
719
+ try: () => {
720
+ const steps = planWrites(pending, options.plan?.writes);
721
+ if (pending.length === proposed.length)
722
+ return { steps, written: proposed };
723
+ const whole = planWrites(proposed, options.plan?.writes);
724
+ return whole.length < steps.length ? { steps: whole, written: proposed } : { steps, written: pending };
725
+ },
726
+ catch: invalidArgument
727
+ });
728
+ const { steps, written } = plan;
729
+ const carried = new Set(written.map((write) => write.address));
730
+ const suppressed = proposed.length - written.length;
731
+ yield* Effect5.forEach(steps, (step) => Effect5.suspend(() => {
732
+ const generation = cache?.generationOf(unitId);
733
+ return step.kind === "single" ? Effect5.tap(client.writeSingleRegister({ address: step.address, value: step.value }), () => Effect5.sync(() => cache?.observe(unitId, step.address, step.value, generation))) : Effect5.tap(client.writeMultipleRegisters({ address: step.address, values: step.values }), () => Effect5.sync(() => step.values.forEach((value, index) => cache?.observe(unitId, step.address + index, value, generation))));
734
+ }), { discard: true }).pipe(Effect5.tapError(() => Effect5.sync(() => cache?.invalidate(unitId))), Effect5.withSpan("modbus.write", {
735
+ kind: "client",
736
+ attributes: {
737
+ ...mergeSpanAttributes(batch.filter((entry) => carried.has(entry.address)).map((entry) => entry.attributes)),
738
+ ...own(written.length, steps.length, suppressed)
739
+ }
740
+ }));
741
+ });
742
+ const issueReads = (name, read) => (spans) => Effect5.forEach(spans, (span) => read({ address: span.address, quantity: span.quantity })).pipe(Effect5.withSpan(name, {
743
+ kind: "client",
744
+ attributes: own(spans.reduce((total, span) => total + span.quantity, 0), spans.length, 0)
745
+ }));
746
+ const writes = yield* createWriteDebouncer({
747
+ window: options.debounce?.writes?.window ?? 0,
748
+ maxHold: options.debounce?.writes?.maxHold,
749
+ flush: issueWrites
750
+ });
751
+ const holding = yield* createReadDebouncer({
752
+ window: options.debounce?.reads?.window ?? 0,
753
+ plan: options.plan?.reads,
754
+ fetch: issueReads("modbus.read", client.readHoldingRegisters)
755
+ });
756
+ const inputs = yield* createReadDebouncer({
757
+ window: options.debounce?.reads?.window ?? 0,
758
+ plan: options.plan?.reads,
759
+ fetch: issueReads("modbus.read", client.readInputRegisters)
760
+ });
761
+ const readerOf = (debouncer) => ({
762
+ read: debouncer.read,
763
+ readNow: debouncer.readNow,
764
+ readAll: debouncer.readAll,
765
+ readAllNow: debouncer.readAllNow
766
+ });
767
+ const flush = Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
768
+ const fiber = yield* Effect5.forkIn(Effect5.andThen(writes.flush, Effect5.andThen(holding.flush, inputs.flush)), scope);
769
+ yield* restore(Fiber3.await(fiber));
770
+ }));
771
+ const onShutdown = (action) => Effect5.addFinalizer(() => action.pipe(Effect5.tapError((error) => Effect5.logError(`Shutdown action for unit ${unitId} failed: ${error.message}`)), Effect5.orDie));
772
+ return {
773
+ unitId,
774
+ cache,
775
+ write: writes.write,
776
+ writeNow: writes.writeNow,
777
+ writeAll: writes.writeAll,
778
+ writeAllNow: writes.writeAllNow,
779
+ ...readerOf(holding),
780
+ inputs: readerOf(inputs),
781
+ flush,
782
+ onShutdown
783
+ };
784
+ });
785
+ var createBatchingRegistry = (deps) => {
786
+ const clientScope = Scope.forkUnsafe(deps.scope);
787
+ const clients = new Map;
788
+ const creating = new Map;
789
+ const writeLocks = new Map;
790
+ let shared;
791
+ let closed = false;
792
+ let watchingScope = false;
793
+ const scopeClosedError = () => {
794
+ const message = "The batching registry scope has been closed";
795
+ return new ModbusNotConnectedError({ cause: new Error(message), message });
796
+ };
797
+ const watchScope = Effect5.suspend(() => {
798
+ if (watchingScope)
799
+ return Effect5.void;
800
+ watchingScope = true;
801
+ return Scope.addFinalizer(clientScope, Effect5.gen(function* () {
802
+ closed = true;
803
+ const pending = Array.from(creating.values());
804
+ creating.clear();
805
+ yield* Effect5.forEach(pending, (deferred) => Deferred3.fail(deferred, scopeClosedError()), {
806
+ discard: true
807
+ });
808
+ }));
809
+ });
810
+ const sharedCache = Effect5.fnUntraced(function* () {
811
+ if (shared)
812
+ return shared;
813
+ const cache = createRegisterCache();
814
+ shared = cache;
815
+ yield* Effect5.forkIn(Stream.runForEach(SubscriptionRef2.changes(deps.connectionState), (state) => Effect5.sync(() => {
816
+ if (!ConnectionState.$is("Connected")(state))
817
+ cache.invalidate();
818
+ })), clientScope);
819
+ return cache;
820
+ });
821
+ const writeLockFor = (unitId) => {
822
+ const existing = writeLocks.get(unitId);
823
+ if (existing)
824
+ return existing;
825
+ const created = Semaphore3.makeUnsafe(1);
826
+ writeLocks.set(unitId, created);
827
+ return created;
828
+ };
829
+ const guardRawWrites = (unitId, operations) => {
830
+ const write = (operation) => writeLockFor(unitId).withPermits(1)(Effect5.suspend(() => {
831
+ if (!clients.has(unitId) && !creating.has(unitId))
832
+ return operation();
833
+ const message = `Raw register writes are disabled for unit ${unitId} because a batching client exists. ` + `Use the batching client for every holding-register write on that unit.`;
834
+ return Effect5.fail(new ModbusInvalidArgumentError({ cause: new Error(message), message }));
835
+ }));
836
+ return {
837
+ ...operations,
838
+ writeSingleRegister: (options) => write(() => operations.writeSingleRegister(options)),
839
+ writeMultipleRegisters: (options) => write(() => operations.writeMultipleRegisters(options)),
840
+ readWriteMultipleRegisters: (options) => write(() => operations.readWriteMultipleRegisters(options))
841
+ };
842
+ };
843
+ const alreadyDeclaredError = (unitId) => {
844
+ const message = `Unit ${unitId} already has a batching client. One unit has one batch, so a unit ` + `is declared once. Use batchingClient(${unitId}) to reach the existing one.`;
845
+ return new ModbusInvalidArgumentError({ cause: new Error(message), message });
846
+ };
847
+ const notDeclaredError = (unitId) => {
848
+ const message = `No batching client is declared for unit ${unitId}. Declare one with ` + `withBatchingClient(${unitId}) before reaching for it.`;
849
+ return new ModbusInvalidArgumentError({ cause: new Error(message), message });
850
+ };
851
+ return {
852
+ withBatchingClient: (unitId, options) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
853
+ yield* watchScope;
854
+ if (closed)
855
+ return yield* scopeClosedError();
856
+ const configuredCache = options?.cache;
857
+ const injected = configuredCache === undefined || configuredCache === true || configuredCache === false ? undefined : configuredCache;
858
+ const fresh = yield* Deferred3.make();
859
+ const election = yield* Effect5.sync(() => {
860
+ if (closed)
861
+ return { _tag: "Closed" };
862
+ if (clients.has(unitId) || creating.has(unitId)) {
863
+ return { _tag: "Declared" };
864
+ }
865
+ creating.set(unitId, fresh);
866
+ return { _tag: "Leader" };
867
+ });
868
+ if (election._tag === "Closed")
869
+ return yield* scopeClosedError();
870
+ if (election._tag === "Declared")
871
+ return yield* alreadyDeclaredError(unitId);
872
+ const build = writeLockFor(unitId).withPermits(1)(Effect5.gen(function* () {
873
+ const raw = yield* deps.withClient(unitId, { retry: options?.retry });
874
+ let cache;
875
+ if (options?.cache !== false)
876
+ cache = injected ?? (yield* sharedCache());
877
+ const client = yield* Scope.provide(makeBatchingClient({
878
+ unitId,
879
+ client: raw,
880
+ cache,
881
+ debounce: options?.debounce,
882
+ plan: options?.plan
883
+ }), clientScope);
884
+ return { cache, client };
885
+ }));
886
+ yield* Effect5.forkIn(Effect5.uninterruptible(Effect5.flatMap(Effect5.exit(Effect5.interruptible(build)), (exit) => Effect5.andThen(Effect5.sync(() => {
887
+ creating.delete(unitId);
888
+ if (Exit2.isSuccess(exit))
889
+ clients.set(unitId, exit.value.client);
890
+ }), Deferred3.done(fresh, Exit2.hasInterrupts(exit) ? Exit2.fail(scopeClosedError()) : Exit2.map(exit, ({ client }) => client))))), clientScope);
891
+ return yield* restore(Deferred3.await(fresh));
892
+ })),
893
+ batchingClient: (unitId) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
894
+ if (closed)
895
+ return yield* scopeClosedError();
896
+ const existing = clients.get(unitId);
897
+ if (existing)
898
+ return existing;
899
+ const pending = creating.get(unitId);
900
+ if (pending)
901
+ return yield* restore(Deferred3.await(pending));
902
+ return yield* notDeclaredError(unitId);
903
+ })),
904
+ guardRawWrites
905
+ };
906
+ };
209
907
  // src/AsciiTransportService.ts
210
908
  import { Context, Layer } from "effect";
211
909
 
212
910
  // src/mocks.ts
213
- import { Effect as Effect4, Schema, SubscriptionRef as SubscriptionRef2 } from "effect";
911
+ import { Effect as Effect7, Exit as Exit3, Schema, Scope as Scope2, SubscriptionRef as SubscriptionRef3 } from "effect";
214
912
  import {
215
913
  CoilState
216
914
  } from "modbus-rs";
217
915
 
218
916
  // src/modbus-client.ts
219
- import { Effect as Effect3 } from "effect";
220
- var wrap = (try_) => Effect3.tryPromise({
917
+ import { Effect as Effect6 } from "effect";
918
+ var wrap = (try_) => Effect6.tryPromise({
221
919
  try: try_,
222
- catch: (error) => toModbusError(error)
920
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
223
921
  });
224
- var makeEffectModbusClient = (client) => ({
922
+ var createEffectModbusClient = (client) => ({
225
923
  readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
226
924
  readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
227
925
  writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
@@ -240,7 +938,7 @@ var makeEffectModbusClient = (client) => ({
240
938
  });
241
939
  var withResilience = (operations, resilience) => {
242
940
  const run = (operation) => {
243
- const attempt = Effect3.andThen(resilience.guard, Effect3.suspend(operation)).pipe(Effect3.tapError((error) => resilience.report(error)));
941
+ const attempt = Effect6.andThen(resilience.guard, Effect6.suspend(operation)).pipe(Effect6.tap(() => resilience.onSuccess ?? Effect6.void), Effect6.tapError((error) => resilience.report(error)));
244
942
  return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
245
943
  };
246
944
  return {
@@ -278,10 +976,10 @@ var RegisterDefinition = Schema.Struct({
278
976
  });
279
977
  var SlaveDeviceDefinition = Schema.Struct({
280
978
  unitId: Schema.Number,
281
- coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
282
- discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
283
- holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
284
- inputRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([])))
979
+ coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
980
+ discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
981
+ holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
982
+ inputRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([])))
285
983
  });
286
984
  var SlaveDeviceDefinitions = Schema.Array(SlaveDeviceDefinition);
287
985
  var buildCoils = (defs) => {
@@ -309,8 +1007,8 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
309
1007
  message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
310
1008
  });
311
1009
  var makeMockModbusClient = (state, unitId) => ({
312
- readCoils: Effect4.fnUntraced(function* (opts) {
313
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
1010
+ readCoils: Effect7.fnUntraced(function* (opts) {
1011
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
314
1012
  if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
315
1013
  return yield* failOutOfRange("Coil", opts.address, opts.quantity);
316
1014
  }
@@ -320,8 +1018,8 @@ var makeMockModbusClient = (state, unitId) => ({
320
1018
  }
321
1019
  return result;
322
1020
  }),
323
- readDiscreteInputs: Effect4.fnUntraced(function* (opts) {
324
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
1021
+ readDiscreteInputs: Effect7.fnUntraced(function* (opts) {
1022
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
325
1023
  if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
326
1024
  return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
327
1025
  }
@@ -331,8 +1029,8 @@ var makeMockModbusClient = (state, unitId) => ({
331
1029
  }
332
1030
  return result;
333
1031
  }),
334
- readHoldingRegisters: Effect4.fnUntraced(function* (opts) {
335
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
1032
+ readHoldingRegisters: Effect7.fnUntraced(function* (opts) {
1033
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
336
1034
  if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
337
1035
  return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
338
1036
  }
@@ -342,8 +1040,8 @@ var makeMockModbusClient = (state, unitId) => ({
342
1040
  }
343
1041
  return new Uint16Array(result);
344
1042
  }),
345
- readInputRegisters: Effect4.fnUntraced(function* (opts) {
346
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
1043
+ readInputRegisters: Effect7.fnUntraced(function* (opts) {
1044
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
347
1045
  if (opts.address + opts.quantity > state.maxInputAddress + 1) {
348
1046
  return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
349
1047
  }
@@ -353,15 +1051,15 @@ var makeMockModbusClient = (state, unitId) => ({
353
1051
  }
354
1052
  return new Uint16Array(result);
355
1053
  }),
356
- writeSingleCoil: Effect4.fnUntraced(function* (opts) {
357
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
1054
+ writeSingleCoil: Effect7.fnUntraced(function* (opts) {
1055
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
358
1056
  if (opts.address > state.maxCoilAddress) {
359
1057
  return yield* failOutOfRange("Coil", opts.address);
360
1058
  }
361
1059
  state.coils.set(opts.address, opts.value === CoilState.On);
362
1060
  }),
363
- writeMultipleCoils: Effect4.fnUntraced(function* (opts) {
364
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
1061
+ writeMultipleCoils: Effect7.fnUntraced(function* (opts) {
1062
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
365
1063
  if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
366
1064
  return yield* failOutOfRange("Coil", opts.address, opts.values.length);
367
1065
  }
@@ -369,15 +1067,15 @@ var makeMockModbusClient = (state, unitId) => ({
369
1067
  state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
370
1068
  }
371
1069
  }),
372
- writeSingleRegister: Effect4.fnUntraced(function* (opts) {
373
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
1070
+ writeSingleRegister: Effect7.fnUntraced(function* (opts) {
1071
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
374
1072
  if (opts.address > state.maxHoldingAddress) {
375
1073
  return yield* failOutOfRange("HoldingRegister", opts.address);
376
1074
  }
377
1075
  state.holdingRegisters.set(opts.address, opts.value);
378
1076
  }),
379
- writeMultipleRegisters: Effect4.fnUntraced(function* (opts) {
380
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
1077
+ writeMultipleRegisters: Effect7.fnUntraced(function* (opts) {
1078
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
381
1079
  if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
382
1080
  return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
383
1081
  }
@@ -385,8 +1083,8 @@ var makeMockModbusClient = (state, unitId) => ({
385
1083
  state.holdingRegisters.set(opts.address + i, opts.values[i]);
386
1084
  }
387
1085
  }),
388
- readWriteMultipleRegisters: Effect4.fnUntraced(function* (opts) {
389
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
1086
+ readWriteMultipleRegisters: Effect7.fnUntraced(function* (opts) {
1087
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
390
1088
  if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
391
1089
  return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
392
1090
  }
@@ -402,34 +1100,34 @@ var makeMockModbusClient = (state, unitId) => ({
402
1100
  }
403
1101
  return new Uint16Array(result);
404
1102
  }),
405
- readFifoQueue: Effect4.fnUntraced(function* (_opts) {
1103
+ readFifoQueue: Effect7.fnUntraced(function* (_opts) {
406
1104
  return yield* new ModbusInvalidArgumentError({
407
1105
  cause: new Error("FIFO queue not yet supported in mock"),
408
1106
  message: "FIFO queue not yet supported in mock"
409
1107
  });
410
1108
  }),
411
- readFileRecord: Effect4.fnUntraced(function* (_opts) {
1109
+ readFileRecord: Effect7.fnUntraced(function* (_opts) {
412
1110
  return yield* new ModbusInvalidArgumentError({
413
1111
  cause: new Error("File records not yet supported in mock"),
414
1112
  message: "File records not yet supported in mock"
415
1113
  });
416
1114
  }),
417
- writeFileRecord: Effect4.fnUntraced(function* (_opts) {
1115
+ writeFileRecord: Effect7.fnUntraced(function* (_opts) {
418
1116
  return yield* new ModbusInvalidArgumentError({
419
1117
  cause: new Error("File records not yet supported in mock"),
420
1118
  message: "File records not yet supported in mock"
421
1119
  });
422
1120
  }),
423
- readExceptionStatus: Effect4.fnUntraced(function* () {
424
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
1121
+ readExceptionStatus: Effect7.fnUntraced(function* () {
1122
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
425
1123
  return 0;
426
1124
  }),
427
- diagnostics: Effect4.fnUntraced(function* (opts) {
428
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
1125
+ diagnostics: Effect7.fnUntraced(function* (opts) {
1126
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
429
1127
  return { subFunction: opts.subFunction, data: new Uint16Array };
430
1128
  }),
431
- readDeviceIdentification: Effect4.fnUntraced(function* (opts) {
432
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
1129
+ readDeviceIdentification: Effect7.fnUntraced(function* (opts) {
1130
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
433
1131
  return {
434
1132
  conformityLevel: 1,
435
1133
  moreFollows: false,
@@ -438,7 +1136,7 @@ var makeMockModbusClient = (state, unitId) => ({
438
1136
  };
439
1137
  })
440
1138
  });
441
- var makeMockTransport = (devices) => {
1139
+ var createMockTransport = (devices) => {
442
1140
  const deviceDefs = Schema.decodeUnknownSync(SlaveDeviceDefinitions)(devices);
443
1141
  const deviceStates = new Map;
444
1142
  for (const def of deviceDefs) {
@@ -457,144 +1155,260 @@ var makeMockTransport = (devices) => {
457
1155
  maxInputAddress: input.maxAddress
458
1156
  });
459
1157
  }
460
- return (options) => Effect4.gen(function* () {
461
- yield* Effect4.logDebug("Mock transport opened with devices:", deviceDefs);
462
- const connectionState = yield* SubscriptionRef2.make(ConnectionState.Connected());
463
- const supervised = options.reconnect ? resolveReconnect(options.reconnect) : null;
464
- const serviceScope = yield* Effect4.scope;
465
- const reconnectOnce = Effect4.andThen(Effect4.logDebug("Mock: reconnecting"), Effect4.suspend(() => {
1158
+ return (options) => Effect7.gen(function* () {
1159
+ yield* Effect7.logDebug("Mock transport opened with devices:", deviceDefs);
1160
+ const connectionState = yield* SubscriptionRef3.make(ConnectionState.Connected());
1161
+ const resolvedReconnect = resolveReconnect(options.reconnect ?? {});
1162
+ const supervised = options.reconnect ? resolvedReconnect : null;
1163
+ const serviceScope = yield* Effect7.scope;
1164
+ let closed = false;
1165
+ const transportClosed = () => new ModbusNotConnectedError({
1166
+ cause: new Error("Mock transport has been closed"),
1167
+ message: "Mock transport has been closed"
1168
+ });
1169
+ const ensureOpen = Effect7.suspend(() => closed ? Effect7.fail(transportClosed()) : Effect7.void);
1170
+ const closeTransport = Effect7.suspend(() => {
1171
+ if (closed)
1172
+ return Effect7.void;
1173
+ closed = true;
1174
+ return Effect7.andThen(Effect7.logDebug("Mock: closing transport"), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected()));
1175
+ });
1176
+ yield* Effect7.addFinalizer(() => closeTransport);
1177
+ const reconnectOnce = Effect7.andThen(ensureOpen, Effect7.andThen(Effect7.logDebug("Mock: reconnecting"), Effect7.suspend(() => {
466
1178
  const injected = options.reconnectFault?.();
467
- return injected ? Effect4.fail(injected) : Effect4.void;
468
- }));
1179
+ return injected ? Effect7.fail(injected) : Effect7.void;
1180
+ })));
469
1181
  const report = (error) => {
470
- if (!supervised || !supervised.triggers(error))
471
- return Effect4.void;
1182
+ if (closed || !resolvedReconnect.triggers(error))
1183
+ return Effect7.void;
1184
+ if (!supervised) {
1185
+ return SubscriptionRef3.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
1186
+ }
472
1187
  return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope);
473
1188
  };
474
- const guard = Effect4.andThen(supervised ? guardCircuit(connectionState) : Effect4.void, Effect4.suspend(() => {
1189
+ const guard = Effect7.andThen(ensureOpen, Effect7.andThen(supervised ? guardCircuit(connectionState) : Effect7.void, Effect7.suspend(() => {
475
1190
  const injected = options.fault?.();
476
- return injected ? Effect4.fail(injected) : Effect4.void;
477
- }));
1191
+ return injected ? Effect7.fail(injected) : Effect7.void;
1192
+ })));
1193
+ const onSuccess = supervised ? undefined : SubscriptionRef3.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current);
1194
+ const touchedUnits = new Set;
1195
+ const makeOperations = Effect7.fnUntraced(function* (unitId) {
1196
+ yield* ensureOpen;
1197
+ const state = deviceStates.get(unitId);
1198
+ if (!state) {
1199
+ return yield* new ModbusInvalidArgumentError({
1200
+ cause: new Error(`Device with unitId ${unitId} not found in mock configuration`),
1201
+ message: `Device with unitId ${unitId} not found in mock configuration`
1202
+ });
1203
+ }
1204
+ touchedUnits.add(unitId);
1205
+ return makeMockModbusClient(state, unitId);
1206
+ });
1207
+ const makeClient = Effect7.fnUntraced(function* (unitId, clientOptions) {
1208
+ const operations = yield* makeOperations(unitId);
1209
+ return withResilience(operations, {
1210
+ guard,
1211
+ onSuccess,
1212
+ report,
1213
+ policy: clientOptions?.retry ?? options.retry
1214
+ });
1215
+ });
1216
+ const batching = createBatchingRegistry({
1217
+ withClient: makeClient,
1218
+ connectionState,
1219
+ scope: serviceScope
1220
+ });
1221
+ const withClient = Effect7.fnUntraced(function* (unitId, clientOptions) {
1222
+ const operations = yield* makeOperations(unitId);
1223
+ return withResilience(batching.guardRawWrites(unitId, operations), {
1224
+ guard,
1225
+ onSuccess,
1226
+ report,
1227
+ policy: clientOptions?.retry ?? options.retry
1228
+ });
1229
+ });
478
1230
  return {
479
1231
  connectionState,
480
- withClient: Effect4.fnUntraced(function* (unitId, clientOptions) {
481
- const state = deviceStates.get(unitId);
482
- if (!state) {
483
- return yield* new ModbusInvalidArgumentError({
484
- cause: new Error(`Device with unitId ${unitId} not found in mock configuration`),
485
- message: `Device with unitId ${unitId} not found in mock configuration`
486
- });
487
- }
488
- return withResilience(makeMockModbusClient(state, unitId), {
489
- guard,
490
- report,
491
- policy: clientOptions?.retry ?? options.retry
492
- });
1232
+ withClient,
1233
+ withBatchingClient: (unitId, clientOptions) => Effect7.andThen(ensureOpen, batching.withBatchingClient(unitId, clientOptions)),
1234
+ batchingClient: (unitId) => Effect7.andThen(ensureOpen, batching.batchingClient(unitId)),
1235
+ get touchedUnits() {
1236
+ return new Set(touchedUnits);
1237
+ },
1238
+ setRequestTimeout: (_timeoutMs) => ensureOpen,
1239
+ clearRequestTimeout: () => ensureOpen,
1240
+ reconnect: () => Effect7.andThen(reconnectOnce, SubscriptionRef3.set(connectionState, ConnectionState.Connected())),
1241
+ close: () => Effect7.gen(function* () {
1242
+ if (closed)
1243
+ return;
1244
+ const scope = yield* Effect7.scope;
1245
+ yield* Scope2.close(scope, Exit3.void).pipe(Effect7.onExit(() => closeTransport));
493
1246
  }),
494
- setRequestTimeout: (_timeoutMs) => Effect4.void,
495
- clearRequestTimeout: () => Effect4.void,
496
- reconnect: () => Effect4.andThen(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
497
- close: () => Effect4.andThen(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
498
1247
  hasPendingRequests: () => false
499
1248
  };
500
1249
  });
501
1250
  };
502
1251
 
503
1252
  // src/shared-transport.ts
504
- import { Deferred, Effect as Effect5, Exit, Option, Ref, Scope, SubscriptionRef as SubscriptionRef3 } from "effect";
1253
+ import { Deferred as Deferred4, Duration as Duration6, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
505
1254
  var makeInFlight = () => Ref.make(Option.none());
506
- var singleFlight = (inFlight, work) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
507
- const fresh = yield* Deferred.make();
1255
+ var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
1256
+ const fresh = yield* Deferred4.make();
508
1257
  const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
509
1258
  onSome: (existing) => [[existing, false], current],
510
1259
  onNone: () => [[fresh, true], Option.some(fresh)]
511
1260
  }));
512
1261
  if (isLeader) {
513
- yield* Effect5.forkDetach(Effect5.interruptible(work).pipe(Effect5.onExit((exit) => Effect5.andThen(Ref.set(inFlight, Option.none()), Deferred.done(deferred, exit)))));
1262
+ yield* Effect8.forkDetach(Effect8.interruptible(work).pipe(Effect8.onExit((exit) => Effect8.andThen(Ref.set(inFlight, Option.none()), Deferred4.done(deferred, exit)))));
514
1263
  }
515
- return yield* restore(Deferred.await(deferred));
1264
+ return yield* restore(Deferred4.await(deferred));
516
1265
  }));
517
- function makeTransportScoped(transportKey, openMethod, serviceName, config) {
518
- return Effect5.fnUntraced(function* (options) {
519
- const { retry: transportRetry, reconnect: reconnectOptions, ...rest } = options;
520
- const openOptions = rest;
521
- const mod = config?.moduleSpecifier === "modbus-rs/web" ? yield* Effect5.promise(() => import("modbus-rs/web")) : yield* Effect5.promise(() => import("modbus-rs"));
522
- const TC = mod[transportKey];
1266
+ var loadTransportConstructor = async (transportKey, moduleSpecifier) => {
1267
+ if (moduleSpecifier === "modbus-rs/web") {
1268
+ const mod = await import("modbus-rs/web");
1269
+ switch (transportKey) {
1270
+ case "WasmAsciiTransport":
1271
+ return mod.WasmAsciiTransport;
1272
+ case "WasmRtuTransport":
1273
+ return mod.WasmRtuTransport;
1274
+ case "WasmWsTransport":
1275
+ return mod.WasmWsTransport;
1276
+ default:
1277
+ throw new Error(`${transportKey} is not exported by modbus-rs/web`);
1278
+ }
1279
+ }
1280
+ const mod = await import("modbus-rs");
1281
+ switch (transportKey) {
1282
+ case "AsyncAsciiTransport":
1283
+ return mod.AsyncAsciiTransport;
1284
+ case "AsyncRtuTransport":
1285
+ return mod.AsyncRtuTransport;
1286
+ case "AsyncTcpTransport":
1287
+ return mod.AsyncTcpTransport;
1288
+ default:
1289
+ throw new Error(`${transportKey} is not exported by modbus-rs`);
1290
+ }
1291
+ };
1292
+ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1293
+ return Effect8.fnUntraced(function* (options) {
1294
+ const { retry: transportRetry, reconnect: reconnectOptions } = options;
1295
+ const connectTimeout = options.connectTimeout === undefined ? undefined : Duration6.fromInputUnsafe(options.connectTimeout);
1296
+ const openOptions = { ...options };
1297
+ Reflect.deleteProperty(openOptions, "retry");
1298
+ Reflect.deleteProperty(openOptions, "reconnect");
1299
+ Reflect.deleteProperty(openOptions, "connectTimeout");
1300
+ const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
523
1301
  let transport = null;
524
1302
  const opening = yield* makeInFlight();
525
1303
  const reconnecting = yield* makeInFlight();
526
- const connectionState = yield* SubscriptionRef3.make(ConnectionState.Disconnected());
527
- const supervised = reconnectOptions ? resolveReconnect(reconnectOptions) : null;
528
- const serviceScope = yield* Effect5.scope;
1304
+ const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
1305
+ const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
1306
+ const supervised = reconnectOptions ? resolvedReconnect : null;
1307
+ const serviceScope = yield* Effect8.scope;
529
1308
  const clientSet = new Map;
530
1309
  let closed = false;
531
1310
  const transportClosed = () => new ModbusNotConnectedError({
532
1311
  cause: new Error("Transport has been closed"),
533
1312
  message: "Transport has been closed"
534
1313
  });
535
- const closeOrphan = (t) => Effect5.forkDetach(Effect5.ignore(Effect5.tryPromise(() => t.close())));
536
- const openTransport = Effect5.tryPromise({
1314
+ const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
1315
+ const limitConnect = (wait) => {
1316
+ if (connectTimeout === undefined)
1317
+ return wait;
1318
+ const message = `${serviceName}: connect did not complete within ${Duration6.format(connectTimeout)}`;
1319
+ return Effect8.timeoutOrElse(wait, {
1320
+ duration: connectTimeout,
1321
+ orElse: () => Effect8.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
1322
+ });
1323
+ };
1324
+ const openTransport = Effect8.tryPromise({
537
1325
  try: () => openMethod(TC, openOptions),
538
- catch: (error) => toModbusError(error)
539
- }).pipe(Effect5.tap((t) => closed ? closeOrphan(t) : Effect5.andThen(Effect5.sync(() => {
1326
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1327
+ }).pipe(Effect8.tap((t) => closed ? closeOrphan(t) : Effect8.andThen(Effect8.sync(() => {
540
1328
  transport = t;
541
- }), SubscriptionRef3.set(connectionState, ConnectionState.Connected()))));
542
- const ensureOpen = Effect5.fnUntraced(function* () {
1329
+ }), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
1330
+ const ensureOpen = Effect8.fnUntraced(function* () {
543
1331
  if (closed)
544
1332
  return yield* transportClosed();
545
1333
  if (transport)
546
1334
  return transport;
547
- const t = yield* singleFlight(opening, openTransport);
1335
+ const t = yield* limitConnect(singleFlight(opening, openTransport));
548
1336
  if (closed)
549
1337
  return yield* transportClosed();
550
1338
  return t;
551
1339
  });
552
- yield* Effect5.addFinalizer(() => {
1340
+ const closeTransport = Effect8.suspend(() => {
553
1341
  if (closed)
554
- return Effect5.void;
1342
+ return Effect8.void;
555
1343
  closed = true;
556
1344
  const t = transport;
557
1345
  if (!t)
558
- return SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
559
- return Effect5.andThen(Effect5.logDebug(`Closing ${serviceName}`), Effect5.andThen(Effect5.promise(() => t.close()), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected())));
1346
+ return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
1347
+ return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
1348
+ try: () => t.close(),
1349
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1350
+ }), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
560
1351
  });
1352
+ yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
561
1353
  const notConnectedMsg = "Transport is not connected. Call withClient() first.";
562
- const reconnectOnce = Effect5.suspend(() => {
1354
+ const reconnectTransport = (t) => limitConnect(singleFlight(reconnecting, Effect8.tryPromise({
1355
+ try: () => t.reconnect(),
1356
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1357
+ }).pipe(Effect8.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
1358
+ const reconnectOnce = Effect8.suspend(() => {
563
1359
  const t = transport;
564
1360
  if (closed || !t)
565
1361
  return transportClosed();
566
- return singleFlight(reconnecting, Effect5.tryPromise({
567
- try: () => t.reconnect(),
568
- catch: (error) => toModbusError(error)
569
- }).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
1362
+ return reconnectTransport(t);
570
1363
  });
571
1364
  const report = (error) => {
572
- if (!supervised || closed || !supervised.triggers(error))
573
- return Effect5.void;
574
- return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect5.logDebug(`${serviceName}: reconnecting after ${error.message}`));
1365
+ if (closed || !resolvedReconnect.triggers(error))
1366
+ return Effect8.void;
1367
+ if (!supervised) {
1368
+ return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
1369
+ }
1370
+ return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect8.logDebug(`${serviceName}: reconnecting after ${error.message}`));
575
1371
  };
576
1372
  const resilience = {
577
- guard: supervised ? guardCircuit(connectionState) : Effect5.void,
1373
+ guard: supervised ? guardCircuit(connectionState) : Effect8.void,
1374
+ onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
578
1375
  report
579
1376
  };
1377
+ const makeOperations = Effect8.fnUntraced(function* (unitId) {
1378
+ const t = yield* ensureOpen();
1379
+ let client = clientSet.get(unitId);
1380
+ if (!client) {
1381
+ client = yield* Effect8.try({
1382
+ try: () => t.createClient({ unitId }),
1383
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1384
+ });
1385
+ clientSet.set(unitId, client);
1386
+ }
1387
+ return createEffectModbusClient(client);
1388
+ });
1389
+ const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1390
+ const operations = yield* makeOperations(unitId);
1391
+ return withResilience(operations, {
1392
+ ...resilience,
1393
+ policy: clientOptions?.retry ?? transportRetry
1394
+ });
1395
+ });
1396
+ const batching = createBatchingRegistry({
1397
+ withClient: makeClient,
1398
+ connectionState,
1399
+ scope: serviceScope
1400
+ });
1401
+ const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1402
+ const operations = yield* makeOperations(unitId);
1403
+ return withResilience(batching.guardRawWrites(unitId, operations), {
1404
+ ...resilience,
1405
+ policy: clientOptions?.retry ?? transportRetry
1406
+ });
1407
+ });
580
1408
  return {
581
1409
  connectionState,
582
- withClient: Effect5.fnUntraced(function* (unitId, clientOptions) {
583
- const t = yield* ensureOpen();
584
- let client = clientSet.get(unitId);
585
- if (!client) {
586
- client = yield* Effect5.try({
587
- try: () => t.createClient({ unitId }),
588
- catch: (error) => toModbusError(error)
589
- });
590
- clientSet.set(unitId, client);
591
- }
592
- return withResilience(makeEffectModbusClient(client), {
593
- ...resilience,
594
- policy: clientOptions?.retry ?? transportRetry
595
- });
596
- }),
597
- setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
1410
+ withClient,
1411
+ setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
598
1412
  const t = transport;
599
1413
  if (!t || closed) {
600
1414
  return yield* new ModbusNotConnectedError({
@@ -604,7 +1418,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
604
1418
  }
605
1419
  t.setRequestTimeout(timeoutMs);
606
1420
  }),
607
- clearRequestTimeout: Effect5.fnUntraced(function* () {
1421
+ clearRequestTimeout: Effect8.fnUntraced(function* () {
608
1422
  const t = transport;
609
1423
  if (!t || closed) {
610
1424
  return yield* new ModbusNotConnectedError({
@@ -614,7 +1428,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
614
1428
  }
615
1429
  t.clearRequestTimeout();
616
1430
  }),
617
- reconnect: Effect5.fnUntraced(function* () {
1431
+ reconnect: Effect8.fnUntraced(function* () {
618
1432
  if (closed)
619
1433
  return yield* transportClosed();
620
1434
  const t = transport;
@@ -622,29 +1436,22 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
622
1436
  yield* ensureOpen();
623
1437
  return;
624
1438
  }
625
- yield* singleFlight(reconnecting, Effect5.tryPromise({
626
- try: () => t.reconnect(),
627
- catch: (error) => toModbusError(error)
628
- }).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
1439
+ yield* reconnectTransport(t);
629
1440
  if (closed)
630
1441
  return yield* transportClosed();
631
- yield* SubscriptionRef3.set(connectionState, ConnectionState.Connected());
1442
+ yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
632
1443
  }),
633
- close: Effect5.fnUntraced(function* () {
1444
+ close: Effect8.fnUntraced(function* () {
634
1445
  if (closed)
635
1446
  return;
636
- closed = true;
637
- yield* SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
638
- const t = transport;
639
- if (t) {
640
- yield* Effect5.tryPromise({
641
- try: () => t.close(),
642
- catch: (error) => toModbusError(error)
643
- });
644
- }
645
- const scope = yield* Effect5.scope;
646
- yield* Scope.close(scope, Exit.void);
1447
+ const scope = yield* Effect8.scope;
1448
+ yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
647
1449
  }),
1450
+ withBatchingClient: batching.withBatchingClient,
1451
+ batchingClient: batching.batchingClient,
1452
+ get touchedUnits() {
1453
+ return new Set(clientSet.keys());
1454
+ },
648
1455
  hasPendingRequests: () => {
649
1456
  if (closed)
650
1457
  return false;
@@ -659,10 +1466,12 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
659
1466
 
660
1467
  // src/AsciiTransportService.ts
661
1468
  class AsciiTransportService extends Context.Service()("AsciiTransportService") {
662
- static makeScoped = makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService");
1469
+ static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
1470
+ return transportConstructor.open(options);
1471
+ }, "AsciiTransportService");
663
1472
  static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
664
1473
  static makeMockTransport = (devices) => {
665
- const factory = makeMockTransport(devices);
1474
+ const factory = createMockTransport(devices);
666
1475
  return (options) => Layer.effect(AsciiTransportService, factory(options));
667
1476
  };
668
1477
  }
@@ -672,10 +1481,12 @@ import { Context as Context3, Layer as Layer3 } from "effect";
672
1481
  // src/RtuTransportService.ts
673
1482
  import { Context as Context2, Layer as Layer2 } from "effect";
674
1483
  class RtuTransportService extends Context2.Service()("RtuTransportService") {
675
- static makeScoped = makeTransportScoped("AsyncRtuTransport", (TC, options) => TC.open(options), "RtuTransportService");
1484
+ static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
1485
+ return transportConstructor.open(options);
1486
+ }, "RtuTransportService");
676
1487
  static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
677
1488
  static makeMockTransport = (devices) => {
678
- const factory = makeMockTransport(devices);
1489
+ const factory = createMockTransport(devices);
679
1490
  return (options) => Layer2.effect(RtuTransportService, factory(options));
680
1491
  };
681
1492
  }
@@ -689,17 +1500,19 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
689
1500
  return Layer3.flatMap(RtuTransportService.make(options), (context) => Layer3.succeed(SerialTransportService, Context3.get(context, RtuTransportService)));
690
1501
  }
691
1502
  static makeMockTransport = (devices) => {
692
- const factory = makeMockTransport(devices);
1503
+ const factory = createMockTransport(devices);
693
1504
  return (options) => Layer3.effect(SerialTransportService, factory(options));
694
1505
  };
695
1506
  }
696
1507
  // src/TcpTransportService.ts
697
1508
  import { Context as Context4, Layer as Layer4 } from "effect";
698
1509
  class TcpTransportService extends Context4.Service()("TcpTransportService") {
699
- static makeScoped = makeTransportScoped("AsyncTcpTransport", (TC, options) => TC.connect(options), "TcpTransportService");
1510
+ static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
1511
+ return transportConstructor.connect(options);
1512
+ }, "TcpTransportService");
700
1513
  static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
701
1514
  static makeMockTransport = (devices) => {
702
- const factory = makeMockTransport(devices);
1515
+ const factory = createMockTransport(devices);
703
1516
  return (options) => Layer4.effect(TcpTransportService, factory(options));
704
1517
  };
705
1518
  }
@@ -709,24 +1522,24 @@ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.g
709
1522
  const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
710
1523
  const server = yield* Effect9.tryPromise({
711
1524
  try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
712
- catch: (error) => toModbusError(error)
1525
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
713
1526
  });
714
1527
  yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
715
1528
  yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
716
1529
  try: () => server.shutdown(),
717
- catch: (error) => toModbusError(error)
1530
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
718
1531
  })), Effect9.catch(() => Effect9.void)));
719
1532
  }));
720
1533
  var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
721
1534
  const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
722
1535
  const server = yield* Effect9.tryPromise({
723
1536
  try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
724
- catch: (error) => toModbusError(error)
1537
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
725
1538
  });
726
1539
  yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
727
1540
  yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
728
1541
  try: () => server.shutdown(),
729
- catch: (error) => toModbusError(error)
1542
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
730
1543
  })), Effect9.catch(() => Effect9.void)));
731
1544
  }));
732
1545
  // src/TcpModbusServerService.ts
@@ -735,12 +1548,12 @@ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(fu
735
1548
  const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
736
1549
  const server = yield* Effect10.tryPromise({
737
1550
  try: () => AsyncTcpModbusServer.bind(options, handlers),
738
- catch: (error) => toModbusError(error)
1551
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
739
1552
  });
740
1553
  yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
741
1554
  yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
742
1555
  try: () => server.shutdown(),
743
- catch: (error) => toModbusError(error)
1556
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
744
1557
  })), Effect10.catch(() => Effect10.void)));
745
1558
  }));
746
1559
  // src/TcpGatewayService.ts
@@ -749,41 +1562,47 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.
749
1562
  const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
750
1563
  const gateway = yield* Effect11.tryPromise({
751
1564
  try: () => AsyncTcpGateway.bind(options, gatewayConfig),
752
- catch: (error) => toModbusError(error)
1565
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
753
1566
  });
754
1567
  yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
755
1568
  yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
756
1569
  try: () => gateway.shutdown(),
757
- catch: (error) => toModbusError(error)
1570
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
758
1571
  })), Effect11.catch(() => Effect11.void)));
759
1572
  }));
760
1573
  // src/WasmWsTransportService.ts
761
1574
  import { Context as Context5, Layer as Layer8 } from "effect";
762
1575
  class WasmWsTransportService extends Context5.Service()("WasmWsTransportService") {
763
- static makeScoped = makeTransportScoped("WasmWsTransport", (TC, options) => TC.connect(options), "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
1576
+ static makeScoped = createTransportScoped("WasmWsTransport", (transportConstructor, options) => {
1577
+ return transportConstructor.connect(options);
1578
+ }, "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
764
1579
  static make = (options) => Layer8.effect(WasmWsTransportService, WasmWsTransportService.makeScoped(options));
765
1580
  static makeMockTransport = (devices) => {
766
- const factory = makeMockTransport(devices);
1581
+ const factory = createMockTransport(devices);
767
1582
  return (options) => Layer8.effect(WasmWsTransportService, factory(options));
768
1583
  };
769
1584
  }
770
1585
  // src/WasmRtuTransportService.ts
771
1586
  import { Context as Context6, Layer as Layer9 } from "effect";
772
1587
  class WasmRtuTransportService extends Context6.Service()("WasmRtuTransportService") {
773
- static makeScoped = makeTransportScoped("WasmRtuTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
1588
+ static makeScoped = createTransportScoped("WasmRtuTransport", (transportConstructor, { port, ...rest }) => {
1589
+ return transportConstructor.open(port, rest);
1590
+ }, "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
774
1591
  static make = (options) => Layer9.effect(WasmRtuTransportService, WasmRtuTransportService.makeScoped(options));
775
1592
  static makeMockTransport = (devices) => {
776
- const factory = makeMockTransport(devices);
1593
+ const factory = createMockTransport(devices);
777
1594
  return (options) => Layer9.effect(WasmRtuTransportService, factory(options));
778
1595
  };
779
1596
  }
780
1597
  // src/WasmAsciiTransportService.ts
781
1598
  import { Context as Context7, Layer as Layer10 } from "effect";
782
1599
  class WasmAsciiTransportService extends Context7.Service()("WasmAsciiTransportService") {
783
- static makeScoped = makeTransportScoped("WasmAsciiTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
1600
+ static makeScoped = createTransportScoped("WasmAsciiTransport", (transportConstructor, { port, ...rest }) => {
1601
+ return transportConstructor.open(port, rest);
1602
+ }, "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
784
1603
  static make = (options) => Layer10.effect(WasmAsciiTransportService, WasmAsciiTransportService.makeScoped(options));
785
1604
  static makeMockTransport = (devices) => {
786
- const factory = makeMockTransport(devices);
1605
+ const factory = createMockTransport(devices);
787
1606
  return (options) => Layer10.effect(WasmAsciiTransportService, factory(options));
788
1607
  };
789
1608
  }
@@ -797,74 +1616,76 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
797
1616
  return Layer11.flatMap(WasmRtuTransportService.make(options), (context) => Layer11.succeed(WasmSerialTransportService, Context8.get(context, WasmRtuTransportService)));
798
1617
  }
799
1618
  static makeMockTransport = (devices) => {
800
- const factory = makeMockTransport(devices);
1619
+ const factory = createMockTransport(devices);
801
1620
  return (options) => Layer11.effect(WasmSerialTransportService, factory(options));
802
1621
  };
803
1622
  }
804
1623
  // src/WasmSerialPort.ts
805
- import { Effect as Effect15 } from "effect";
806
- var requestSerialPort = () => Effect15.tryPromise({
1624
+ import { Effect as Effect12 } from "effect";
1625
+ var requestSerialPort = () => Effect12.tryPromise({
807
1626
  try: async () => {
808
1627
  const mod = await import("modbus-rs/web");
809
1628
  return mod.requestSerialPort();
810
1629
  },
811
- catch: (error) => toModbusError(error)
1630
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
812
1631
  });
813
1632
  // src/WasmTcpServerService.ts
814
- import { Effect as Effect16, Layer as Layer12 } from "effect";
815
- var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect16.gen(function* () {
816
- const { WasmWsModbusServer } = yield* Effect16.promise(() => import("modbus-rs/web"));
817
- const server = yield* Effect16.tryPromise({
1633
+ import { Effect as Effect13, Layer as Layer12 } from "effect";
1634
+ var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
1635
+ const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
1636
+ const server = yield* Effect13.tryPromise({
818
1637
  try: () => WasmWsModbusServer.bind(options, handlers),
819
- catch: (error) => toModbusError(error)
1638
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
820
1639
  });
821
- yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
822
- yield* Effect16.forkScoped(Effect16.tryPromise({
1640
+ yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1641
+ yield* Effect13.forkScoped(Effect13.tryPromise({
823
1642
  try: () => server.serve(),
824
- catch: (error) => toModbusError(error)
825
- }).pipe(Effect16.catch((error) => Effect16.logError("WASM WS server loop ended", error))));
826
- yield* Effect16.addFinalizer(() => Effect16.logDebug("WASM WS server shutting down").pipe(Effect16.andThen(Effect16.tryPromise({
1643
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1644
+ }).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
1645
+ yield* Effect13.addFinalizer(() => Effect13.logDebug("WASM WS server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
827
1646
  try: () => server.shutdown(),
828
- catch: (error) => toModbusError(error)
829
- })), Effect16.catch(() => Effect16.void)));
1647
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1648
+ })), Effect13.catch(() => Effect13.void)));
830
1649
  }));
831
1650
  // src/WasmSerialModbusServerService.ts
832
- import { Effect as Effect17, Layer as Layer13 } from "effect";
833
- var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
834
- const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
835
- const server = yield* Effect17.tryPromise({
1651
+ import { Effect as Effect14, Layer as Layer13 } from "effect";
1652
+ var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1653
+ const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1654
+ const server = yield* Effect14.tryPromise({
836
1655
  try: () => WasmSerialModbusServer.bindRtu(options, handlers),
837
- catch: (error) => toModbusError(error)
1656
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
838
1657
  });
839
- yield* Effect17.logDebug("WASM serial RTU server bound");
840
- yield* Effect17.forkScoped(Effect17.tryPromise({
1658
+ yield* Effect14.logDebug("WASM serial RTU server bound");
1659
+ yield* Effect14.forkScoped(Effect14.tryPromise({
841
1660
  try: () => server.serve(),
842
- catch: (error) => toModbusError(error)
843
- }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial RTU server loop ended", error))));
844
- yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial RTU server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1661
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1662
+ }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
1663
+ yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial RTU server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
845
1664
  try: () => server.shutdown(),
846
- catch: (error) => toModbusError(error)
847
- })), Effect17.catch(() => Effect17.void)));
1665
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1666
+ })), Effect14.catch(() => Effect14.void)));
848
1667
  }));
849
- var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
850
- const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
851
- const server = yield* Effect17.tryPromise({
1668
+ var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1669
+ const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1670
+ const server = yield* Effect14.tryPromise({
852
1671
  try: () => WasmSerialModbusServer.bindAscii(options, handlers),
853
- catch: (error) => toModbusError(error)
1672
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
854
1673
  });
855
- yield* Effect17.logDebug("WASM serial ASCII server bound");
856
- yield* Effect17.forkScoped(Effect17.tryPromise({
1674
+ yield* Effect14.logDebug("WASM serial ASCII server bound");
1675
+ yield* Effect14.forkScoped(Effect14.tryPromise({
857
1676
  try: () => server.serve(),
858
- catch: (error) => toModbusError(error)
859
- }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial ASCII server loop ended", error))));
860
- yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial ASCII server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1677
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1678
+ }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
1679
+ yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial ASCII server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
861
1680
  try: () => server.shutdown(),
862
- catch: (error) => toModbusError(error)
863
- })), Effect17.catch(() => Effect17.void)));
1681
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1682
+ })), Effect14.catch(() => Effect14.void)));
864
1683
  }));
865
1684
  export {
866
1685
  AsciiTransportService,
867
1686
  ConnectionState,
1687
+ MODBUS_MAX_READ_REGISTERS,
1688
+ MODBUS_MAX_WRITE_REGISTERS,
868
1689
  ModbusCircuitOpenError,
869
1690
  ModbusConnectionClosedError,
870
1691
  ModbusExceptionError,
@@ -881,7 +1702,16 @@ export {
881
1702
  WasmRtuTransportService,
882
1703
  WasmSerialTransportService,
883
1704
  WasmWsTransportService,
884
- makeRetryPolicy,
1705
+ createBatchingRegistry,
1706
+ createReadDebouncer,
1707
+ createRegisterCache,
1708
+ createRetryPolicy,
1709
+ createWriteDebouncer,
1710
+ encodeRegisterValue,
1711
+ makeBatchingClient,
1712
+ mergeSpanAttributes,
1713
+ planReads,
1714
+ planWrites,
885
1715
  requestSerialPort,
886
1716
  retryModbus,
887
1717
  retryableExceptionCodes,