@flux-control/effect-modbus-rs 0.6.0 → 0.8.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 +19 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +31 -7
- package/dist/src/AsciiTransportService.d.ts +2 -2
- package/dist/src/RtuTransportService.d.ts +2 -2
- package/dist/src/TcpTransportService.d.ts +2 -2
- package/dist/src/WasmAsciiTransportService.d.ts +2 -2
- package/dist/src/WasmRtuTransportService.d.ts +2 -2
- package/dist/src/WasmWsTransportService.d.ts +2 -2
- package/dist/src/batching-client.d.ts +34 -3
- package/dist/src/errors.d.ts +36 -0
- package/dist/src/mocks.d.ts +2 -2
- package/dist/src/shared-transport.d.ts +17 -8
- package/dist/src/write-debouncer.d.ts +13 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -394,7 +394,23 @@ const damper =
|
|
|
394
394
|
const same = yield * transport.batchingClient(3); // elsewhere, no options to restate
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
`withBatchingClient` declares. Declaring a unit twice fails with `
|
|
397
|
+
`withBatchingClient` declares. Declaring a unit twice fails with `ModbusUnitAlreadyDeclaredError`, whatever the second call asks for. `batchingClient` looks up, and fails with `ModbusInvalidArgumentError` when nothing has declared that unit. A lookup that arrives while a declaration is still in flight waits for it, so neither call has to know which one ran first.
|
|
398
|
+
|
|
399
|
+
A declaration belongs to the transport scope, not to the caller. It stays when the caller is interrupted, and when the owner that made it closes while the transport stays open. An owner that can start again over the same transport can catch the error and recover the existing client:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const damper =
|
|
403
|
+
yield *
|
|
404
|
+
transport
|
|
405
|
+
.withBatchingClient(3, { debounce })
|
|
406
|
+
.pipe(
|
|
407
|
+
Effect.catchTag('ModbusUnitAlreadyDeclaredError', (error) =>
|
|
408
|
+
transport.batchingClient(error.unitId),
|
|
409
|
+
),
|
|
410
|
+
);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The recovered client keeps the options of the first declaration. `client.debounce` gives its debounce windows as frozen, resolved `Duration` values, so the owner can compare them with the windows that it expects. An omitted `maxHold` shows as four times the write window, which is the limit that the client applies.
|
|
398
414
|
|
|
399
415
|
### Shutdown
|
|
400
416
|
|
|
@@ -547,6 +563,8 @@ Errors from the underlying Rust layer are mapped to typed `Effect` errors via `D
|
|
|
547
563
|
|
|
548
564
|
Handle with `Effect.catchTags`. The `ModbusError` union type covers all seven variants.
|
|
549
565
|
|
|
566
|
+
`withBatchingClient` can also fail with `ModbusUnitAlreadyDeclaredError` when the unit already has a batching client. This error is not a member of `ModbusError`, because it never comes from the bus. See [Declaring a unit, and reaching for it](#declaring-a-unit-and-reaching-for-it).
|
|
567
|
+
|
|
550
568
|
## Resilience: retries, reconnection, and circuit breaking
|
|
551
569
|
|
|
552
570
|
> **These are application-level policies, and they are opt-in.**
|
package/dist/index.d.ts
CHANGED
|
@@ -159,7 +159,7 @@ export type { DebouncedWrite, WriteDebouncer, WriteDebouncerOptions } from './sr
|
|
|
159
159
|
export { createReadDebouncer } from './src/read-debouncer';
|
|
160
160
|
export type { ReadDebouncer, ReadDebouncerOptions } from './src/read-debouncer';
|
|
161
161
|
export { makeBatchingClient, createBatchingRegistry } from './src/batching-client';
|
|
162
|
-
export type { BatchingClientOptions, BatchingDebounceOptions, BatchingModbusClient, BatchingRegistry, BatchingRegistryDeps, BatchingRegisterReader, } from './src/batching-client';
|
|
162
|
+
export type { BatchingClientOptions, BatchingDebounceOptions, BatchingDebounceWindows, BatchingModbusClient, BatchingRegistry, BatchingRegistryDeps, BatchingRegisterReader, } from './src/batching-client';
|
|
163
163
|
export { mergeSpanAttributes } from './src/span-attributes';
|
|
164
164
|
export type { ModbusSpanAttributes } from './src/span-attributes';
|
|
165
165
|
export { AsciiTransportService } from './src/AsciiTransportService';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,9 @@ class ModbusNotConnectedError extends Data.TaggedError("ModbusNotConnectedError"
|
|
|
25
25
|
|
|
26
26
|
class ModbusCircuitOpenError extends Data.TaggedError("ModbusCircuitOpenError") {
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
class ModbusUnitAlreadyDeclaredError extends Data.TaggedError("ModbusUnitAlreadyDeclaredError") {
|
|
30
|
+
}
|
|
28
31
|
function parseExceptionCode(message) {
|
|
29
32
|
const m = message.match(/\[MODBUS_EXCEPTION:(\d+)\]/);
|
|
30
33
|
return m ? Number(m[1]) : undefined;
|
|
@@ -393,9 +396,13 @@ var createRegisterCache = () => {
|
|
|
393
396
|
};
|
|
394
397
|
// src/write-debouncer.ts
|
|
395
398
|
import { Clock, Deferred, Duration as Duration3, Effect as Effect3, Fiber, Semaphore } from "effect";
|
|
399
|
+
var resolveWriteHold = (window, maxHold) => {
|
|
400
|
+
const windowMs = Duration3.toMillis(Duration3.fromInputUnsafe(window));
|
|
401
|
+
const maxHoldMs = maxHold === undefined ? windowMs * 4 : Duration3.toMillis(Duration3.fromInputUnsafe(maxHold));
|
|
402
|
+
return { windowMs, maxHoldMs };
|
|
403
|
+
};
|
|
396
404
|
var createWriteDebouncer = (options) => Effect3.gen(function* () {
|
|
397
|
-
const windowMs =
|
|
398
|
-
const maxHoldMs = options.maxHold === undefined ? windowMs * 4 : Duration3.toMillis(Duration3.fromInputUnsafe(options.maxHold));
|
|
405
|
+
const { windowMs, maxHoldMs } = resolveWriteHold(options.window, options.maxHold);
|
|
399
406
|
const scope = yield* Effect3.scope;
|
|
400
407
|
const lock = yield* Semaphore.make(1);
|
|
401
408
|
const flushLock = yield* Semaphore.make(1);
|
|
@@ -663,6 +670,7 @@ var createReadDebouncer = (options) => Effect4.gen(function* () {
|
|
|
663
670
|
// src/batching-client.ts
|
|
664
671
|
import {
|
|
665
672
|
Deferred as Deferred3,
|
|
673
|
+
Duration as Duration5,
|
|
666
674
|
Effect as Effect5,
|
|
667
675
|
Exit as Exit2,
|
|
668
676
|
Fiber as Fiber3,
|
|
@@ -693,6 +701,22 @@ var mergeSpanAttributes = (sources) => {
|
|
|
693
701
|
};
|
|
694
702
|
|
|
695
703
|
// src/batching-client.ts
|
|
704
|
+
var resolveDebounceWindows = (options) => {
|
|
705
|
+
if (options === undefined)
|
|
706
|
+
return;
|
|
707
|
+
const hold = options.writes === undefined ? undefined : resolveWriteHold(options.writes.window, options.writes.maxHold);
|
|
708
|
+
return Object.freeze({
|
|
709
|
+
...hold && {
|
|
710
|
+
writes: Object.freeze({
|
|
711
|
+
window: Duration5.millis(hold.windowMs),
|
|
712
|
+
maxHold: Duration5.millis(hold.maxHoldMs)
|
|
713
|
+
})
|
|
714
|
+
},
|
|
715
|
+
...options.reads && {
|
|
716
|
+
reads: Object.freeze({ window: Duration5.fromInputUnsafe(options.reads.window) })
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
};
|
|
696
720
|
var makeBatchingClient = (options) => Effect5.gen(function* () {
|
|
697
721
|
const { unitId, client, cache } = options;
|
|
698
722
|
const scope = yield* Effect5.scope;
|
|
@@ -772,6 +796,7 @@ var makeBatchingClient = (options) => Effect5.gen(function* () {
|
|
|
772
796
|
return {
|
|
773
797
|
unitId,
|
|
774
798
|
cache,
|
|
799
|
+
debounce: resolveDebounceWindows(options.debounce),
|
|
775
800
|
write: writes.write,
|
|
776
801
|
writeNow: writes.writeNow,
|
|
777
802
|
writeAll: writes.writeAll,
|
|
@@ -842,7 +867,7 @@ var createBatchingRegistry = (deps) => {
|
|
|
842
867
|
};
|
|
843
868
|
const alreadyDeclaredError = (unitId) => {
|
|
844
869
|
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
|
|
870
|
+
return new ModbusUnitAlreadyDeclaredError({ cause: new Error(message), message, unitId });
|
|
846
871
|
};
|
|
847
872
|
const notDeclaredError = (unitId) => {
|
|
848
873
|
const message = `No batching client is declared for unit ${unitId}. Declare one with ` + `withBatchingClient(${unitId}) before reaching for it.`;
|
|
@@ -1238,10 +1263,9 @@ var createMockTransport = (devices) => {
|
|
|
1238
1263
|
setRequestTimeout: (_timeoutMs) => ensureOpen,
|
|
1239
1264
|
clearRequestTimeout: () => ensureOpen,
|
|
1240
1265
|
reconnect: () => Effect7.andThen(reconnectOnce, SubscriptionRef3.set(connectionState, ConnectionState.Connected())),
|
|
1241
|
-
close: () => Effect7.gen(function* () {
|
|
1266
|
+
close: (scope) => Effect7.gen(function* () {
|
|
1242
1267
|
if (closed)
|
|
1243
1268
|
return;
|
|
1244
|
-
const scope = yield* Effect7.scope;
|
|
1245
1269
|
yield* Scope2.close(scope, Exit3.void).pipe(Effect7.onExit(() => closeTransport));
|
|
1246
1270
|
}),
|
|
1247
1271
|
hasPendingRequests: () => false
|
|
@@ -1441,10 +1465,9 @@ function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
1441
1465
|
return yield* transportClosed();
|
|
1442
1466
|
yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
|
|
1443
1467
|
}),
|
|
1444
|
-
close: Effect8.fnUntraced(function* () {
|
|
1468
|
+
close: Effect8.fnUntraced(function* (scope) {
|
|
1445
1469
|
if (closed)
|
|
1446
1470
|
return;
|
|
1447
|
-
const scope = yield* Effect8.scope;
|
|
1448
1471
|
yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
|
|
1449
1472
|
}),
|
|
1450
1473
|
withBatchingClient: batching.withBatchingClient,
|
|
@@ -1694,6 +1717,7 @@ export {
|
|
|
1694
1717
|
ModbusNotConnectedError,
|
|
1695
1718
|
ModbusTimeoutError,
|
|
1696
1719
|
ModbusTransportError,
|
|
1720
|
+
ModbusUnitAlreadyDeclaredError,
|
|
1697
1721
|
RetryPolicies,
|
|
1698
1722
|
RtuTransportService,
|
|
1699
1723
|
SerialTransportService,
|
|
@@ -48,10 +48,10 @@ export declare class AsciiTransportService extends AsciiTransportService_base {
|
|
|
48
48
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
49
49
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
50
50
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
51
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
51
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
52
52
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
53
53
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
54
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
54
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
55
55
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
56
56
|
readonly touchedUnits: Set<number>;
|
|
57
57
|
hasPendingRequests: () => boolean;
|
|
@@ -48,10 +48,10 @@ export declare class RtuTransportService extends RtuTransportService_base {
|
|
|
48
48
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
49
49
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
50
50
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
51
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
51
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
52
52
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
53
53
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
54
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
54
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
55
55
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
56
56
|
readonly touchedUnits: Set<number>;
|
|
57
57
|
hasPendingRequests: () => boolean;
|
|
@@ -48,10 +48,10 @@ export declare class TcpTransportService extends TcpTransportService_base {
|
|
|
48
48
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
49
49
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
50
50
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
51
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
51
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
52
52
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
53
53
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
54
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
54
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
55
55
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
56
56
|
readonly touchedUnits: Set<number>;
|
|
57
57
|
hasPendingRequests: () => boolean;
|
|
@@ -54,10 +54,10 @@ export declare class WasmAsciiTransportService extends WasmAsciiTransportService
|
|
|
54
54
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
55
55
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
56
56
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
57
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
57
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
58
58
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
59
59
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
60
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
60
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
61
61
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
62
62
|
readonly touchedUnits: Set<number>;
|
|
63
63
|
hasPendingRequests: () => boolean;
|
|
@@ -54,10 +54,10 @@ export declare class WasmRtuTransportService extends WasmRtuTransportService_bas
|
|
|
54
54
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
55
55
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
56
56
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
57
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
57
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
58
58
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
59
59
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
60
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
60
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
61
61
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
62
62
|
readonly touchedUnits: Set<number>;
|
|
63
63
|
hasPendingRequests: () => boolean;
|
|
@@ -41,10 +41,10 @@ export declare class WasmWsTransportService extends WasmWsTransportService_base
|
|
|
41
41
|
setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
42
42
|
clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
|
|
43
43
|
reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
|
|
44
|
-
close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError,
|
|
44
|
+
close: (scope: import("effect/Scope").Closeable) => import("effect/Effect").Effect<void, import("./errors").ModbusError, never>;
|
|
45
45
|
withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
|
|
46
46
|
readonly retry?: import("./retry").ModbusRetryPolicy;
|
|
47
|
-
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
47
|
+
}) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError | import("./errors").ModbusUnitAlreadyDeclaredError>;
|
|
48
48
|
batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
|
|
49
49
|
readonly touchedUnits: Set<number>;
|
|
50
50
|
hasPendingRequests: () => boolean;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Duration, Effect, Scope, SubscriptionRef } from 'effect';
|
|
14
14
|
import { ConnectionState } from './connection';
|
|
15
|
-
import { type ModbusError } from './errors';
|
|
15
|
+
import { ModbusUnitAlreadyDeclaredError, type ModbusError } from './errors';
|
|
16
16
|
import type { EffectModbusClient, ModbusOperations } from './modbus-client';
|
|
17
17
|
import { type RegisterCache } from './register-cache';
|
|
18
18
|
import { type PlanReadsOptions, type PlanWritesOptions, type RegisterWrite } from './register-plan';
|
|
@@ -44,6 +44,23 @@ export interface BatchingDebounceOptions {
|
|
|
44
44
|
readonly window: Duration.Input;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The debounce windows that a batching client applies, as resolved values.
|
|
49
|
+
*
|
|
50
|
+
* A write entry always has `maxHold`. When the declaration omitted it, it is
|
|
51
|
+
* four times the window, which is the limit the write debouncer applies.
|
|
52
|
+
*/
|
|
53
|
+
export interface BatchingDebounceWindows {
|
|
54
|
+
/** The write window and the hold limit. Omitted when writes are not debounced. */
|
|
55
|
+
readonly writes?: {
|
|
56
|
+
readonly window: Duration.Duration;
|
|
57
|
+
readonly maxHold: Duration.Duration;
|
|
58
|
+
};
|
|
59
|
+
/** The read window. Omitted when reads are not debounced. */
|
|
60
|
+
readonly reads?: {
|
|
61
|
+
readonly window: Duration.Duration;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
47
64
|
/** Options for {@link makeBatchingClient}. */
|
|
48
65
|
export interface BatchingClientOptions {
|
|
49
66
|
/**
|
|
@@ -111,6 +128,17 @@ export interface BatchingModbusClient extends BatchingRegisterReader {
|
|
|
111
128
|
readonly flush: Effect.Effect<void>;
|
|
112
129
|
/** The cache this client filters against, or `undefined` when `cache` is `false`. */
|
|
113
130
|
readonly cache: RegisterCache | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* The debounce windows this client applies, or `undefined` when its
|
|
133
|
+
* declaration had no `debounce` option.
|
|
134
|
+
*
|
|
135
|
+
* The values are resolved and frozen when the client is built. An omitted
|
|
136
|
+
* `maxHold` shows as four times the write window.
|
|
137
|
+
*
|
|
138
|
+
* A caller that recovers an existing client with `batchingClient` did not
|
|
139
|
+
* choose these windows. Compare them with the windows that the caller expects.
|
|
140
|
+
*/
|
|
141
|
+
readonly debounce: BatchingDebounceWindows | undefined;
|
|
114
142
|
/**
|
|
115
143
|
* Runs an action when the calling scope closes, while the bus is still open
|
|
116
144
|
* and before this client is torn down.
|
|
@@ -161,7 +189,10 @@ export interface BatchingRegistryDeps {
|
|
|
161
189
|
/** The batching half of a transport's API. */
|
|
162
190
|
export interface BatchingRegistry {
|
|
163
191
|
/**
|
|
164
|
-
* Declares the batching client for a unit.
|
|
192
|
+
* Declares the batching client for a unit.
|
|
193
|
+
*
|
|
194
|
+
* Fails with `ModbusUnitAlreadyDeclaredError` if the unit already has a
|
|
195
|
+
* completed declaration or a declaration in progress.
|
|
165
196
|
*
|
|
166
197
|
* The declaration belongs to the registry's transport scope, not to the
|
|
167
198
|
* calling fiber. Construction therefore continues if the caller is
|
|
@@ -171,7 +202,7 @@ export interface BatchingRegistry {
|
|
|
171
202
|
*/
|
|
172
203
|
withBatchingClient(unitId: number, options?: BatchingClientOptions & {
|
|
173
204
|
readonly retry?: ModbusRetryPolicy;
|
|
174
|
-
}): Effect.Effect<BatchingModbusClient, ModbusError>;
|
|
205
|
+
}): Effect.Effect<BatchingModbusClient, ModbusError | ModbusUnitAlreadyDeclaredError>;
|
|
175
206
|
/** The batching client for a unit. Fails if nothing has declared one. */
|
|
176
207
|
batchingClient(unitId: number): Effect.Effect<BatchingModbusClient, ModbusError>;
|
|
177
208
|
/** Disables raw holding-register writes once a batching client exists for the unit. */
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -162,6 +162,42 @@ export declare class ModbusCircuitOpenError extends ModbusCircuitOpenError_base<
|
|
|
162
162
|
readonly message: string;
|
|
163
163
|
}> {
|
|
164
164
|
}
|
|
165
|
+
declare const ModbusUnitAlreadyDeclaredError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
166
|
+
readonly _tag: "ModbusUnitAlreadyDeclaredError";
|
|
167
|
+
} & Readonly<A>;
|
|
168
|
+
/**
|
|
169
|
+
* Error indicating that a unit already has a batching client on this transport.
|
|
170
|
+
*
|
|
171
|
+
* This is a **local** error. `modbus-rs` never returns it. The transport
|
|
172
|
+
* raises it from `withBatchingClient` when the unit has a completed
|
|
173
|
+
* declaration or a declaration in progress.
|
|
174
|
+
*
|
|
175
|
+
* The declaration belongs to the transport scope, not to the caller. It stays
|
|
176
|
+
* after its caller is interrupted, and after the owner that made it closes.
|
|
177
|
+
* Catch this error to recover the existing client with `batchingClient(unitId)`.
|
|
178
|
+
*
|
|
179
|
+
* This error is not a member of {@link ModbusError}. It can occur only when a
|
|
180
|
+
* caller declares a unit. It never occurs on the bus, so retry policies do not
|
|
181
|
+
* apply to it.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* const client = yield* transport.withBatchingClient(3).pipe(
|
|
186
|
+
* Effect.catchTag('ModbusUnitAlreadyDeclaredError', (error) =>
|
|
187
|
+
* transport.batchingClient(error.unitId),
|
|
188
|
+
* ),
|
|
189
|
+
* );
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export declare class ModbusUnitAlreadyDeclaredError extends ModbusUnitAlreadyDeclaredError_base<{
|
|
193
|
+
/** A descriptive error. */
|
|
194
|
+
readonly cause: Error;
|
|
195
|
+
/** Human-readable explanation of the error. */
|
|
196
|
+
readonly message: string;
|
|
197
|
+
/** The unit that already has a batching client. */
|
|
198
|
+
readonly unitId: number;
|
|
199
|
+
}> {
|
|
200
|
+
}
|
|
165
201
|
/**
|
|
166
202
|
* Union of all typed Modbus errors emitted by this library.
|
|
167
203
|
*
|
package/dist/src/mocks.d.ts
CHANGED
|
@@ -153,12 +153,12 @@ export declare const createMockTransport: (devices: SlaveDeviceDefinitions) => (
|
|
|
153
153
|
} | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, ModbusInvalidArgumentError | ModbusNotConnectedError, never>;
|
|
154
154
|
withBatchingClient: (unitId: number, clientOptions?: BatchingClientOptions & {
|
|
155
155
|
readonly retry?: ModbusRetryPolicy;
|
|
156
|
-
}) => Effect.Effect<import("./batching-client").BatchingModbusClient, ModbusError, never>;
|
|
156
|
+
}) => Effect.Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusUnitAlreadyDeclaredError | ModbusError, never>;
|
|
157
157
|
batchingClient: (unitId: number) => Effect.Effect<import("./batching-client").BatchingModbusClient, ModbusError, never>;
|
|
158
158
|
readonly touchedUnits: Set<number>;
|
|
159
159
|
setRequestTimeout: (_timeoutMs: number) => Effect.Effect<void, ModbusNotConnectedError, never>;
|
|
160
160
|
clearRequestTimeout: () => Effect.Effect<void, ModbusNotConnectedError, never>;
|
|
161
161
|
reconnect: () => Effect.Effect<void, ModbusError, never>;
|
|
162
|
-
close: () => Effect.Effect<void, never,
|
|
162
|
+
close: (scope: Scope.Closeable) => Effect.Effect<void, never, never>;
|
|
163
163
|
hasPendingRequests: () => false;
|
|
164
164
|
}, never, Scope.Scope>;
|
|
@@ -3,7 +3,7 @@ import type { AsyncAsciiTransport, AsyncRtuTransport, AsyncTcpTransport } from '
|
|
|
3
3
|
import type { WasmAsciiTransport, WasmRtuTransport, WasmWsTransport } from 'modbus-rs/web';
|
|
4
4
|
import { type BatchingClientOptions, type BatchingModbusClient } from './batching-client';
|
|
5
5
|
import { ConnectionState, type ReconnectOptions } from './connection';
|
|
6
|
-
import { ModbusNotConnectedError, type ModbusError } from './errors';
|
|
6
|
+
import { ModbusNotConnectedError, type ModbusError, type ModbusUnitAlreadyDeclaredError } from './errors';
|
|
7
7
|
import { type AnyModbusClient, type EffectModbusClient } from './modbus-client';
|
|
8
8
|
import type { ModbusRetryPolicy } from './retry';
|
|
9
9
|
/**
|
|
@@ -132,8 +132,13 @@ export interface TransportServiceApi {
|
|
|
132
132
|
* while the reconnect was running.
|
|
133
133
|
*/
|
|
134
134
|
reconnect(): Effect.Effect<void, ModbusError>;
|
|
135
|
-
/**
|
|
136
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Closes `scope`, then closes the transport.
|
|
137
|
+
*
|
|
138
|
+
* Pass the scope that holds the work to finish first, such as
|
|
139
|
+
* `onShutdown` writes. The transport closes even if a finalizer fails.
|
|
140
|
+
*/
|
|
141
|
+
close(scope: Scope.Closeable): Effect.Effect<void, ModbusError>;
|
|
137
142
|
/**
|
|
138
143
|
* Declares the {@link BatchingModbusClient} for a unit.
|
|
139
144
|
*
|
|
@@ -145,8 +150,12 @@ export interface TransportServiceApi {
|
|
|
145
150
|
* A unit is declared once, because every option here is a fact about the unit
|
|
146
151
|
* rather than about a caller: one unit holds one batch, one belief about the
|
|
147
152
|
* device, and one window. Declaring a unit twice fails with
|
|
148
|
-
* `
|
|
149
|
-
* another part of the program declared.
|
|
153
|
+
* `ModbusUnitAlreadyDeclaredError`. Use {@link batchingClient} to reach a
|
|
154
|
+
* client another part of the program declared.
|
|
155
|
+
*
|
|
156
|
+
* The declaration lasts as long as the transport. A new owner that starts over
|
|
157
|
+
* a transport that stays open finds the unit declared. It can catch
|
|
158
|
+
* `ModbusUnitAlreadyDeclaredError` and use {@link batchingClient} instead.
|
|
150
159
|
*
|
|
151
160
|
* Nothing is debounced unless `debounce` asks for it, matching the rest of
|
|
152
161
|
* this package: default timing stays predictable.
|
|
@@ -156,7 +165,7 @@ export interface TransportServiceApi {
|
|
|
156
165
|
*/
|
|
157
166
|
withBatchingClient(unitId: number, options?: BatchingClientOptions & {
|
|
158
167
|
readonly retry?: ModbusRetryPolicy;
|
|
159
|
-
}): Effect.Effect<BatchingModbusClient, ModbusError>;
|
|
168
|
+
}): Effect.Effect<BatchingModbusClient, ModbusError | ModbusUnitAlreadyDeclaredError>;
|
|
160
169
|
/**
|
|
161
170
|
* The {@link BatchingModbusClient} declared for a unit.
|
|
162
171
|
*
|
|
@@ -232,10 +241,10 @@ export declare function createTransportScoped<TOptions, TClient extends AnyModbu
|
|
|
232
241
|
setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, ModbusNotConnectedError, never>;
|
|
233
242
|
clearRequestTimeout: () => Effect.Effect<undefined, ModbusNotConnectedError, never>;
|
|
234
243
|
reconnect: () => Effect.Effect<undefined, ModbusError, never>;
|
|
235
|
-
close: () => Effect.Effect<void, ModbusError,
|
|
244
|
+
close: (scope: Scope.Closeable) => Effect.Effect<void, ModbusError, never>;
|
|
236
245
|
withBatchingClient: (unitId: number, options?: BatchingClientOptions & {
|
|
237
246
|
readonly retry?: ModbusRetryPolicy;
|
|
238
|
-
}) => Effect.Effect<BatchingModbusClient, ModbusError>;
|
|
247
|
+
}) => Effect.Effect<BatchingModbusClient, ModbusError | ModbusUnitAlreadyDeclaredError>;
|
|
239
248
|
batchingClient: (unitId: number) => Effect.Effect<BatchingModbusClient, ModbusError>;
|
|
240
249
|
readonly touchedUnits: Set<number>;
|
|
241
250
|
hasPendingRequests: () => boolean;
|
|
@@ -131,4 +131,17 @@ export interface WriteDebouncer {
|
|
|
131
131
|
* debouncer.write({ address: 2001, value: 20 }),
|
|
132
132
|
* ], { concurrency: 'unbounded' });
|
|
133
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Resolves the window and the hold limit that a write debouncer applies.
|
|
136
|
+
*
|
|
137
|
+
* The batching client reports these values, so both use this one rule.
|
|
138
|
+
*
|
|
139
|
+
* @param window - The write window.
|
|
140
|
+
* @param maxHold - The hold limit. When omitted, it is four times the window.
|
|
141
|
+
* @returns The window and the hold limit in milliseconds.
|
|
142
|
+
*/
|
|
143
|
+
export declare const resolveWriteHold: (window: Duration.Input, maxHold: Duration.Input | undefined) => {
|
|
144
|
+
windowMs: number;
|
|
145
|
+
maxHoldMs: number;
|
|
146
|
+
};
|
|
134
147
|
export declare const createWriteDebouncer: (options: WriteDebouncerOptions) => Effect.Effect<WriteDebouncer, never, Scope.Scope>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flux-control/effect-modbus-rs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Type-safe Modbus communication via Effect-TS, wrapping the modbus-rs npm bindings.",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@changesets/cli": "^2.31.1",
|
|
46
46
|
"@effect/language-service": "0.87.2",
|
|
47
|
-
"@effect/platform-bun": "^4.0.0-rc.
|
|
47
|
+
"@effect/platform-bun": "^4.0.0-rc.118",
|
|
48
48
|
"@oxlint/plugins": "1.76.0",
|
|
49
49
|
"@types/bun": "latest",
|
|
50
|
-
"effect": "
|
|
50
|
+
"effect": "4.0.0-rc.118",
|
|
51
51
|
"fallow": "^2.104.0",
|
|
52
52
|
"husky": "^9.1.7",
|
|
53
|
-
"oxfmt": "^0.
|
|
53
|
+
"oxfmt": "^0.71.0",
|
|
54
54
|
"oxlint": "1.76.0",
|
|
55
55
|
"typescript": "^7.0.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"effect": "^4.0.0-rc.
|
|
58
|
+
"effect": "^4.0.0-rc.118"
|
|
59
59
|
}
|
|
60
60
|
}
|