@flux-control/effect-modbus-rs 0.4.0 → 0.5.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 (35) hide show
  1. package/README.md +317 -20
  2. package/dist/index.d.ts +62 -1
  3. package/dist/index.js +1043 -225
  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 +31 -4
  12. package/dist/src/mocks.d.ts +14 -8
  13. package/dist/src/modbus-client.d.ts +3 -1
  14. package/dist/src/read-debouncer.d.ts +100 -0
  15. package/dist/src/register-cache.d.ts +90 -0
  16. package/dist/src/register-plan.d.ts +212 -0
  17. package/dist/src/retry.d.ts +4 -4
  18. package/dist/src/shared-transport.d.ts +60 -4
  19. package/dist/src/span-attributes.d.ts +34 -0
  20. package/dist/src/write-debouncer.d.ts +134 -0
  21. package/dist/test/retry.test.d.ts +1 -0
  22. package/dist/test/shared-transport.test.d.ts +1 -0
  23. package/dist/test/span-attributes.test.d.ts +1 -0
  24. package/dist/test/upstream-options.test.d.ts +1 -0
  25. package/dist/test/wasm-mocks.test.d.ts +1 -0
  26. package/dist/test/write-debouncer.test.d.ts +1 -0
  27. package/package.json +6 -4
  28. /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
  29. /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
  30. /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
  31. /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
  32. /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
  33. /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
  34. /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
  35. /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,
@@ -198,22 +210,716 @@ var superviseReconnect = (reconnect, state, resolved) => Effect2.gen(function* (
198
210
  return;
199
211
  }
200
212
  });
213
+ var claimReconnect = (reconnect, state, resolved, scope, onClaim) => Effect2.gen(function* () {
214
+ const claimed = yield* SubscriptionRef.modify(state, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
215
+ if (!claimed)
216
+ return;
217
+ if (onClaim)
218
+ yield* onClaim;
219
+ yield* Effect2.forkIn(superviseReconnect(reconnect, state, resolved), scope);
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
+ };
201
907
  // src/AsciiTransportService.ts
202
908
  import { Context, Layer } from "effect";
203
909
 
204
910
  // src/mocks.ts
205
- 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";
206
912
  import {
207
913
  CoilState
208
914
  } from "modbus-rs";
209
915
 
210
916
  // src/modbus-client.ts
211
- import { Effect as Effect3 } from "effect";
212
- var wrap = (try_) => Effect3.tryPromise({
917
+ import { Effect as Effect6 } from "effect";
918
+ var wrap = (try_) => Effect6.tryPromise({
213
919
  try: try_,
214
- catch: (error) => toModbusError(error)
920
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
215
921
  });
216
- var makeEffectModbusClient = (client) => ({
922
+ var createEffectModbusClient = (client) => ({
217
923
  readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
218
924
  readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
219
925
  writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
@@ -232,7 +938,7 @@ var makeEffectModbusClient = (client) => ({
232
938
  });
233
939
  var withResilience = (operations, resilience) => {
234
940
  const run = (operation) => {
235
- 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)));
236
942
  return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
237
943
  };
238
944
  return {
@@ -270,10 +976,10 @@ var RegisterDefinition = Schema.Struct({
270
976
  });
271
977
  var SlaveDeviceDefinition = Schema.Struct({
272
978
  unitId: Schema.Number,
273
- coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
274
- discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
275
- holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect4.succeed([]))),
276
- 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([])))
277
983
  });
278
984
  var SlaveDeviceDefinitions = Schema.Array(SlaveDeviceDefinition);
279
985
  var buildCoils = (defs) => {
@@ -301,8 +1007,8 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
301
1007
  message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
302
1008
  });
303
1009
  var makeMockModbusClient = (state, unitId) => ({
304
- readCoils: Effect4.fnUntraced(function* (opts) {
305
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
1010
+ readCoils: Effect7.fnUntraced(function* (opts) {
1011
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
306
1012
  if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
307
1013
  return yield* failOutOfRange("Coil", opts.address, opts.quantity);
308
1014
  }
@@ -312,8 +1018,8 @@ var makeMockModbusClient = (state, unitId) => ({
312
1018
  }
313
1019
  return result;
314
1020
  }),
315
- readDiscreteInputs: Effect4.fnUntraced(function* (opts) {
316
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
1021
+ readDiscreteInputs: Effect7.fnUntraced(function* (opts) {
1022
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
317
1023
  if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
318
1024
  return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
319
1025
  }
@@ -323,8 +1029,8 @@ var makeMockModbusClient = (state, unitId) => ({
323
1029
  }
324
1030
  return result;
325
1031
  }),
326
- readHoldingRegisters: Effect4.fnUntraced(function* (opts) {
327
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
1032
+ readHoldingRegisters: Effect7.fnUntraced(function* (opts) {
1033
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
328
1034
  if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
329
1035
  return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
330
1036
  }
@@ -334,8 +1040,8 @@ var makeMockModbusClient = (state, unitId) => ({
334
1040
  }
335
1041
  return new Uint16Array(result);
336
1042
  }),
337
- readInputRegisters: Effect4.fnUntraced(function* (opts) {
338
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
1043
+ readInputRegisters: Effect7.fnUntraced(function* (opts) {
1044
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
339
1045
  if (opts.address + opts.quantity > state.maxInputAddress + 1) {
340
1046
  return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
341
1047
  }
@@ -345,15 +1051,15 @@ var makeMockModbusClient = (state, unitId) => ({
345
1051
  }
346
1052
  return new Uint16Array(result);
347
1053
  }),
348
- writeSingleCoil: Effect4.fnUntraced(function* (opts) {
349
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
1054
+ writeSingleCoil: Effect7.fnUntraced(function* (opts) {
1055
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
350
1056
  if (opts.address > state.maxCoilAddress) {
351
1057
  return yield* failOutOfRange("Coil", opts.address);
352
1058
  }
353
1059
  state.coils.set(opts.address, opts.value === CoilState.On);
354
1060
  }),
355
- writeMultipleCoils: Effect4.fnUntraced(function* (opts) {
356
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
1061
+ writeMultipleCoils: Effect7.fnUntraced(function* (opts) {
1062
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
357
1063
  if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
358
1064
  return yield* failOutOfRange("Coil", opts.address, opts.values.length);
359
1065
  }
@@ -361,15 +1067,15 @@ var makeMockModbusClient = (state, unitId) => ({
361
1067
  state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
362
1068
  }
363
1069
  }),
364
- writeSingleRegister: Effect4.fnUntraced(function* (opts) {
365
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
1070
+ writeSingleRegister: Effect7.fnUntraced(function* (opts) {
1071
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
366
1072
  if (opts.address > state.maxHoldingAddress) {
367
1073
  return yield* failOutOfRange("HoldingRegister", opts.address);
368
1074
  }
369
1075
  state.holdingRegisters.set(opts.address, opts.value);
370
1076
  }),
371
- writeMultipleRegisters: Effect4.fnUntraced(function* (opts) {
372
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
1077
+ writeMultipleRegisters: Effect7.fnUntraced(function* (opts) {
1078
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
373
1079
  if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
374
1080
  return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
375
1081
  }
@@ -377,8 +1083,8 @@ var makeMockModbusClient = (state, unitId) => ({
377
1083
  state.holdingRegisters.set(opts.address + i, opts.values[i]);
378
1084
  }
379
1085
  }),
380
- readWriteMultipleRegisters: Effect4.fnUntraced(function* (opts) {
381
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
1086
+ readWriteMultipleRegisters: Effect7.fnUntraced(function* (opts) {
1087
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
382
1088
  if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
383
1089
  return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
384
1090
  }
@@ -394,34 +1100,34 @@ var makeMockModbusClient = (state, unitId) => ({
394
1100
  }
395
1101
  return new Uint16Array(result);
396
1102
  }),
397
- readFifoQueue: Effect4.fnUntraced(function* (_opts) {
1103
+ readFifoQueue: Effect7.fnUntraced(function* (_opts) {
398
1104
  return yield* new ModbusInvalidArgumentError({
399
1105
  cause: new Error("FIFO queue not yet supported in mock"),
400
1106
  message: "FIFO queue not yet supported in mock"
401
1107
  });
402
1108
  }),
403
- readFileRecord: Effect4.fnUntraced(function* (_opts) {
1109
+ readFileRecord: Effect7.fnUntraced(function* (_opts) {
404
1110
  return yield* new ModbusInvalidArgumentError({
405
1111
  cause: new Error("File records not yet supported in mock"),
406
1112
  message: "File records not yet supported in mock"
407
1113
  });
408
1114
  }),
409
- writeFileRecord: Effect4.fnUntraced(function* (_opts) {
1115
+ writeFileRecord: Effect7.fnUntraced(function* (_opts) {
410
1116
  return yield* new ModbusInvalidArgumentError({
411
1117
  cause: new Error("File records not yet supported in mock"),
412
1118
  message: "File records not yet supported in mock"
413
1119
  });
414
1120
  }),
415
- readExceptionStatus: Effect4.fnUntraced(function* () {
416
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
1121
+ readExceptionStatus: Effect7.fnUntraced(function* () {
1122
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
417
1123
  return 0;
418
1124
  }),
419
- diagnostics: Effect4.fnUntraced(function* (opts) {
420
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
1125
+ diagnostics: Effect7.fnUntraced(function* (opts) {
1126
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
421
1127
  return { subFunction: opts.subFunction, data: new Uint16Array };
422
1128
  }),
423
- readDeviceIdentification: Effect4.fnUntraced(function* (opts) {
424
- yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
1129
+ readDeviceIdentification: Effect7.fnUntraced(function* (opts) {
1130
+ yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
425
1131
  return {
426
1132
  conformityLevel: 1,
427
1133
  moreFollows: false,
@@ -430,7 +1136,7 @@ var makeMockModbusClient = (state, unitId) => ({
430
1136
  };
431
1137
  })
432
1138
  });
433
- var makeMockTransport = (devices) => {
1139
+ var createMockTransport = (devices) => {
434
1140
  const deviceDefs = Schema.decodeUnknownSync(SlaveDeviceDefinitions)(devices);
435
1141
  const deviceStates = new Map;
436
1142
  for (const def of deviceDefs) {
@@ -449,94 +1155,168 @@ var makeMockTransport = (devices) => {
449
1155
  maxInputAddress: input.maxAddress
450
1156
  });
451
1157
  }
452
- return (options) => Effect4.gen(function* () {
453
- yield* Effect4.logDebug("Mock transport opened with devices:", deviceDefs);
454
- const connectionState = yield* SubscriptionRef2.make(ConnectionState.Connected());
455
- const supervised = options.reconnect ? resolveReconnect(options.reconnect) : null;
456
- const serviceScope = yield* Effect4.scope;
457
- 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(() => {
458
1178
  const injected = options.reconnectFault?.();
459
- return injected ? Effect4.fail(injected) : Effect4.void;
460
- }));
1179
+ return injected ? Effect7.fail(injected) : Effect7.void;
1180
+ })));
461
1181
  const report = (error) => {
462
- if (!supervised || !supervised.triggers(error))
463
- return Effect4.void;
464
- return Effect4.gen(function* () {
465
- const claimed = yield* SubscriptionRef2.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
466
- if (!claimed)
467
- return;
468
- yield* Effect4.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
469
- });
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
+ }
1187
+ return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope);
470
1188
  };
471
- 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(() => {
472
1190
  const injected = options.fault?.();
473
- return injected ? Effect4.fail(injected) : Effect4.void;
474
- }));
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
+ });
475
1230
  return {
476
1231
  connectionState,
477
- withClient: Effect4.fnUntraced(function* (unitId, clientOptions) {
478
- const state = deviceStates.get(unitId);
479
- if (!state) {
480
- return yield* new ModbusInvalidArgumentError({
481
- cause: new Error(`Device with unitId ${unitId} not found in mock configuration`),
482
- message: `Device with unitId ${unitId} not found in mock configuration`
483
- });
484
- }
485
- return withResilience(makeMockModbusClient(state, unitId), {
486
- guard,
487
- report,
488
- policy: clientOptions?.retry ?? options.retry
489
- });
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));
490
1246
  }),
491
- setRequestTimeout: (_timeoutMs) => Effect4.void,
492
- clearRequestTimeout: () => Effect4.void,
493
- reconnect: () => Effect4.andThen(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
494
- close: () => Effect4.andThen(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
495
1247
  hasPendingRequests: () => false
496
1248
  };
497
1249
  });
498
1250
  };
499
1251
 
500
1252
  // src/shared-transport.ts
501
- import { Deferred, Effect as Effect5, Exit, Option, Ref, Scope, SubscriptionRef as SubscriptionRef3 } from "effect";
1253
+ import { Deferred as Deferred4, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
502
1254
  var makeInFlight = () => Ref.make(Option.none());
503
- var singleFlight = (inFlight, work) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
504
- const fresh = yield* Deferred.make();
1255
+ var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
1256
+ const fresh = yield* Deferred4.make();
505
1257
  const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
506
1258
  onSome: (existing) => [[existing, false], current],
507
1259
  onNone: () => [[fresh, true], Option.some(fresh)]
508
1260
  }));
509
1261
  if (isLeader) {
510
- 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)))));
511
1263
  }
512
- return yield* restore(Deferred.await(deferred));
1264
+ return yield* restore(Deferred4.await(deferred));
513
1265
  }));
514
- function makeTransportScoped(transportKey, openMethod, serviceName, config) {
515
- return Effect5.fnUntraced(function* (options) {
516
- const { retry: transportRetry, reconnect: reconnectOptions, ...rest } = options;
517
- const openOptions = rest;
518
- const mod = config?.moduleSpecifier === "modbus-rs/web" ? yield* Effect5.promise(() => import("modbus-rs/web")) : yield* Effect5.promise(() => import("modbus-rs"));
519
- 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 openOptions = { ...options };
1296
+ Reflect.deleteProperty(openOptions, "retry");
1297
+ Reflect.deleteProperty(openOptions, "reconnect");
1298
+ const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
520
1299
  let transport = null;
521
1300
  const opening = yield* makeInFlight();
522
1301
  const reconnecting = yield* makeInFlight();
523
- const connectionState = yield* SubscriptionRef3.make(ConnectionState.Disconnected());
524
- const supervised = reconnectOptions ? resolveReconnect(reconnectOptions) : null;
525
- const serviceScope = yield* Effect5.scope;
1302
+ const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
1303
+ const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
1304
+ const supervised = reconnectOptions ? resolvedReconnect : null;
1305
+ const serviceScope = yield* Effect8.scope;
526
1306
  const clientSet = new Map;
527
1307
  let closed = false;
528
1308
  const transportClosed = () => new ModbusNotConnectedError({
529
1309
  cause: new Error("Transport has been closed"),
530
1310
  message: "Transport has been closed"
531
1311
  });
532
- const closeOrphan = (t) => Effect5.forkDetach(Effect5.ignore(Effect5.tryPromise(() => t.close())));
533
- const openTransport = Effect5.tryPromise({
1312
+ const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
1313
+ const openTransport = Effect8.tryPromise({
534
1314
  try: () => openMethod(TC, openOptions),
535
- catch: (error) => toModbusError(error)
536
- }).pipe(Effect5.tap((t) => closed ? closeOrphan(t) : Effect5.andThen(Effect5.sync(() => {
1315
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1316
+ }).pipe(Effect8.tap((t) => closed ? closeOrphan(t) : Effect8.andThen(Effect8.sync(() => {
537
1317
  transport = t;
538
- }), SubscriptionRef3.set(connectionState, ConnectionState.Connected()))));
539
- const ensureOpen = Effect5.fnUntraced(function* () {
1318
+ }), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
1319
+ const ensureOpen = Effect8.fnUntraced(function* () {
540
1320
  if (closed)
541
1321
  return yield* transportClosed();
542
1322
  if (transport)
@@ -546,58 +1326,77 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
546
1326
  return yield* transportClosed();
547
1327
  return t;
548
1328
  });
549
- yield* Effect5.addFinalizer(() => {
1329
+ const closeTransport = Effect8.suspend(() => {
550
1330
  if (closed)
551
- return Effect5.void;
1331
+ return Effect8.void;
552
1332
  closed = true;
553
1333
  const t = transport;
554
1334
  if (!t)
555
- return SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
556
- return Effect5.andThen(Effect5.logDebug(`Closing ${serviceName}`), Effect5.andThen(Effect5.promise(() => t.close()), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected())));
1335
+ return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
1336
+ return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
1337
+ try: () => t.close(),
1338
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1339
+ }), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
557
1340
  });
1341
+ yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
558
1342
  const notConnectedMsg = "Transport is not connected. Call withClient() first.";
559
- const reconnectOnce = Effect5.suspend(() => {
1343
+ const reconnectOnce = Effect8.suspend(() => {
560
1344
  const t = transport;
561
1345
  if (closed || !t)
562
1346
  return transportClosed();
563
- return singleFlight(reconnecting, Effect5.tryPromise({
1347
+ return singleFlight(reconnecting, Effect8.tryPromise({
564
1348
  try: () => t.reconnect(),
565
- catch: (error) => toModbusError(error)
566
- }).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
1349
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1350
+ }).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
567
1351
  });
568
1352
  const report = (error) => {
569
- if (!supervised || closed || !supervised.triggers(error))
570
- return Effect5.void;
571
- return Effect5.gen(function* () {
572
- const claimed = yield* SubscriptionRef3.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
573
- if (!claimed)
574
- return;
575
- yield* Effect5.logDebug(`${serviceName}: reconnecting after ${error.message}`);
576
- yield* Effect5.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
577
- });
1353
+ if (closed || !resolvedReconnect.triggers(error))
1354
+ return Effect8.void;
1355
+ if (!supervised) {
1356
+ return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
1357
+ }
1358
+ return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect8.logDebug(`${serviceName}: reconnecting after ${error.message}`));
578
1359
  };
579
1360
  const resilience = {
580
- guard: supervised ? guardCircuit(connectionState) : Effect5.void,
1361
+ guard: supervised ? guardCircuit(connectionState) : Effect8.void,
1362
+ onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
581
1363
  report
582
1364
  };
1365
+ const makeOperations = Effect8.fnUntraced(function* (unitId) {
1366
+ const t = yield* ensureOpen();
1367
+ let client = clientSet.get(unitId);
1368
+ if (!client) {
1369
+ client = yield* Effect8.try({
1370
+ try: () => t.createClient({ unitId }),
1371
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1372
+ });
1373
+ clientSet.set(unitId, client);
1374
+ }
1375
+ return createEffectModbusClient(client);
1376
+ });
1377
+ const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1378
+ const operations = yield* makeOperations(unitId);
1379
+ return withResilience(operations, {
1380
+ ...resilience,
1381
+ policy: clientOptions?.retry ?? transportRetry
1382
+ });
1383
+ });
1384
+ const batching = createBatchingRegistry({
1385
+ withClient: makeClient,
1386
+ connectionState,
1387
+ scope: serviceScope
1388
+ });
1389
+ const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1390
+ const operations = yield* makeOperations(unitId);
1391
+ return withResilience(batching.guardRawWrites(unitId, operations), {
1392
+ ...resilience,
1393
+ policy: clientOptions?.retry ?? transportRetry
1394
+ });
1395
+ });
583
1396
  return {
584
1397
  connectionState,
585
- withClient: Effect5.fnUntraced(function* (unitId, clientOptions) {
586
- const t = yield* ensureOpen();
587
- let client = clientSet.get(unitId);
588
- if (!client) {
589
- client = yield* Effect5.try({
590
- try: () => t.createClient({ unitId }),
591
- catch: (error) => toModbusError(error)
592
- });
593
- clientSet.set(unitId, client);
594
- }
595
- return withResilience(makeEffectModbusClient(client), {
596
- ...resilience,
597
- policy: clientOptions?.retry ?? transportRetry
598
- });
599
- }),
600
- setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
1398
+ withClient,
1399
+ setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
601
1400
  const t = transport;
602
1401
  if (!t || closed) {
603
1402
  return yield* new ModbusNotConnectedError({
@@ -607,7 +1406,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
607
1406
  }
608
1407
  t.setRequestTimeout(timeoutMs);
609
1408
  }),
610
- clearRequestTimeout: Effect5.fnUntraced(function* () {
1409
+ clearRequestTimeout: Effect8.fnUntraced(function* () {
611
1410
  const t = transport;
612
1411
  if (!t || closed) {
613
1412
  return yield* new ModbusNotConnectedError({
@@ -617,7 +1416,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
617
1416
  }
618
1417
  t.clearRequestTimeout();
619
1418
  }),
620
- reconnect: Effect5.fnUntraced(function* () {
1419
+ reconnect: Effect8.fnUntraced(function* () {
621
1420
  if (closed)
622
1421
  return yield* transportClosed();
623
1422
  const t = transport;
@@ -625,29 +1424,25 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
625
1424
  yield* ensureOpen();
626
1425
  return;
627
1426
  }
628
- yield* singleFlight(reconnecting, Effect5.tryPromise({
1427
+ yield* singleFlight(reconnecting, Effect8.tryPromise({
629
1428
  try: () => t.reconnect(),
630
- catch: (error) => toModbusError(error)
631
- }).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
1429
+ catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1430
+ }).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
632
1431
  if (closed)
633
1432
  return yield* transportClosed();
634
- yield* SubscriptionRef3.set(connectionState, ConnectionState.Connected());
1433
+ yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
635
1434
  }),
636
- close: Effect5.fnUntraced(function* () {
1435
+ close: Effect8.fnUntraced(function* () {
637
1436
  if (closed)
638
1437
  return;
639
- closed = true;
640
- yield* SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
641
- const t = transport;
642
- if (t) {
643
- yield* Effect5.tryPromise({
644
- try: () => t.close(),
645
- catch: (error) => toModbusError(error)
646
- });
647
- }
648
- const scope = yield* Effect5.scope;
649
- yield* Scope.close(scope, Exit.void);
1438
+ const scope = yield* Effect8.scope;
1439
+ yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
650
1440
  }),
1441
+ withBatchingClient: batching.withBatchingClient,
1442
+ batchingClient: batching.batchingClient,
1443
+ get touchedUnits() {
1444
+ return new Set(clientSet.keys());
1445
+ },
651
1446
  hasPendingRequests: () => {
652
1447
  if (closed)
653
1448
  return false;
@@ -662,10 +1457,12 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
662
1457
 
663
1458
  // src/AsciiTransportService.ts
664
1459
  class AsciiTransportService extends Context.Service()("AsciiTransportService") {
665
- static makeScoped = makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService");
1460
+ static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
1461
+ return transportConstructor.open(options);
1462
+ }, "AsciiTransportService");
666
1463
  static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
667
1464
  static makeMockTransport = (devices) => {
668
- const factory = makeMockTransport(devices);
1465
+ const factory = createMockTransport(devices);
669
1466
  return (options) => Layer.effect(AsciiTransportService, factory(options));
670
1467
  };
671
1468
  }
@@ -675,10 +1472,12 @@ import { Context as Context3, Layer as Layer3 } from "effect";
675
1472
  // src/RtuTransportService.ts
676
1473
  import { Context as Context2, Layer as Layer2 } from "effect";
677
1474
  class RtuTransportService extends Context2.Service()("RtuTransportService") {
678
- static makeScoped = makeTransportScoped("AsyncRtuTransport", (TC, options) => TC.open(options), "RtuTransportService");
1475
+ static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
1476
+ return transportConstructor.open(options);
1477
+ }, "RtuTransportService");
679
1478
  static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
680
1479
  static makeMockTransport = (devices) => {
681
- const factory = makeMockTransport(devices);
1480
+ const factory = createMockTransport(devices);
682
1481
  return (options) => Layer2.effect(RtuTransportService, factory(options));
683
1482
  };
684
1483
  }
@@ -692,17 +1491,19 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
692
1491
  return Layer3.flatMap(RtuTransportService.make(options), (context) => Layer3.succeed(SerialTransportService, Context3.get(context, RtuTransportService)));
693
1492
  }
694
1493
  static makeMockTransport = (devices) => {
695
- const factory = makeMockTransport(devices);
1494
+ const factory = createMockTransport(devices);
696
1495
  return (options) => Layer3.effect(SerialTransportService, factory(options));
697
1496
  };
698
1497
  }
699
1498
  // src/TcpTransportService.ts
700
1499
  import { Context as Context4, Layer as Layer4 } from "effect";
701
1500
  class TcpTransportService extends Context4.Service()("TcpTransportService") {
702
- static makeScoped = makeTransportScoped("AsyncTcpTransport", (TC, options) => TC.connect(options), "TcpTransportService");
1501
+ static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
1502
+ return transportConstructor.connect(options);
1503
+ }, "TcpTransportService");
703
1504
  static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
704
1505
  static makeMockTransport = (devices) => {
705
- const factory = makeMockTransport(devices);
1506
+ const factory = createMockTransport(devices);
706
1507
  return (options) => Layer4.effect(TcpTransportService, factory(options));
707
1508
  };
708
1509
  }
@@ -712,24 +1513,24 @@ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.g
712
1513
  const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
713
1514
  const server = yield* Effect9.tryPromise({
714
1515
  try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
715
- catch: (error) => toModbusError(error)
1516
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
716
1517
  });
717
1518
  yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
718
1519
  yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
719
1520
  try: () => server.shutdown(),
720
- catch: (error) => toModbusError(error)
1521
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
721
1522
  })), Effect9.catch(() => Effect9.void)));
722
1523
  }));
723
1524
  var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
724
1525
  const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
725
1526
  const server = yield* Effect9.tryPromise({
726
1527
  try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
727
- catch: (error) => toModbusError(error)
1528
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
728
1529
  });
729
1530
  yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
730
1531
  yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
731
1532
  try: () => server.shutdown(),
732
- catch: (error) => toModbusError(error)
1533
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
733
1534
  })), Effect9.catch(() => Effect9.void)));
734
1535
  }));
735
1536
  // src/TcpModbusServerService.ts
@@ -738,12 +1539,12 @@ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(fu
738
1539
  const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
739
1540
  const server = yield* Effect10.tryPromise({
740
1541
  try: () => AsyncTcpModbusServer.bind(options, handlers),
741
- catch: (error) => toModbusError(error)
1542
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
742
1543
  });
743
1544
  yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
744
1545
  yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
745
1546
  try: () => server.shutdown(),
746
- catch: (error) => toModbusError(error)
1547
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
747
1548
  })), Effect10.catch(() => Effect10.void)));
748
1549
  }));
749
1550
  // src/TcpGatewayService.ts
@@ -752,41 +1553,47 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.
752
1553
  const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
753
1554
  const gateway = yield* Effect11.tryPromise({
754
1555
  try: () => AsyncTcpGateway.bind(options, gatewayConfig),
755
- catch: (error) => toModbusError(error)
1556
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
756
1557
  });
757
1558
  yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
758
1559
  yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
759
1560
  try: () => gateway.shutdown(),
760
- catch: (error) => toModbusError(error)
1561
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
761
1562
  })), Effect11.catch(() => Effect11.void)));
762
1563
  }));
763
1564
  // src/WasmWsTransportService.ts
764
1565
  import { Context as Context5, Layer as Layer8 } from "effect";
765
1566
  class WasmWsTransportService extends Context5.Service()("WasmWsTransportService") {
766
- static makeScoped = makeTransportScoped("WasmWsTransport", (TC, options) => TC.connect(options), "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
1567
+ static makeScoped = createTransportScoped("WasmWsTransport", (transportConstructor, options) => {
1568
+ return transportConstructor.connect(options);
1569
+ }, "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
767
1570
  static make = (options) => Layer8.effect(WasmWsTransportService, WasmWsTransportService.makeScoped(options));
768
1571
  static makeMockTransport = (devices) => {
769
- const factory = makeMockTransport(devices);
1572
+ const factory = createMockTransport(devices);
770
1573
  return (options) => Layer8.effect(WasmWsTransportService, factory(options));
771
1574
  };
772
1575
  }
773
1576
  // src/WasmRtuTransportService.ts
774
1577
  import { Context as Context6, Layer as Layer9 } from "effect";
775
1578
  class WasmRtuTransportService extends Context6.Service()("WasmRtuTransportService") {
776
- static makeScoped = makeTransportScoped("WasmRtuTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
1579
+ static makeScoped = createTransportScoped("WasmRtuTransport", (transportConstructor, { port, ...rest }) => {
1580
+ return transportConstructor.open(port, rest);
1581
+ }, "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
777
1582
  static make = (options) => Layer9.effect(WasmRtuTransportService, WasmRtuTransportService.makeScoped(options));
778
1583
  static makeMockTransport = (devices) => {
779
- const factory = makeMockTransport(devices);
1584
+ const factory = createMockTransport(devices);
780
1585
  return (options) => Layer9.effect(WasmRtuTransportService, factory(options));
781
1586
  };
782
1587
  }
783
1588
  // src/WasmAsciiTransportService.ts
784
1589
  import { Context as Context7, Layer as Layer10 } from "effect";
785
1590
  class WasmAsciiTransportService extends Context7.Service()("WasmAsciiTransportService") {
786
- static makeScoped = makeTransportScoped("WasmAsciiTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
1591
+ static makeScoped = createTransportScoped("WasmAsciiTransport", (transportConstructor, { port, ...rest }) => {
1592
+ return transportConstructor.open(port, rest);
1593
+ }, "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
787
1594
  static make = (options) => Layer10.effect(WasmAsciiTransportService, WasmAsciiTransportService.makeScoped(options));
788
1595
  static makeMockTransport = (devices) => {
789
- const factory = makeMockTransport(devices);
1596
+ const factory = createMockTransport(devices);
790
1597
  return (options) => Layer10.effect(WasmAsciiTransportService, factory(options));
791
1598
  };
792
1599
  }
@@ -800,74 +1607,76 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
800
1607
  return Layer11.flatMap(WasmRtuTransportService.make(options), (context) => Layer11.succeed(WasmSerialTransportService, Context8.get(context, WasmRtuTransportService)));
801
1608
  }
802
1609
  static makeMockTransport = (devices) => {
803
- const factory = makeMockTransport(devices);
1610
+ const factory = createMockTransport(devices);
804
1611
  return (options) => Layer11.effect(WasmSerialTransportService, factory(options));
805
1612
  };
806
1613
  }
807
1614
  // src/WasmSerialPort.ts
808
- import { Effect as Effect15 } from "effect";
809
- var requestSerialPort = () => Effect15.tryPromise({
1615
+ import { Effect as Effect12 } from "effect";
1616
+ var requestSerialPort = () => Effect12.tryPromise({
810
1617
  try: async () => {
811
1618
  const mod = await import("modbus-rs/web");
812
1619
  return mod.requestSerialPort();
813
1620
  },
814
- catch: (error) => toModbusError(error)
1621
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
815
1622
  });
816
1623
  // src/WasmTcpServerService.ts
817
- import { Effect as Effect16, Layer as Layer12 } from "effect";
818
- var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect16.gen(function* () {
819
- const { WasmWsModbusServer } = yield* Effect16.promise(() => import("modbus-rs/web"));
820
- const server = yield* Effect16.tryPromise({
1624
+ import { Effect as Effect13, Layer as Layer12 } from "effect";
1625
+ var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
1626
+ const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
1627
+ const server = yield* Effect13.tryPromise({
821
1628
  try: () => WasmWsModbusServer.bind(options, handlers),
822
- catch: (error) => toModbusError(error)
1629
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
823
1630
  });
824
- yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
825
- yield* Effect16.forkScoped(Effect16.tryPromise({
1631
+ yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1632
+ yield* Effect13.forkScoped(Effect13.tryPromise({
826
1633
  try: () => server.serve(),
827
- catch: (error) => toModbusError(error)
828
- }).pipe(Effect16.catch((error) => Effect16.logError("WASM WS server loop ended", error))));
829
- yield* Effect16.addFinalizer(() => Effect16.logDebug("WASM WS server shutting down").pipe(Effect16.andThen(Effect16.tryPromise({
1634
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1635
+ }).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
1636
+ yield* Effect13.addFinalizer(() => Effect13.logDebug("WASM WS server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
830
1637
  try: () => server.shutdown(),
831
- catch: (error) => toModbusError(error)
832
- })), Effect16.catch(() => Effect16.void)));
1638
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1639
+ })), Effect13.catch(() => Effect13.void)));
833
1640
  }));
834
1641
  // src/WasmSerialModbusServerService.ts
835
- import { Effect as Effect17, Layer as Layer13 } from "effect";
836
- var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
837
- const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
838
- const server = yield* Effect17.tryPromise({
1642
+ import { Effect as Effect14, Layer as Layer13 } from "effect";
1643
+ var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1644
+ const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1645
+ const server = yield* Effect14.tryPromise({
839
1646
  try: () => WasmSerialModbusServer.bindRtu(options, handlers),
840
- catch: (error) => toModbusError(error)
1647
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
841
1648
  });
842
- yield* Effect17.logDebug("WASM serial RTU server bound");
843
- yield* Effect17.forkScoped(Effect17.tryPromise({
1649
+ yield* Effect14.logDebug("WASM serial RTU server bound");
1650
+ yield* Effect14.forkScoped(Effect14.tryPromise({
844
1651
  try: () => server.serve(),
845
- catch: (error) => toModbusError(error)
846
- }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial RTU server loop ended", error))));
847
- yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial RTU server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1652
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1653
+ }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
1654
+ yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial RTU server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
848
1655
  try: () => server.shutdown(),
849
- catch: (error) => toModbusError(error)
850
- })), Effect17.catch(() => Effect17.void)));
1656
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1657
+ })), Effect14.catch(() => Effect14.void)));
851
1658
  }));
852
- var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
853
- const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
854
- const server = yield* Effect17.tryPromise({
1659
+ var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1660
+ const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1661
+ const server = yield* Effect14.tryPromise({
855
1662
  try: () => WasmSerialModbusServer.bindAscii(options, handlers),
856
- catch: (error) => toModbusError(error)
1663
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
857
1664
  });
858
- yield* Effect17.logDebug("WASM serial ASCII server bound");
859
- yield* Effect17.forkScoped(Effect17.tryPromise({
1665
+ yield* Effect14.logDebug("WASM serial ASCII server bound");
1666
+ yield* Effect14.forkScoped(Effect14.tryPromise({
860
1667
  try: () => server.serve(),
861
- catch: (error) => toModbusError(error)
862
- }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial ASCII server loop ended", error))));
863
- yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial ASCII server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1668
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1669
+ }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
1670
+ yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial ASCII server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
864
1671
  try: () => server.shutdown(),
865
- catch: (error) => toModbusError(error)
866
- })), Effect17.catch(() => Effect17.void)));
1672
+ catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1673
+ })), Effect14.catch(() => Effect14.void)));
867
1674
  }));
868
1675
  export {
869
1676
  AsciiTransportService,
870
1677
  ConnectionState,
1678
+ MODBUS_MAX_READ_REGISTERS,
1679
+ MODBUS_MAX_WRITE_REGISTERS,
871
1680
  ModbusCircuitOpenError,
872
1681
  ModbusConnectionClosedError,
873
1682
  ModbusExceptionError,
@@ -884,7 +1693,16 @@ export {
884
1693
  WasmRtuTransportService,
885
1694
  WasmSerialTransportService,
886
1695
  WasmWsTransportService,
887
- makeRetryPolicy,
1696
+ createBatchingRegistry,
1697
+ createReadDebouncer,
1698
+ createRegisterCache,
1699
+ createRetryPolicy,
1700
+ createWriteDebouncer,
1701
+ encodeRegisterValue,
1702
+ makeBatchingClient,
1703
+ mergeSpanAttributes,
1704
+ planReads,
1705
+ planWrites,
888
1706
  requestSerialPort,
889
1707
  retryModbus,
890
1708
  retryableExceptionCodes,