@flux-control/effect-modbus-rs 0.8.0 → 0.9.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 (33) hide show
  1. package/README.md +499 -832
  2. package/dist/index.d.ts +56 -113
  3. package/dist/index.js +234 -116
  4. package/dist/src/AsciiTransportService.d.ts +15 -14
  5. package/dist/src/RtuTransportService.d.ts +15 -14
  6. package/dist/src/SerialModbusServerService.d.ts +3 -2
  7. package/dist/src/SerialTransportService.d.ts +8 -2
  8. package/dist/src/TcpGatewayService.d.ts +2 -1
  9. package/dist/src/TcpModbusServerService.d.ts +2 -1
  10. package/dist/src/TcpTransportService.d.ts +6 -5
  11. package/dist/src/WasmAsciiTransportService.d.ts +13 -7
  12. package/dist/src/WasmRtuTransportService.d.ts +13 -7
  13. package/dist/src/WasmSerialModbusServerService.d.ts +4 -2
  14. package/dist/src/WasmSerialPort.d.ts +6 -4
  15. package/dist/src/WasmSerialTransportService.d.ts +9 -2
  16. package/dist/src/WasmTcpServerService.d.ts +2 -1
  17. package/dist/src/WasmWsTransportService.d.ts +9 -5
  18. package/dist/src/batching-client.d.ts +61 -28
  19. package/dist/src/connection.d.ts +6 -2
  20. package/dist/src/errors.d.ts +11 -12
  21. package/dist/src/mocks.d.ts +3 -2
  22. package/dist/src/modbus-client.d.ts +14 -10
  23. package/dist/src/read-debouncer.d.ts +8 -22
  24. package/dist/src/register-cache.d.ts +1 -7
  25. package/dist/src/register-plan.d.ts +2 -12
  26. package/dist/src/retry.d.ts +1 -0
  27. package/dist/src/serial-port-path.d.ts +21 -0
  28. package/dist/src/shared-transport.d.ts +25 -5
  29. package/dist/src/span-attributes.d.ts +1 -1
  30. package/dist/src/write-debouncer.d.ts +15 -49
  31. package/dist/test/native-transport.test.d.ts +1 -0
  32. package/dist/test/serial-port-path.test.d.ts +1 -0
  33. package/package.json +20 -11
package/dist/index.js CHANGED
@@ -930,7 +930,7 @@ var createBatchingRegistry = (deps) => {
930
930
  };
931
931
  };
932
932
  // src/AsciiTransportService.ts
933
- import { Context, Layer } from "effect";
933
+ import { Context, Effect as Effect10, Layer } from "effect";
934
934
 
935
935
  // src/mocks.ts
936
936
  import { Effect as Effect7, Exit as Exit3, Schema, Scope as Scope2, SubscriptionRef as SubscriptionRef3 } from "effect";
@@ -940,30 +940,36 @@ import {
940
940
 
941
941
  // src/modbus-client.ts
942
942
  import { Effect as Effect6 } from "effect";
943
+ var identityWrapper = (call) => call;
943
944
  var wrap = (try_) => Effect6.tryPromise({
944
945
  try: try_,
945
946
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
946
947
  });
947
- var createEffectModbusClient = (client) => ({
948
- readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
949
- readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
950
- writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
951
- writeMultipleRegisters: (opts) => wrap(() => client.writeMultipleRegisters(opts)),
952
- readWriteMultipleRegisters: (opts) => wrap(() => client.readWriteMultipleRegisters(opts)),
953
- readCoils: (opts) => wrap(() => client.readCoils(opts)),
954
- writeSingleCoil: (opts) => wrap(() => client.writeSingleCoil(opts)),
955
- writeMultipleCoils: (opts) => wrap(() => client.writeMultipleCoils(opts)),
956
- readDiscreteInputs: (opts) => wrap(() => client.readDiscreteInputs(opts)),
957
- readFifoQueue: (opts) => wrap(() => client.readFifoQueue(opts)),
958
- readFileRecord: (opts) => wrap(() => client.readFileRecord(opts)),
959
- writeFileRecord: (opts) => wrap(() => client.writeFileRecord(opts)),
960
- readExceptionStatus: () => wrap(() => client.readExceptionStatus()),
961
- diagnostics: (opts) => wrap(() => client.diagnostics(opts)),
962
- readDeviceIdentification: (opts) => wrap(() => client.readDeviceIdentification(opts))
963
- });
948
+ var createEffectModbusClient = (client, around = identityWrapper) => {
949
+ const resolve = Effect6.isEffect(client) ? client : Effect6.succeed(client);
950
+ const call = (try_) => around(Effect6.flatMap(resolve, (current) => wrap(() => try_(current))));
951
+ return {
952
+ readHoldingRegisters: (opts) => call((current) => current.readHoldingRegisters(opts)),
953
+ readInputRegisters: (opts) => call((current) => current.readInputRegisters(opts)),
954
+ writeSingleRegister: (opts) => call((current) => current.writeSingleRegister(opts)),
955
+ writeMultipleRegisters: (opts) => call((current) => current.writeMultipleRegisters(opts)),
956
+ readWriteMultipleRegisters: (opts) => call((current) => current.readWriteMultipleRegisters(opts)),
957
+ readCoils: (opts) => call((current) => current.readCoils(opts)),
958
+ writeSingleCoil: (opts) => call((current) => current.writeSingleCoil(opts)),
959
+ writeMultipleCoils: (opts) => call((current) => current.writeMultipleCoils(opts)),
960
+ readDiscreteInputs: (opts) => call((current) => current.readDiscreteInputs(opts)),
961
+ readFifoQueue: (opts) => call((current) => current.readFifoQueue(opts)),
962
+ readFileRecord: (opts) => call((current) => current.readFileRecord(opts)),
963
+ writeFileRecord: (opts) => call((current) => current.writeFileRecord(opts)),
964
+ readExceptionStatus: () => call((current) => current.readExceptionStatus()),
965
+ diagnostics: (opts) => call((current) => current.diagnostics(opts)),
966
+ readDeviceIdentification: (opts) => call((current) => current.readDeviceIdentification(opts))
967
+ };
968
+ };
964
969
  var withResilience = (operations, resilience) => {
965
970
  const run = (operation) => {
966
- const attempt = Effect6.andThen(resilience.guard, Effect6.suspend(operation)).pipe(Effect6.tap(() => resilience.onSuccess ?? Effect6.void), Effect6.tapError((error) => resilience.report(error)));
971
+ const guarded = Effect6.andThen(resilience.guard, Effect6.suspend(operation)).pipe(Effect6.tap(() => resilience.onSuccess ?? Effect6.void), Effect6.tapError((error) => resilience.report(error)));
972
+ const attempt = resilience.aroundAttempt?.(guarded) ?? guarded;
967
973
  return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
968
974
  };
969
975
  return {
@@ -1273,17 +1279,68 @@ var createMockTransport = (devices) => {
1273
1279
  });
1274
1280
  };
1275
1281
 
1282
+ // src/serial-port-path.ts
1283
+ import { Effect as Effect8 } from "effect";
1284
+ var MAX_SERIAL_PORT_PATH = 128;
1285
+ var resolveSerialPortPath = async (portPath) => {
1286
+ if (portPath.length <= MAX_SERIAL_PORT_PATH)
1287
+ return portPath;
1288
+ const { realpath } = await import("node:fs/promises");
1289
+ const target = await realpath(portPath).catch(() => portPath);
1290
+ if (target.length <= MAX_SERIAL_PORT_PATH)
1291
+ return target;
1292
+ throw new Error(`[MODBUS_INVALID_ARGUMENT] Serial port path is ${portPath.length} characters long. modbus-rs accepts at most ${MAX_SERIAL_PORT_PATH} characters. Use a shorter path or a shorter link to the device: ${portPath}`);
1293
+ };
1294
+ var serialPortPathScoped = Effect8.fnUntraced(function* (portPath) {
1295
+ let directory;
1296
+ let alias;
1297
+ yield* Effect8.addFinalizer(() => Effect8.promise(async () => {
1298
+ if (directory === undefined)
1299
+ return;
1300
+ const { rm } = await import("node:fs/promises");
1301
+ await rm(directory, { recursive: true, force: true });
1302
+ }));
1303
+ return async () => {
1304
+ if (portPath.length <= MAX_SERIAL_PORT_PATH)
1305
+ return portPath;
1306
+ if (alias !== undefined)
1307
+ return alias;
1308
+ await resolveSerialPortPath(portPath);
1309
+ const { mkdtemp, symlink } = await import("node:fs/promises");
1310
+ const { tmpdir } = await import("node:os");
1311
+ const { join, resolve } = await import("node:path");
1312
+ directory = await mkdtemp(join(tmpdir(), "modbus-port-"));
1313
+ const path = join(directory, "port");
1314
+ if (path.length > MAX_SERIAL_PORT_PATH)
1315
+ throw new Error("[MODBUS_INVALID_ARGUMENT] The temporary serial alias path exceeds the native limit");
1316
+ await symlink(resolve(portPath), path);
1317
+ alias = path;
1318
+ return alias;
1319
+ };
1320
+ });
1321
+
1276
1322
  // src/shared-transport.ts
1277
- import { Deferred as Deferred4, Duration as Duration6, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
1323
+ import {
1324
+ Deferred as Deferred4,
1325
+ Duration as Duration6,
1326
+ Effect as Effect9,
1327
+ Exit as Exit4,
1328
+ Fiber as Fiber4,
1329
+ Option,
1330
+ Ref,
1331
+ Scope as Scope3,
1332
+ Semaphore as Semaphore4,
1333
+ SubscriptionRef as SubscriptionRef4
1334
+ } from "effect";
1278
1335
  var makeInFlight = () => Ref.make(Option.none());
1279
- var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
1336
+ var singleFlight = (inFlight, work) => Effect9.uninterruptibleMask((restore) => Effect9.gen(function* () {
1280
1337
  const fresh = yield* Deferred4.make();
1281
1338
  const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
1282
1339
  onSome: (existing) => [[existing, false], current],
1283
1340
  onNone: () => [[fresh, true], Option.some(fresh)]
1284
1341
  }));
1285
1342
  if (isLeader) {
1286
- yield* Effect8.forkDetach(Effect8.interruptible(work).pipe(Effect8.onExit((exit) => Effect8.andThen(Ref.set(inFlight, Option.none()), Deferred4.done(deferred, exit)))));
1343
+ yield* Effect9.forkDetach(Effect9.interruptible(work).pipe(Effect9.onExit((exit) => Effect9.andThen(Ref.set(inFlight, Option.none()), Deferred4.done(deferred, exit)))));
1287
1344
  }
1288
1345
  return yield* restore(Deferred4.await(deferred));
1289
1346
  }));
@@ -1314,44 +1371,67 @@ var loadTransportConstructor = async (transportKey, moduleSpecifier) => {
1314
1371
  }
1315
1372
  };
1316
1373
  function createTransportScoped(transportKey, openMethod, serviceName, config) {
1317
- return Effect8.fnUntraced(function* (options) {
1374
+ return Effect9.fnUntraced(function* (options) {
1318
1375
  const { retry: transportRetry, reconnect: reconnectOptions } = options;
1319
1376
  const connectTimeout = options.connectTimeout === undefined ? undefined : Duration6.fromInputUnsafe(options.connectTimeout);
1320
1377
  const openOptions = { ...options };
1321
1378
  Reflect.deleteProperty(openOptions, "retry");
1322
1379
  Reflect.deleteProperty(openOptions, "reconnect");
1323
1380
  Reflect.deleteProperty(openOptions, "connectTimeout");
1324
- const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
1381
+ const nativeLock = config?.serializeRequests ? yield* Semaphore4.make(1) : undefined;
1382
+ let nativeLimit = config?.responseTimeoutMs?.(options) ?? 1000;
1383
+ let requestLimitOverride;
1384
+ const aroundNative = nativeLock === undefined ? undefined : (call) => Effect9.uninterruptibleMask((restore) => Effect9.gen(function* () {
1385
+ yield* restore(nativeLock.take(1));
1386
+ const work = call.pipe(Effect9.ensuring(nativeLock.release(1)));
1387
+ if (nativeLimit !== undefined)
1388
+ return yield* work;
1389
+ const worker = yield* Effect9.forkDetach(Effect9.exit(work));
1390
+ const exit = yield* restore(Fiber4.join(worker)).pipe(Effect9.onInterrupt(() => invalidateNative));
1391
+ return yield* exit;
1392
+ }));
1393
+ const TC = yield* Effect9.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
1325
1394
  let transport = null;
1326
1395
  const opening = yield* makeInFlight();
1327
1396
  const reconnecting = yield* makeInFlight();
1328
1397
  const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
1329
1398
  const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
1330
1399
  const supervised = reconnectOptions ? resolvedReconnect : null;
1331
- const serviceScope = yield* Effect8.scope;
1400
+ const serviceScope = yield* Effect9.scope;
1332
1401
  const clientSet = new Map;
1402
+ const touchedUnits = new Set;
1333
1403
  let closed = false;
1404
+ const invalidateNative = Effect9.suspend(() => {
1405
+ const current = transport;
1406
+ transport = null;
1407
+ clientSet.clear();
1408
+ return Effect9.andThen(SubscriptionRef4.set(connectionState, ConnectionState.Disconnected()), current === null ? Effect9.void : Effect9.ignore(Effect9.tryPromise(() => current.close())));
1409
+ });
1334
1410
  const transportClosed = () => new ModbusNotConnectedError({
1335
1411
  cause: new Error("Transport has been closed"),
1336
1412
  message: "Transport has been closed"
1337
1413
  });
1338
- const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
1414
+ const closeOrphan = (t) => Effect9.forkDetach(Effect9.ignore(Effect9.tryPromise(() => t.close())));
1339
1415
  const limitConnect = (wait) => {
1340
1416
  if (connectTimeout === undefined)
1341
1417
  return wait;
1342
1418
  const message = `${serviceName}: connect did not complete within ${Duration6.format(connectTimeout)}`;
1343
- return Effect8.timeoutOrElse(wait, {
1419
+ return Effect9.timeoutOrElse(wait, {
1344
1420
  duration: connectTimeout,
1345
- orElse: () => Effect8.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
1421
+ orElse: () => Effect9.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
1346
1422
  });
1347
1423
  };
1348
- const openTransport = Effect8.tryPromise({
1424
+ const openTransport = Effect9.tryPromise({
1349
1425
  try: () => openMethod(TC, openOptions),
1350
1426
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1351
- }).pipe(Effect8.tap((t) => closed ? closeOrphan(t) : Effect8.andThen(Effect8.sync(() => {
1427
+ }).pipe(Effect9.tap((t) => closed ? closeOrphan(t) : Effect9.andThen(Effect9.sync(() => {
1428
+ if (requestLimitOverride === null)
1429
+ t.clearRequestTimeout();
1430
+ else if (requestLimitOverride !== undefined)
1431
+ t.setRequestTimeout(requestLimitOverride);
1352
1432
  transport = t;
1353
1433
  }), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
1354
- const ensureOpen = Effect8.fnUntraced(function* () {
1434
+ const ensureOpen = Effect9.fnUntraced(function* () {
1355
1435
  if (closed)
1356
1436
  return yield* transportClosed();
1357
1437
  if (transport)
@@ -1361,56 +1441,65 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1361
1441
  return yield* transportClosed();
1362
1442
  return t;
1363
1443
  });
1364
- const closeTransport = Effect8.suspend(() => {
1444
+ const closeTransport = Effect9.suspend(() => {
1365
1445
  if (closed)
1366
- return Effect8.void;
1446
+ return Effect9.void;
1367
1447
  closed = true;
1368
1448
  const t = transport;
1369
1449
  if (!t)
1370
1450
  return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
1371
- return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
1451
+ return Effect9.andThen(Effect9.logDebug(`Closing ${serviceName}`), Effect9.andThen(Effect9.tryPromise({
1372
1452
  try: () => t.close(),
1373
1453
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1374
1454
  }), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
1375
1455
  });
1376
- yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
1456
+ yield* Effect9.addFinalizer(() => Effect9.orDie(closeTransport));
1377
1457
  const notConnectedMsg = "Transport is not connected. Call withClient() first.";
1378
- const reconnectTransport = (t) => limitConnect(singleFlight(reconnecting, Effect8.tryPromise({
1458
+ const withNativeLock = (effect) => nativeLock === undefined ? effect : nativeLock.withPermits(1)(effect);
1459
+ const reconnectTransport = (t) => limitConnect(singleFlight(reconnecting, withNativeLock(Effect9.tryPromise({
1379
1460
  try: () => t.reconnect(),
1380
1461
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1381
- }).pipe(Effect8.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
1382
- const reconnectOnce = Effect8.suspend(() => {
1462
+ })).pipe(Effect9.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
1463
+ const reconnectOnce = Effect9.suspend(() => {
1383
1464
  const t = transport;
1384
1465
  if (closed || !t)
1385
1466
  return transportClosed();
1386
1467
  return reconnectTransport(t);
1387
1468
  });
1388
1469
  const report = (error) => {
1389
- if (closed || !resolvedReconnect.triggers(error))
1390
- return Effect8.void;
1470
+ if (closed)
1471
+ return Effect9.void;
1472
+ if (!resolvedReconnect.triggers(error))
1473
+ return Effect9.void;
1391
1474
  if (!supervised) {
1392
1475
  return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
1393
1476
  }
1394
- return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect8.logDebug(`${serviceName}: reconnecting after ${error.message}`));
1477
+ return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect9.logDebug(`${serviceName}: reconnecting after ${error.message}`));
1395
1478
  };
1396
1479
  const resilience = {
1397
- guard: supervised ? guardCircuit(connectionState) : Effect8.void,
1480
+ guard: supervised ? Effect9.suspend(() => transport === null ? Effect9.void : guardCircuit(connectionState)) : Effect9.void,
1481
+ aroundAttempt: aroundNative,
1398
1482
  onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
1399
1483
  report
1400
1484
  };
1401
- const makeOperations = Effect8.fnUntraced(function* (unitId) {
1485
+ const resolveClient = Effect9.fnUntraced(function* (unitId) {
1402
1486
  const t = yield* ensureOpen();
1403
1487
  let client = clientSet.get(unitId);
1404
1488
  if (!client) {
1405
- client = yield* Effect8.try({
1489
+ client = yield* Effect9.try({
1406
1490
  try: () => t.createClient({ unitId }),
1407
1491
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1408
1492
  });
1409
1493
  clientSet.set(unitId, client);
1494
+ touchedUnits.add(unitId);
1410
1495
  }
1411
- return createEffectModbusClient(client);
1496
+ return client;
1497
+ });
1498
+ const makeOperations = Effect9.fnUntraced(function* (unitId) {
1499
+ yield* withNativeLock(resolveClient(unitId));
1500
+ return createEffectModbusClient(resolveClient(unitId));
1412
1501
  });
1413
- const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1502
+ const makeClient = Effect9.fnUntraced(function* (unitId, clientOptions) {
1414
1503
  const operations = yield* makeOperations(unitId);
1415
1504
  return withResilience(operations, {
1416
1505
  ...resilience,
@@ -1422,7 +1511,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1422
1511
  connectionState,
1423
1512
  scope: serviceScope
1424
1513
  });
1425
- const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1514
+ const withClient = Effect9.fnUntraced(function* (unitId, clientOptions) {
1426
1515
  const operations = yield* makeOperations(unitId);
1427
1516
  return withResilience(batching.guardRawWrites(unitId, operations), {
1428
1517
  ...resilience,
@@ -1432,7 +1521,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1432
1521
  return {
1433
1522
  connectionState,
1434
1523
  withClient,
1435
- setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
1524
+ setRequestTimeout: Effect9.fnUntraced(function* (timeoutMs) {
1436
1525
  const t = transport;
1437
1526
  if (!t || closed) {
1438
1527
  return yield* new ModbusNotConnectedError({
@@ -1441,8 +1530,10 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1441
1530
  });
1442
1531
  }
1443
1532
  t.setRequestTimeout(timeoutMs);
1533
+ nativeLimit = timeoutMs;
1534
+ requestLimitOverride = timeoutMs;
1444
1535
  }),
1445
- clearRequestTimeout: Effect8.fnUntraced(function* () {
1536
+ clearRequestTimeout: Effect9.fnUntraced(function* () {
1446
1537
  const t = transport;
1447
1538
  if (!t || closed) {
1448
1539
  return yield* new ModbusNotConnectedError({
@@ -1451,8 +1542,10 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1451
1542
  });
1452
1543
  }
1453
1544
  t.clearRequestTimeout();
1545
+ nativeLimit = undefined;
1546
+ requestLimitOverride = null;
1454
1547
  }),
1455
- reconnect: Effect8.fnUntraced(function* () {
1548
+ reconnect: Effect9.fnUntraced(function* () {
1456
1549
  if (closed)
1457
1550
  return yield* transportClosed();
1458
1551
  const t = transport;
@@ -1465,15 +1558,15 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1465
1558
  return yield* transportClosed();
1466
1559
  yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
1467
1560
  }),
1468
- close: Effect8.fnUntraced(function* (scope) {
1561
+ close: Effect9.fnUntraced(function* (scope) {
1469
1562
  if (closed)
1470
1563
  return;
1471
- yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
1564
+ yield* Scope3.close(scope, Exit4.void).pipe(Effect9.onExit(() => closeTransport));
1472
1565
  }),
1473
1566
  withBatchingClient: batching.withBatchingClient,
1474
1567
  batchingClient: batching.batchingClient,
1475
1568
  get touchedUnits() {
1476
- return new Set(clientSet.keys());
1569
+ return new Set(touchedUnits);
1477
1570
  },
1478
1571
  hasPendingRequests: () => {
1479
1572
  if (closed)
@@ -1489,9 +1582,19 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1489
1582
 
1490
1583
  // src/AsciiTransportService.ts
1491
1584
  class AsciiTransportService extends Context.Service()("AsciiTransportService") {
1492
- static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
1493
- return transportConstructor.open(options);
1494
- }, "AsciiTransportService");
1585
+ static makeScoped = Effect10.fnUntraced(function* (options) {
1586
+ const portPath = yield* serialPortPathScoped(options.portPath);
1587
+ return yield* createTransportScoped("AsyncAsciiTransport", async (transportConstructor, options2) => {
1588
+ return transportConstructor.open({
1589
+ ...options2,
1590
+ portPath: await portPath(),
1591
+ responseTimeoutMs: options2.responseTimeoutMs ?? options2.requestTimeoutMs
1592
+ });
1593
+ }, "AsciiTransportService", {
1594
+ serializeRequests: true,
1595
+ responseTimeoutMs: (options2) => options2.responseTimeoutMs ?? options2.requestTimeoutMs
1596
+ })(options);
1597
+ });
1495
1598
  static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
1496
1599
  static makeMockTransport = (devices) => {
1497
1600
  const factory = createMockTransport(devices);
@@ -1502,11 +1605,21 @@ class AsciiTransportService extends Context.Service()("AsciiTransportService") {
1502
1605
  import { Context as Context3, Layer as Layer3 } from "effect";
1503
1606
 
1504
1607
  // src/RtuTransportService.ts
1505
- import { Context as Context2, Layer as Layer2 } from "effect";
1608
+ import { Context as Context2, Effect as Effect11, Layer as Layer2 } from "effect";
1506
1609
  class RtuTransportService extends Context2.Service()("RtuTransportService") {
1507
- static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
1508
- return transportConstructor.open(options);
1509
- }, "RtuTransportService");
1610
+ static makeScoped = Effect11.fnUntraced(function* (options) {
1611
+ const portPath = yield* serialPortPathScoped(options.portPath);
1612
+ return yield* createTransportScoped("AsyncRtuTransport", async (transportConstructor, options2) => {
1613
+ return transportConstructor.open({
1614
+ ...options2,
1615
+ portPath: await portPath(),
1616
+ responseTimeoutMs: options2.responseTimeoutMs ?? options2.requestTimeoutMs
1617
+ });
1618
+ }, "RtuTransportService", {
1619
+ serializeRequests: true,
1620
+ responseTimeoutMs: (options2) => options2.responseTimeoutMs ?? options2.requestTimeoutMs
1621
+ })(options);
1622
+ });
1510
1623
  static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
1511
1624
  static makeMockTransport = (devices) => {
1512
1625
  const factory = createMockTransport(devices);
@@ -1531,7 +1644,10 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
1531
1644
  import { Context as Context4, Layer as Layer4 } from "effect";
1532
1645
  class TcpTransportService extends Context4.Service()("TcpTransportService") {
1533
1646
  static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
1534
- return transportConstructor.connect(options);
1647
+ return transportConstructor.connect({
1648
+ ...options,
1649
+ responseTimeoutMs: options.responseTimeoutMs ?? options.requestTimeoutMs
1650
+ });
1535
1651
  }, "TcpTransportService");
1536
1652
  static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
1537
1653
  static makeMockTransport = (devices) => {
@@ -1540,58 +1656,58 @@ class TcpTransportService extends Context4.Service()("TcpTransportService") {
1540
1656
  };
1541
1657
  }
1542
1658
  // src/SerialModbusServerService.ts
1543
- import { Effect as Effect9, Layer as Layer5 } from "effect";
1544
- var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
1545
- const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
1546
- const server = yield* Effect9.tryPromise({
1659
+ import { Effect as Effect12, Layer as Layer5 } from "effect";
1660
+ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect12.gen(function* () {
1661
+ const { AsyncSerialModbusServer } = yield* Effect12.promise(() => import("modbus-rs"));
1662
+ const server = yield* Effect12.tryPromise({
1547
1663
  try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
1548
1664
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1549
1665
  });
1550
- yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
1551
- yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
1666
+ yield* Effect12.logDebug(`Serial RTU server bound to ${options.portPath}`);
1667
+ yield* Effect12.addFinalizer(() => Effect12.logDebug("Serial RTU server shutting down").pipe(Effect12.andThen(Effect12.tryPromise({
1552
1668
  try: () => server.shutdown(),
1553
1669
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1554
- })), Effect9.catch(() => Effect9.void)));
1670
+ })), Effect12.catch(() => Effect12.void)));
1555
1671
  }));
1556
- var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
1557
- const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
1558
- const server = yield* Effect9.tryPromise({
1672
+ var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect12.gen(function* () {
1673
+ const { AsyncSerialModbusServer } = yield* Effect12.promise(() => import("modbus-rs"));
1674
+ const server = yield* Effect12.tryPromise({
1559
1675
  try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
1560
1676
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1561
1677
  });
1562
- yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
1563
- yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
1678
+ yield* Effect12.logDebug(`Serial ASCII server bound to ${options.portPath}`);
1679
+ yield* Effect12.addFinalizer(() => Effect12.logDebug("Serial ASCII server shutting down").pipe(Effect12.andThen(Effect12.tryPromise({
1564
1680
  try: () => server.shutdown(),
1565
1681
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1566
- })), Effect9.catch(() => Effect9.void)));
1682
+ })), Effect12.catch(() => Effect12.void)));
1567
1683
  }));
1568
1684
  // src/TcpModbusServerService.ts
1569
- import { Effect as Effect10, Layer as Layer6 } from "effect";
1570
- var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(function* () {
1571
- const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
1572
- const server = yield* Effect10.tryPromise({
1685
+ import { Effect as Effect13, Layer as Layer6 } from "effect";
1686
+ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect13.gen(function* () {
1687
+ const { AsyncTcpModbusServer } = yield* Effect13.promise(() => import("modbus-rs"));
1688
+ const server = yield* Effect13.tryPromise({
1573
1689
  try: () => AsyncTcpModbusServer.bind(options, handlers),
1574
1690
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1575
1691
  });
1576
- yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
1577
- yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
1692
+ yield* Effect13.logDebug(`TCP server bound to ${options.host}:${options.port}`);
1693
+ yield* Effect13.addFinalizer(() => Effect13.logDebug("TCP server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
1578
1694
  try: () => server.shutdown(),
1579
1695
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1580
- })), Effect10.catch(() => Effect10.void)));
1696
+ })), Effect13.catch(() => Effect13.void)));
1581
1697
  }));
1582
1698
  // src/TcpGatewayService.ts
1583
- import { Effect as Effect11, Layer as Layer7 } from "effect";
1584
- var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.gen(function* () {
1585
- const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
1586
- const gateway = yield* Effect11.tryPromise({
1699
+ import { Effect as Effect14, Layer as Layer7 } from "effect";
1700
+ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect14.gen(function* () {
1701
+ const { AsyncTcpGateway } = yield* Effect14.promise(() => import("modbus-rs"));
1702
+ const gateway = yield* Effect14.tryPromise({
1587
1703
  try: () => AsyncTcpGateway.bind(options, gatewayConfig),
1588
1704
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1589
1705
  });
1590
- yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
1591
- yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
1706
+ yield* Effect14.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
1707
+ yield* Effect14.addFinalizer(() => Effect14.logDebug("TCP gateway shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
1592
1708
  try: () => gateway.shutdown(),
1593
1709
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1594
- })), Effect11.catch(() => Effect11.void)));
1710
+ })), Effect14.catch(() => Effect14.void)));
1595
1711
  }));
1596
1712
  // src/WasmWsTransportService.ts
1597
1713
  import { Context as Context5, Layer as Layer8 } from "effect";
@@ -1644,8 +1760,8 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
1644
1760
  };
1645
1761
  }
1646
1762
  // src/WasmSerialPort.ts
1647
- import { Effect as Effect12 } from "effect";
1648
- var requestSerialPort = () => Effect12.tryPromise({
1763
+ import { Effect as Effect15 } from "effect";
1764
+ var requestSerialPort = () => Effect15.tryPromise({
1649
1765
  try: async () => {
1650
1766
  const mod = await import("modbus-rs/web");
1651
1767
  return mod.requestSerialPort();
@@ -1653,60 +1769,61 @@ var requestSerialPort = () => Effect12.tryPromise({
1653
1769
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1654
1770
  });
1655
1771
  // src/WasmTcpServerService.ts
1656
- import { Effect as Effect13, Layer as Layer12 } from "effect";
1657
- var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
1658
- const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
1659
- const server = yield* Effect13.tryPromise({
1772
+ import { Effect as Effect16, Layer as Layer12 } from "effect";
1773
+ var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect16.gen(function* () {
1774
+ const { WasmWsModbusServer } = yield* Effect16.promise(() => import("modbus-rs/web"));
1775
+ const server = yield* Effect16.tryPromise({
1660
1776
  try: () => WasmWsModbusServer.bind(options, handlers),
1661
1777
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1662
1778
  });
1663
- yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1664
- yield* Effect13.forkScoped(Effect13.tryPromise({
1779
+ yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1780
+ yield* Effect16.forkScoped(Effect16.tryPromise({
1665
1781
  try: () => server.serve(),
1666
1782
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1667
- }).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
1668
- yield* Effect13.addFinalizer(() => Effect13.logDebug("WASM WS server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
1783
+ }).pipe(Effect16.catch((error) => Effect16.logError("WASM WS server loop ended", error))));
1784
+ yield* Effect16.addFinalizer(() => Effect16.logDebug("WASM WS server shutting down").pipe(Effect16.andThen(Effect16.tryPromise({
1669
1785
  try: () => server.shutdown(),
1670
1786
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1671
- })), Effect13.catch(() => Effect13.void)));
1787
+ })), Effect16.catch(() => Effect16.void)));
1672
1788
  }));
1673
1789
  // src/WasmSerialModbusServerService.ts
1674
- import { Effect as Effect14, Layer as Layer13 } from "effect";
1675
- var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1676
- const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1677
- const server = yield* Effect14.tryPromise({
1790
+ import { Effect as Effect17, Layer as Layer13 } from "effect";
1791
+ var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
1792
+ const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
1793
+ const server = yield* Effect17.tryPromise({
1678
1794
  try: () => WasmSerialModbusServer.bindRtu(options, handlers),
1679
1795
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1680
1796
  });
1681
- yield* Effect14.logDebug("WASM serial RTU server bound");
1682
- yield* Effect14.forkScoped(Effect14.tryPromise({
1797
+ yield* Effect17.logDebug("WASM serial RTU server bound");
1798
+ yield* Effect17.forkScoped(Effect17.tryPromise({
1683
1799
  try: () => server.serve(),
1684
1800
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1685
- }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
1686
- yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial RTU server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
1801
+ }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial RTU server loop ended", error))));
1802
+ yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial RTU server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1687
1803
  try: () => server.shutdown(),
1688
1804
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1689
- })), Effect14.catch(() => Effect14.void)));
1805
+ })), Effect17.catch(() => Effect17.void)));
1690
1806
  }));
1691
- var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1692
- const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1693
- const server = yield* Effect14.tryPromise({
1807
+ var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect17.gen(function* () {
1808
+ const { WasmSerialModbusServer } = yield* Effect17.promise(() => import("modbus-rs/web"));
1809
+ const server = yield* Effect17.tryPromise({
1694
1810
  try: () => WasmSerialModbusServer.bindAscii(options, handlers),
1695
1811
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1696
1812
  });
1697
- yield* Effect14.logDebug("WASM serial ASCII server bound");
1698
- yield* Effect14.forkScoped(Effect14.tryPromise({
1813
+ yield* Effect17.logDebug("WASM serial ASCII server bound");
1814
+ yield* Effect17.forkScoped(Effect17.tryPromise({
1699
1815
  try: () => server.serve(),
1700
1816
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1701
- }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
1702
- yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial ASCII server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
1817
+ }).pipe(Effect17.catch((error) => Effect17.logError("WASM serial ASCII server loop ended", error))));
1818
+ yield* Effect17.addFinalizer(() => Effect17.logDebug("WASM serial ASCII server shutting down").pipe(Effect17.andThen(Effect17.tryPromise({
1703
1819
  try: () => server.shutdown(),
1704
1820
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1705
- })), Effect14.catch(() => Effect14.void)));
1821
+ })), Effect17.catch(() => Effect17.void)));
1706
1822
  }));
1707
1823
  export {
1708
1824
  AsciiTransportService,
1709
1825
  ConnectionState,
1826
+ MAX_SERIAL_PORT_PATH,
1710
1827
  MODBUS_MAX_READ_REGISTERS,
1711
1828
  MODBUS_MAX_WRITE_REGISTERS,
1712
1829
  ModbusCircuitOpenError,
@@ -1737,6 +1854,7 @@ export {
1737
1854
  planReads,
1738
1855
  planWrites,
1739
1856
  requestSerialPort,
1857
+ resolveSerialPortPath,
1740
1858
  retryModbus,
1741
1859
  retryableExceptionCodes,
1742
1860
  serialAsciiServerLayer,