@flux-control/effect-modbus-rs 0.7.0 → 0.8.1

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 +498 -831
  2. package/dist/index.d.ts +56 -113
  3. package/dist/index.js +235 -119
  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 +7 -6
  11. package/dist/src/WasmAsciiTransportService.d.ts +14 -8
  12. package/dist/src/WasmRtuTransportService.d.ts +14 -8
  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 +10 -6
  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 +4 -3
  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 +33 -8
  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 +5 -5
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 {
@@ -1263,10 +1269,9 @@ var createMockTransport = (devices) => {
1263
1269
  setRequestTimeout: (_timeoutMs) => ensureOpen,
1264
1270
  clearRequestTimeout: () => ensureOpen,
1265
1271
  reconnect: () => Effect7.andThen(reconnectOnce, SubscriptionRef3.set(connectionState, ConnectionState.Connected())),
1266
- close: () => Effect7.gen(function* () {
1272
+ close: (scope) => Effect7.gen(function* () {
1267
1273
  if (closed)
1268
1274
  return;
1269
- const scope = yield* Effect7.scope;
1270
1275
  yield* Scope2.close(scope, Exit3.void).pipe(Effect7.onExit(() => closeTransport));
1271
1276
  }),
1272
1277
  hasPendingRequests: () => false
@@ -1274,17 +1279,68 @@ var createMockTransport = (devices) => {
1274
1279
  });
1275
1280
  };
1276
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
+
1277
1322
  // src/shared-transport.ts
1278
- 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";
1279
1335
  var makeInFlight = () => Ref.make(Option.none());
1280
- var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
1336
+ var singleFlight = (inFlight, work) => Effect9.uninterruptibleMask((restore) => Effect9.gen(function* () {
1281
1337
  const fresh = yield* Deferred4.make();
1282
1338
  const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
1283
1339
  onSome: (existing) => [[existing, false], current],
1284
1340
  onNone: () => [[fresh, true], Option.some(fresh)]
1285
1341
  }));
1286
1342
  if (isLeader) {
1287
- 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)))));
1288
1344
  }
1289
1345
  return yield* restore(Deferred4.await(deferred));
1290
1346
  }));
@@ -1315,44 +1371,67 @@ var loadTransportConstructor = async (transportKey, moduleSpecifier) => {
1315
1371
  }
1316
1372
  };
1317
1373
  function createTransportScoped(transportKey, openMethod, serviceName, config) {
1318
- return Effect8.fnUntraced(function* (options) {
1374
+ return Effect9.fnUntraced(function* (options) {
1319
1375
  const { retry: transportRetry, reconnect: reconnectOptions } = options;
1320
1376
  const connectTimeout = options.connectTimeout === undefined ? undefined : Duration6.fromInputUnsafe(options.connectTimeout);
1321
1377
  const openOptions = { ...options };
1322
1378
  Reflect.deleteProperty(openOptions, "retry");
1323
1379
  Reflect.deleteProperty(openOptions, "reconnect");
1324
1380
  Reflect.deleteProperty(openOptions, "connectTimeout");
1325
- 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"));
1326
1394
  let transport = null;
1327
1395
  const opening = yield* makeInFlight();
1328
1396
  const reconnecting = yield* makeInFlight();
1329
1397
  const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
1330
1398
  const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
1331
1399
  const supervised = reconnectOptions ? resolvedReconnect : null;
1332
- const serviceScope = yield* Effect8.scope;
1400
+ const serviceScope = yield* Effect9.scope;
1333
1401
  const clientSet = new Map;
1402
+ const touchedUnits = new Set;
1334
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
+ });
1335
1410
  const transportClosed = () => new ModbusNotConnectedError({
1336
1411
  cause: new Error("Transport has been closed"),
1337
1412
  message: "Transport has been closed"
1338
1413
  });
1339
- const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
1414
+ const closeOrphan = (t) => Effect9.forkDetach(Effect9.ignore(Effect9.tryPromise(() => t.close())));
1340
1415
  const limitConnect = (wait) => {
1341
1416
  if (connectTimeout === undefined)
1342
1417
  return wait;
1343
1418
  const message = `${serviceName}: connect did not complete within ${Duration6.format(connectTimeout)}`;
1344
- return Effect8.timeoutOrElse(wait, {
1419
+ return Effect9.timeoutOrElse(wait, {
1345
1420
  duration: connectTimeout,
1346
- orElse: () => Effect8.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
1421
+ orElse: () => Effect9.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
1347
1422
  });
1348
1423
  };
1349
- const openTransport = Effect8.tryPromise({
1424
+ const openTransport = Effect9.tryPromise({
1350
1425
  try: () => openMethod(TC, openOptions),
1351
1426
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1352
- }).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);
1353
1432
  transport = t;
1354
1433
  }), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
1355
- const ensureOpen = Effect8.fnUntraced(function* () {
1434
+ const ensureOpen = Effect9.fnUntraced(function* () {
1356
1435
  if (closed)
1357
1436
  return yield* transportClosed();
1358
1437
  if (transport)
@@ -1362,56 +1441,65 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1362
1441
  return yield* transportClosed();
1363
1442
  return t;
1364
1443
  });
1365
- const closeTransport = Effect8.suspend(() => {
1444
+ const closeTransport = Effect9.suspend(() => {
1366
1445
  if (closed)
1367
- return Effect8.void;
1446
+ return Effect9.void;
1368
1447
  closed = true;
1369
1448
  const t = transport;
1370
1449
  if (!t)
1371
1450
  return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
1372
- return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
1451
+ return Effect9.andThen(Effect9.logDebug(`Closing ${serviceName}`), Effect9.andThen(Effect9.tryPromise({
1373
1452
  try: () => t.close(),
1374
1453
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1375
1454
  }), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
1376
1455
  });
1377
- yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
1456
+ yield* Effect9.addFinalizer(() => Effect9.orDie(closeTransport));
1378
1457
  const notConnectedMsg = "Transport is not connected. Call withClient() first.";
1379
- 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({
1380
1460
  try: () => t.reconnect(),
1381
1461
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1382
- }).pipe(Effect8.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
1383
- 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(() => {
1384
1464
  const t = transport;
1385
1465
  if (closed || !t)
1386
1466
  return transportClosed();
1387
1467
  return reconnectTransport(t);
1388
1468
  });
1389
1469
  const report = (error) => {
1390
- if (closed || !resolvedReconnect.triggers(error))
1391
- return Effect8.void;
1470
+ if (closed)
1471
+ return Effect9.void;
1472
+ if (!resolvedReconnect.triggers(error))
1473
+ return Effect9.void;
1392
1474
  if (!supervised) {
1393
1475
  return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
1394
1476
  }
1395
- 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}`));
1396
1478
  };
1397
1479
  const resilience = {
1398
- guard: supervised ? guardCircuit(connectionState) : Effect8.void,
1480
+ guard: supervised ? Effect9.suspend(() => transport === null ? Effect9.void : guardCircuit(connectionState)) : Effect9.void,
1481
+ aroundAttempt: aroundNative,
1399
1482
  onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
1400
1483
  report
1401
1484
  };
1402
- const makeOperations = Effect8.fnUntraced(function* (unitId) {
1485
+ const resolveClient = Effect9.fnUntraced(function* (unitId) {
1403
1486
  const t = yield* ensureOpen();
1404
1487
  let client = clientSet.get(unitId);
1405
1488
  if (!client) {
1406
- client = yield* Effect8.try({
1489
+ client = yield* Effect9.try({
1407
1490
  try: () => t.createClient({ unitId }),
1408
1491
  catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
1409
1492
  });
1410
1493
  clientSet.set(unitId, client);
1494
+ touchedUnits.add(unitId);
1411
1495
  }
1412
- return createEffectModbusClient(client);
1496
+ return client;
1497
+ });
1498
+ const makeOperations = Effect9.fnUntraced(function* (unitId) {
1499
+ yield* withNativeLock(resolveClient(unitId));
1500
+ return createEffectModbusClient(resolveClient(unitId));
1413
1501
  });
1414
- const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1502
+ const makeClient = Effect9.fnUntraced(function* (unitId, clientOptions) {
1415
1503
  const operations = yield* makeOperations(unitId);
1416
1504
  return withResilience(operations, {
1417
1505
  ...resilience,
@@ -1423,7 +1511,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1423
1511
  connectionState,
1424
1512
  scope: serviceScope
1425
1513
  });
1426
- const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
1514
+ const withClient = Effect9.fnUntraced(function* (unitId, clientOptions) {
1427
1515
  const operations = yield* makeOperations(unitId);
1428
1516
  return withResilience(batching.guardRawWrites(unitId, operations), {
1429
1517
  ...resilience,
@@ -1433,7 +1521,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1433
1521
  return {
1434
1522
  connectionState,
1435
1523
  withClient,
1436
- setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
1524
+ setRequestTimeout: Effect9.fnUntraced(function* (timeoutMs) {
1437
1525
  const t = transport;
1438
1526
  if (!t || closed) {
1439
1527
  return yield* new ModbusNotConnectedError({
@@ -1442,8 +1530,10 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1442
1530
  });
1443
1531
  }
1444
1532
  t.setRequestTimeout(timeoutMs);
1533
+ nativeLimit = timeoutMs;
1534
+ requestLimitOverride = timeoutMs;
1445
1535
  }),
1446
- clearRequestTimeout: Effect8.fnUntraced(function* () {
1536
+ clearRequestTimeout: Effect9.fnUntraced(function* () {
1447
1537
  const t = transport;
1448
1538
  if (!t || closed) {
1449
1539
  return yield* new ModbusNotConnectedError({
@@ -1452,8 +1542,10 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1452
1542
  });
1453
1543
  }
1454
1544
  t.clearRequestTimeout();
1545
+ nativeLimit = undefined;
1546
+ requestLimitOverride = null;
1455
1547
  }),
1456
- reconnect: Effect8.fnUntraced(function* () {
1548
+ reconnect: Effect9.fnUntraced(function* () {
1457
1549
  if (closed)
1458
1550
  return yield* transportClosed();
1459
1551
  const t = transport;
@@ -1466,16 +1558,15 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1466
1558
  return yield* transportClosed();
1467
1559
  yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
1468
1560
  }),
1469
- close: Effect8.fnUntraced(function* () {
1561
+ close: Effect9.fnUntraced(function* (scope) {
1470
1562
  if (closed)
1471
1563
  return;
1472
- const scope = yield* Effect8.scope;
1473
- yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
1564
+ yield* Scope3.close(scope, Exit4.void).pipe(Effect9.onExit(() => closeTransport));
1474
1565
  }),
1475
1566
  withBatchingClient: batching.withBatchingClient,
1476
1567
  batchingClient: batching.batchingClient,
1477
1568
  get touchedUnits() {
1478
- return new Set(clientSet.keys());
1569
+ return new Set(touchedUnits);
1479
1570
  },
1480
1571
  hasPendingRequests: () => {
1481
1572
  if (closed)
@@ -1491,9 +1582,19 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
1491
1582
 
1492
1583
  // src/AsciiTransportService.ts
1493
1584
  class AsciiTransportService extends Context.Service()("AsciiTransportService") {
1494
- static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
1495
- return transportConstructor.open(options);
1496
- }, "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
+ });
1497
1598
  static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
1498
1599
  static makeMockTransport = (devices) => {
1499
1600
  const factory = createMockTransport(devices);
@@ -1504,11 +1605,21 @@ class AsciiTransportService extends Context.Service()("AsciiTransportService") {
1504
1605
  import { Context as Context3, Layer as Layer3 } from "effect";
1505
1606
 
1506
1607
  // src/RtuTransportService.ts
1507
- import { Context as Context2, Layer as Layer2 } from "effect";
1608
+ import { Context as Context2, Effect as Effect11, Layer as Layer2 } from "effect";
1508
1609
  class RtuTransportService extends Context2.Service()("RtuTransportService") {
1509
- static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
1510
- return transportConstructor.open(options);
1511
- }, "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
+ });
1512
1623
  static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
1513
1624
  static makeMockTransport = (devices) => {
1514
1625
  const factory = createMockTransport(devices);
@@ -1533,7 +1644,10 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
1533
1644
  import { Context as Context4, Layer as Layer4 } from "effect";
1534
1645
  class TcpTransportService extends Context4.Service()("TcpTransportService") {
1535
1646
  static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
1536
- return transportConstructor.connect(options);
1647
+ return transportConstructor.connect({
1648
+ ...options,
1649
+ responseTimeoutMs: options.responseTimeoutMs ?? options.requestTimeoutMs
1650
+ });
1537
1651
  }, "TcpTransportService");
1538
1652
  static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
1539
1653
  static makeMockTransport = (devices) => {
@@ -1542,58 +1656,58 @@ class TcpTransportService extends Context4.Service()("TcpTransportService") {
1542
1656
  };
1543
1657
  }
1544
1658
  // src/SerialModbusServerService.ts
1545
- import { Effect as Effect9, Layer as Layer5 } from "effect";
1546
- var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
1547
- const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
1548
- 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({
1549
1663
  try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
1550
1664
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1551
1665
  });
1552
- yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
1553
- 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({
1554
1668
  try: () => server.shutdown(),
1555
1669
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1556
- })), Effect9.catch(() => Effect9.void)));
1670
+ })), Effect12.catch(() => Effect12.void)));
1557
1671
  }));
1558
- var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
1559
- const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
1560
- 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({
1561
1675
  try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
1562
1676
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1563
1677
  });
1564
- yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
1565
- 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({
1566
1680
  try: () => server.shutdown(),
1567
1681
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1568
- })), Effect9.catch(() => Effect9.void)));
1682
+ })), Effect12.catch(() => Effect12.void)));
1569
1683
  }));
1570
1684
  // src/TcpModbusServerService.ts
1571
- import { Effect as Effect10, Layer as Layer6 } from "effect";
1572
- var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(function* () {
1573
- const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
1574
- 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({
1575
1689
  try: () => AsyncTcpModbusServer.bind(options, handlers),
1576
1690
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1577
1691
  });
1578
- yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
1579
- 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({
1580
1694
  try: () => server.shutdown(),
1581
1695
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1582
- })), Effect10.catch(() => Effect10.void)));
1696
+ })), Effect13.catch(() => Effect13.void)));
1583
1697
  }));
1584
1698
  // src/TcpGatewayService.ts
1585
- import { Effect as Effect11, Layer as Layer7 } from "effect";
1586
- var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.gen(function* () {
1587
- const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
1588
- 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({
1589
1703
  try: () => AsyncTcpGateway.bind(options, gatewayConfig),
1590
1704
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1591
1705
  });
1592
- yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
1593
- 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({
1594
1708
  try: () => gateway.shutdown(),
1595
1709
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1596
- })), Effect11.catch(() => Effect11.void)));
1710
+ })), Effect14.catch(() => Effect14.void)));
1597
1711
  }));
1598
1712
  // src/WasmWsTransportService.ts
1599
1713
  import { Context as Context5, Layer as Layer8 } from "effect";
@@ -1646,8 +1760,8 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
1646
1760
  };
1647
1761
  }
1648
1762
  // src/WasmSerialPort.ts
1649
- import { Effect as Effect12 } from "effect";
1650
- var requestSerialPort = () => Effect12.tryPromise({
1763
+ import { Effect as Effect15 } from "effect";
1764
+ var requestSerialPort = () => Effect15.tryPromise({
1651
1765
  try: async () => {
1652
1766
  const mod = await import("modbus-rs/web");
1653
1767
  return mod.requestSerialPort();
@@ -1655,60 +1769,61 @@ var requestSerialPort = () => Effect12.tryPromise({
1655
1769
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1656
1770
  });
1657
1771
  // src/WasmTcpServerService.ts
1658
- import { Effect as Effect13, Layer as Layer12 } from "effect";
1659
- var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
1660
- const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
1661
- 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({
1662
1776
  try: () => WasmWsModbusServer.bind(options, handlers),
1663
1777
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1664
1778
  });
1665
- yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1666
- yield* Effect13.forkScoped(Effect13.tryPromise({
1779
+ yield* Effect16.logDebug(`WASM WS server bound to ${options.wsUrl}`);
1780
+ yield* Effect16.forkScoped(Effect16.tryPromise({
1667
1781
  try: () => server.serve(),
1668
1782
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1669
- }).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
1670
- 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({
1671
1785
  try: () => server.shutdown(),
1672
1786
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1673
- })), Effect13.catch(() => Effect13.void)));
1787
+ })), Effect16.catch(() => Effect16.void)));
1674
1788
  }));
1675
1789
  // src/WasmSerialModbusServerService.ts
1676
- import { Effect as Effect14, Layer as Layer13 } from "effect";
1677
- var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1678
- const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1679
- 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({
1680
1794
  try: () => WasmSerialModbusServer.bindRtu(options, handlers),
1681
1795
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1682
1796
  });
1683
- yield* Effect14.logDebug("WASM serial RTU server bound");
1684
- yield* Effect14.forkScoped(Effect14.tryPromise({
1797
+ yield* Effect17.logDebug("WASM serial RTU server bound");
1798
+ yield* Effect17.forkScoped(Effect17.tryPromise({
1685
1799
  try: () => server.serve(),
1686
1800
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1687
- }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
1688
- 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({
1689
1803
  try: () => server.shutdown(),
1690
1804
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1691
- })), Effect14.catch(() => Effect14.void)));
1805
+ })), Effect17.catch(() => Effect17.void)));
1692
1806
  }));
1693
- var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
1694
- const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
1695
- 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({
1696
1810
  try: () => WasmSerialModbusServer.bindAscii(options, handlers),
1697
1811
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1698
1812
  });
1699
- yield* Effect14.logDebug("WASM serial ASCII server bound");
1700
- yield* Effect14.forkScoped(Effect14.tryPromise({
1813
+ yield* Effect17.logDebug("WASM serial ASCII server bound");
1814
+ yield* Effect17.forkScoped(Effect17.tryPromise({
1701
1815
  try: () => server.serve(),
1702
1816
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1703
- }).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
1704
- 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({
1705
1819
  try: () => server.shutdown(),
1706
1820
  catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
1707
- })), Effect14.catch(() => Effect14.void)));
1821
+ })), Effect17.catch(() => Effect17.void)));
1708
1822
  }));
1709
1823
  export {
1710
1824
  AsciiTransportService,
1711
1825
  ConnectionState,
1826
+ MAX_SERIAL_PORT_PATH,
1712
1827
  MODBUS_MAX_READ_REGISTERS,
1713
1828
  MODBUS_MAX_WRITE_REGISTERS,
1714
1829
  ModbusCircuitOpenError,
@@ -1739,6 +1854,7 @@ export {
1739
1854
  planReads,
1740
1855
  planWrites,
1741
1856
  requestSerialPort,
1857
+ resolveSerialPortPath,
1742
1858
  retryModbus,
1743
1859
  retryableExceptionCodes,
1744
1860
  serialAsciiServerLayer,