@flux-control/effect-modbus-rs 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.js +19 -10
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/shared-transport.d.ts +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -711,6 +711,32 @@ Effect.gen(function* () {
|
|
|
711
711
|
});
|
|
712
712
|
```
|
|
713
713
|
|
|
714
|
+
### Connect timeout
|
|
715
|
+
|
|
716
|
+
The `modbus-rs` bindings do not limit the time of a connect. A TCP connect to a host that does not answer waits until the operating system stops it. On Linux with default settings, this takes about 2 minutes. During that time, every operation joins the same pending connect.
|
|
717
|
+
|
|
718
|
+
Set `connectTimeout` to limit how long each operation waits for an open or a reconnect:
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
TcpTransportService.make({
|
|
722
|
+
host: '192.0.2.1',
|
|
723
|
+
port: 502,
|
|
724
|
+
connectTimeout: '3 seconds', // limit for each wait on an open or reconnect
|
|
725
|
+
reconnect: {},
|
|
726
|
+
});
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
The limit applies to each operation that waits for an open or reconnect. When it expires, that operation fails with `ModbusTimeoutError`.
|
|
730
|
+
|
|
731
|
+
The native connect cannot be cancelled, so it continues after the timeout:
|
|
732
|
+
|
|
733
|
+
- Later operations wait for the same native connect, each for at most the limit. A transport never has more than one pending native connect.
|
|
734
|
+
- When the native connect succeeds later, the transport uses it. A late reconnect that succeeds after the supervisor published `Down` publishes `Connected` at once.
|
|
735
|
+
- When the native connect fails, the next operation starts a new connect.
|
|
736
|
+
- A supervised reconnect attempt that times out is retried by the reconnect policy by default. When the attempts are exhausted, the transport publishes `Down`.
|
|
737
|
+
|
|
738
|
+
The option applies to every transport. It is unbounded by default. `requestTimeoutMs` limits each request, but it does not limit the connect.
|
|
739
|
+
|
|
714
740
|
### Retrying a transaction
|
|
715
741
|
|
|
716
742
|
`retryModbus(policy)` remains exported for the one case the transport cannot express: driving a **compound** operation as a unit, where retrying individual frames would be wrong.
|
package/dist/index.js
CHANGED
|
@@ -1250,7 +1250,7 @@ var createMockTransport = (devices) => {
|
|
|
1250
1250
|
};
|
|
1251
1251
|
|
|
1252
1252
|
// src/shared-transport.ts
|
|
1253
|
-
import { Deferred as Deferred4, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
|
|
1253
|
+
import { Deferred as Deferred4, Duration as Duration6, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
|
|
1254
1254
|
var makeInFlight = () => Ref.make(Option.none());
|
|
1255
1255
|
var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
|
|
1256
1256
|
const fresh = yield* Deferred4.make();
|
|
@@ -1292,9 +1292,11 @@ var loadTransportConstructor = async (transportKey, moduleSpecifier) => {
|
|
|
1292
1292
|
function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
1293
1293
|
return Effect8.fnUntraced(function* (options) {
|
|
1294
1294
|
const { retry: transportRetry, reconnect: reconnectOptions } = options;
|
|
1295
|
+
const connectTimeout = options.connectTimeout === undefined ? undefined : Duration6.fromInputUnsafe(options.connectTimeout);
|
|
1295
1296
|
const openOptions = { ...options };
|
|
1296
1297
|
Reflect.deleteProperty(openOptions, "retry");
|
|
1297
1298
|
Reflect.deleteProperty(openOptions, "reconnect");
|
|
1299
|
+
Reflect.deleteProperty(openOptions, "connectTimeout");
|
|
1298
1300
|
const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
|
|
1299
1301
|
let transport = null;
|
|
1300
1302
|
const opening = yield* makeInFlight();
|
|
@@ -1310,6 +1312,15 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
1310
1312
|
message: "Transport has been closed"
|
|
1311
1313
|
});
|
|
1312
1314
|
const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
|
|
1315
|
+
const limitConnect = (wait) => {
|
|
1316
|
+
if (connectTimeout === undefined)
|
|
1317
|
+
return wait;
|
|
1318
|
+
const message = `${serviceName}: connect did not complete within ${Duration6.format(connectTimeout)}`;
|
|
1319
|
+
return Effect8.timeoutOrElse(wait, {
|
|
1320
|
+
duration: connectTimeout,
|
|
1321
|
+
orElse: () => Effect8.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
|
|
1322
|
+
});
|
|
1323
|
+
};
|
|
1313
1324
|
const openTransport = Effect8.tryPromise({
|
|
1314
1325
|
try: () => openMethod(TC, openOptions),
|
|
1315
1326
|
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
@@ -1321,7 +1332,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
1321
1332
|
return yield* transportClosed();
|
|
1322
1333
|
if (transport)
|
|
1323
1334
|
return transport;
|
|
1324
|
-
const t = yield* singleFlight(opening, openTransport);
|
|
1335
|
+
const t = yield* limitConnect(singleFlight(opening, openTransport));
|
|
1325
1336
|
if (closed)
|
|
1326
1337
|
return yield* transportClosed();
|
|
1327
1338
|
return t;
|
|
@@ -1340,14 +1351,15 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
1340
1351
|
});
|
|
1341
1352
|
yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
|
|
1342
1353
|
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
1354
|
+
const reconnectTransport = (t) => limitConnect(singleFlight(reconnecting, Effect8.tryPromise({
|
|
1355
|
+
try: () => t.reconnect(),
|
|
1356
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1357
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
|
|
1343
1358
|
const reconnectOnce = Effect8.suspend(() => {
|
|
1344
1359
|
const t = transport;
|
|
1345
1360
|
if (closed || !t)
|
|
1346
1361
|
return transportClosed();
|
|
1347
|
-
return
|
|
1348
|
-
try: () => t.reconnect(),
|
|
1349
|
-
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1350
|
-
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
1362
|
+
return reconnectTransport(t);
|
|
1351
1363
|
});
|
|
1352
1364
|
const report = (error) => {
|
|
1353
1365
|
if (closed || !resolvedReconnect.triggers(error))
|
|
@@ -1424,10 +1436,7 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
1424
1436
|
yield* ensureOpen();
|
|
1425
1437
|
return;
|
|
1426
1438
|
}
|
|
1427
|
-
yield*
|
|
1428
|
-
try: () => t.reconnect(),
|
|
1429
|
-
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1430
|
-
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
1439
|
+
yield* reconnectTransport(t);
|
|
1431
1440
|
if (closed)
|
|
1432
1441
|
return yield* transportClosed();
|
|
1433
1442
|
yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -30,6 +30,9 @@ declare const ModbusTimeoutError_base: new <A extends Record<string, any> = {}>(
|
|
|
30
30
|
* with {@link RtuTransportOptions.requestTimeoutMs | requestTimeoutMs} /
|
|
31
31
|
* {@link RtuTransportOptions.responseTimeoutMs | responseTimeoutMs}.
|
|
32
32
|
*
|
|
33
|
+
* The transport also raises this error locally when an operation waits for an
|
|
34
|
+
* open or reconnect longer than the `connectTimeout` transport option.
|
|
35
|
+
*
|
|
33
36
|
* @see ModbusErrorCode.TIMEOUT — `modbus-rs` error code that triggers this error.
|
|
34
37
|
*/
|
|
35
38
|
export declare class ModbusTimeoutError extends ModbusTimeoutError_base<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Scope, SubscriptionRef } from 'effect';
|
|
1
|
+
import { Duration, Effect, Scope, SubscriptionRef } from 'effect';
|
|
2
2
|
import type { AsyncAsciiTransport, AsyncRtuTransport, AsyncTcpTransport } from 'modbus-rs';
|
|
3
3
|
import type { WasmAsciiTransport, WasmRtuTransport, WasmWsTransport } from 'modbus-rs/web';
|
|
4
4
|
import { type BatchingClientOptions, type BatchingModbusClient } from './batching-client';
|
|
@@ -24,6 +24,24 @@ export interface TransportResilienceOptions {
|
|
|
24
24
|
* enables the circuit breaker that keeps callers off a dead bus.
|
|
25
25
|
*/
|
|
26
26
|
readonly reconnect?: ReconnectOptions;
|
|
27
|
+
/**
|
|
28
|
+
* Time limit for each operation that waits for an open or reconnect of the
|
|
29
|
+
* underlying transport.
|
|
30
|
+
*
|
|
31
|
+
* The `modbus-rs` bindings do not limit the connect time. A TCP connect to a
|
|
32
|
+
* host that does not answer waits until the operating system stops it, which
|
|
33
|
+
* takes about 2 minutes on Linux with default settings.
|
|
34
|
+
*
|
|
35
|
+
* The limit applies to each caller that waits for an open or reconnect.
|
|
36
|
+
* When it expires, that caller fails with {@link ModbusTimeoutError}.
|
|
37
|
+
*
|
|
38
|
+
* The native connect cannot be cancelled, so it continues. Later callers wait
|
|
39
|
+
* for the same native connect, each for at most this limit, so one transport
|
|
40
|
+
* has at most one pending native connect. When the native connect succeeds
|
|
41
|
+
* later, the transport uses it. When it fails, the next caller starts a new
|
|
42
|
+
* connect. Unbounded by default.
|
|
43
|
+
*/
|
|
44
|
+
readonly connectTimeout?: Duration.Input;
|
|
27
45
|
}
|
|
28
46
|
/**
|
|
29
47
|
* The `modbus-rs` transport-level retry knobs this package deliberately does
|