@flux-control/effect-modbus-rs 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +317 -20
  2. package/dist/index.d.ts +62 -1
  3. package/dist/index.js +1034 -213
  4. package/dist/src/AsciiTransportService.d.ts +14 -10
  5. package/dist/src/RtuTransportService.d.ts +14 -10
  6. package/dist/src/TcpTransportService.d.ts +14 -10
  7. package/dist/src/WasmAsciiTransportService.d.ts +15 -11
  8. package/dist/src/WasmRtuTransportService.d.ts +15 -11
  9. package/dist/src/WasmWsTransportService.d.ts +14 -10
  10. package/dist/src/batching-client.d.ts +197 -0
  11. package/dist/src/connection.d.ts +6 -3
  12. package/dist/src/mocks.d.ts +14 -8
  13. package/dist/src/modbus-client.d.ts +3 -1
  14. package/dist/src/read-debouncer.d.ts +100 -0
  15. package/dist/src/register-cache.d.ts +90 -0
  16. package/dist/src/register-plan.d.ts +212 -0
  17. package/dist/src/retry.d.ts +4 -4
  18. package/dist/src/shared-transport.d.ts +60 -4
  19. package/dist/src/span-attributes.d.ts +34 -0
  20. package/dist/src/write-debouncer.d.ts +134 -0
  21. package/dist/test/retry.test.d.ts +1 -0
  22. package/dist/test/shared-transport.test.d.ts +1 -0
  23. package/dist/test/span-attributes.test.d.ts +1 -0
  24. package/dist/test/upstream-options.test.d.ts +1 -0
  25. package/dist/test/wasm-mocks.test.d.ts +1 -0
  26. package/dist/test/write-debouncer.test.d.ts +1 -0
  27. package/package.json +5 -4
  28. /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
  29. /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
  30. /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
  31. /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
  32. /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
  33. /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
  34. /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
  35. /package/dist/{src/wasm-mocks.test.d.ts → test/resilience.test.d.ts} +0 -0
@@ -1,7 +1,6 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { AsciiTransportOptions } from 'modbus-rs';
3
- import { SlaveDeviceDefinitions } from './mocks';
4
- import type { MockFaultOptions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, WithoutUpstreamRetry, TransportServiceApi } from './shared-transport';
6
5
  /**
7
6
  * {@link AsciiTransportOptions} minus the upstream retry knobs.
@@ -24,14 +23,14 @@ declare const AsciiTransportService_base: Context.ServiceClass<AsciiTransportSer
24
23
  *
25
24
  * @see AsyncAsciiTransport — Upstream `modbus-rs` ASCII transport.
26
25
  * @see AsciiTransportOpenOptions — Configuration for the ASCII serial port.
27
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
26
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
28
27
  */
29
28
  export declare class AsciiTransportService extends AsciiTransportService_base {
30
29
  /**
31
30
  * Scoped constructor effect for the service. v4 does not auto-generate a
32
31
  * layer from this, so {@link AsciiTransportService.make} builds one explicitly.
33
32
  */
34
- static readonly makeScoped: (options: AsciiTransportOpenOptions & TransportResilienceOptions) => Effect.Effect<{
33
+ static readonly makeScoped: (options: AsciiTransportOpenOptions & TransportResilienceOptions) => import("effect/Effect").Effect<{
35
34
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
36
35
  readonly _tag: "Connected";
37
36
  } | {
@@ -45,11 +44,16 @@ export declare class AsciiTransportService extends AsciiTransportService_base {
45
44
  }>;
46
45
  withClient: (unitId: number, clientOptions?: {
47
46
  readonly retry?: import("./retry").ModbusRetryPolicy;
48
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
49
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
51
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
52
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
47
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
48
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
49
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
51
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
52
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
53
+ readonly retry?: import("./retry").ModbusRetryPolicy;
54
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
55
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
56
+ readonly touchedUnits: Set<number>;
53
57
  hasPendingRequests: () => boolean;
54
58
  }, never, import("effect/Scope").Scope>;
55
59
  /**
@@ -1,7 +1,6 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { RtuTransportOptions } from 'modbus-rs';
3
- import type { MockFaultOptions } from './mocks';
4
- import type { SlaveDeviceDefinitions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, WithoutUpstreamRetry, TransportServiceApi } from './shared-transport';
6
5
  /**
7
6
  * {@link RtuTransportOptions} minus the upstream retry knobs.
@@ -24,14 +23,14 @@ declare const RtuTransportService_base: Context.ServiceClass<RtuTransportService
24
23
  *
25
24
  * @see AsyncRtuTransport — Upstream `modbus-rs` RTU transport.
26
25
  * @see RtuTransportOpenOptions — Configuration for the RTU serial port.
27
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
26
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
28
27
  */
29
28
  export declare class RtuTransportService extends RtuTransportService_base {
30
29
  /**
31
30
  * Scoped constructor effect for the service. v4 does not auto-generate a
32
31
  * layer from this, so {@link RtuTransportService.make} builds one explicitly.
33
32
  */
34
- static readonly makeScoped: (options: RtuTransportOpenOptions & TransportResilienceOptions) => Effect.Effect<{
33
+ static readonly makeScoped: (options: RtuTransportOpenOptions & TransportResilienceOptions) => import("effect/Effect").Effect<{
35
34
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
36
35
  readonly _tag: "Connected";
37
36
  } | {
@@ -45,11 +44,16 @@ export declare class RtuTransportService extends RtuTransportService_base {
45
44
  }>;
46
45
  withClient: (unitId: number, clientOptions?: {
47
46
  readonly retry?: import("./retry").ModbusRetryPolicy;
48
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
49
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
51
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
52
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
47
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
48
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
49
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
51
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
52
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
53
+ readonly retry?: import("./retry").ModbusRetryPolicy;
54
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
55
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
56
+ readonly touchedUnits: Set<number>;
53
57
  hasPendingRequests: () => boolean;
54
58
  }, never, import("effect/Scope").Scope>;
55
59
  /**
@@ -1,7 +1,6 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { TcpTransportOptions } from 'modbus-rs';
3
- import { SlaveDeviceDefinitions } from './mocks';
4
- import type { MockFaultOptions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, TransportServiceApi, WithoutUpstreamRetry } from './shared-transport';
6
5
  /**
7
6
  * {@link TcpTransportOptions} minus the upstream retry knobs.
@@ -24,14 +23,14 @@ declare const TcpTransportService_base: Context.ServiceClass<TcpTransportService
24
23
  *
25
24
  * @see AsyncTcpTransport — Upstream `modbus-rs` TCP transport.
26
25
  * @see TcpTransportOpenOptions — Configuration for the TCP connection.
27
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
26
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
28
27
  */
29
28
  export declare class TcpTransportService extends TcpTransportService_base {
30
29
  /**
31
30
  * Scoped constructor effect for the service. v4 does not auto-generate a
32
31
  * layer from this, so {@link TcpTransportService.make} builds one explicitly.
33
32
  */
34
- static readonly makeScoped: (options: TcpTransportOpenOptions & TransportResilienceOptions) => Effect.Effect<{
33
+ static readonly makeScoped: (options: TcpTransportOpenOptions & TransportResilienceOptions) => import("effect/Effect").Effect<{
35
34
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
36
35
  readonly _tag: "Connected";
37
36
  } | {
@@ -45,11 +44,16 @@ export declare class TcpTransportService extends TcpTransportService_base {
45
44
  }>;
46
45
  withClient: (unitId: number, clientOptions?: {
47
46
  readonly retry?: import("./retry").ModbusRetryPolicy;
48
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
49
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
51
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
52
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
47
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
48
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
49
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
50
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
51
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
52
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
53
+ readonly retry?: import("./retry").ModbusRetryPolicy;
54
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
55
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
56
+ readonly touchedUnits: Set<number>;
53
57
  hasPendingRequests: () => boolean;
54
58
  }, never, import("effect/Scope").Scope>;
55
59
  /**
@@ -1,12 +1,11 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { WasmSerialPortHandle, WasmSerialTransportOptions } from 'modbus-rs/web';
3
- import { SlaveDeviceDefinitions } from './mocks';
4
- import type { MockFaultOptions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, TransportServiceApi } from './shared-transport';
6
5
  /**
7
6
  * Options for {@link WasmAsciiTransportService}. `WasmAsciiTransport.open()` takes the
8
7
  * serial port handle and the connection options as two separate arguments; this
9
- * combines them into one object so it fits {@link makeTransportScoped}'s single-options
8
+ * combines them into one object so it fits {@link createTransportScoped}'s single-options
10
9
  * shape, with `port` destructured back out inside the service's `openMethod`.
11
10
  *
12
11
  * @see requestSerialPort — Obtains the `port` handle (must be called from a user gesture).
@@ -28,7 +27,7 @@ declare const WasmAsciiTransportService_base: Context.ServiceClass<WasmAsciiTran
28
27
  *
29
28
  * @see WasmAsciiTransport — Upstream `modbus-rs` browser Web Serial ASCII transport.
30
29
  * @see requestSerialPort — Obtains the serial port handle this service's `port` option needs.
31
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
30
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
32
31
  */
33
32
  export declare class WasmAsciiTransportService extends WasmAsciiTransportService_base {
34
33
  /**
@@ -37,7 +36,7 @@ export declare class WasmAsciiTransportService extends WasmAsciiTransportService
37
36
  */
38
37
  static readonly makeScoped: (options: WasmSerialTransportOptions & {
39
38
  port: WasmSerialPortHandle;
40
- } & TransportResilienceOptions) => Effect.Effect<{
39
+ } & TransportResilienceOptions) => import("effect/Effect").Effect<{
41
40
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
42
41
  readonly _tag: "Connected";
43
42
  } | {
@@ -51,11 +50,16 @@ export declare class WasmAsciiTransportService extends WasmAsciiTransportService
51
50
  }>;
52
51
  withClient: (unitId: number, clientOptions?: {
53
52
  readonly retry?: import("./retry").ModbusRetryPolicy;
54
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
55
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
56
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
57
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
58
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
53
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
54
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
55
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
56
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
57
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
58
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
59
+ readonly retry?: import("./retry").ModbusRetryPolicy;
60
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
61
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
62
+ readonly touchedUnits: Set<number>;
59
63
  hasPendingRequests: () => boolean;
60
64
  }, never, import("effect/Scope").Scope>;
61
65
  /**
@@ -1,12 +1,11 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { WasmSerialPortHandle, WasmSerialTransportOptions } from 'modbus-rs/web';
3
- import { SlaveDeviceDefinitions } from './mocks';
4
- import type { MockFaultOptions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, TransportServiceApi } from './shared-transport';
6
5
  /**
7
6
  * Options for {@link WasmRtuTransportService}. `WasmRtuTransport.open()` takes the
8
7
  * serial port handle and the connection options as two separate arguments; this
9
- * combines them into one object so it fits {@link makeTransportScoped}'s single-options
8
+ * combines them into one object so it fits {@link createTransportScoped}'s single-options
10
9
  * shape, with `port` destructured back out inside the service's `openMethod`.
11
10
  *
12
11
  * @see requestSerialPort — Obtains the `port` handle (must be called from a user gesture).
@@ -28,7 +27,7 @@ declare const WasmRtuTransportService_base: Context.ServiceClass<WasmRtuTranspor
28
27
  *
29
28
  * @see WasmRtuTransport — Upstream `modbus-rs` browser Web Serial RTU transport.
30
29
  * @see requestSerialPort — Obtains the serial port handle this service's `port` option needs.
31
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
30
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
32
31
  */
33
32
  export declare class WasmRtuTransportService extends WasmRtuTransportService_base {
34
33
  /**
@@ -37,7 +36,7 @@ export declare class WasmRtuTransportService extends WasmRtuTransportService_bas
37
36
  */
38
37
  static readonly makeScoped: (options: WasmSerialTransportOptions & {
39
38
  port: WasmSerialPortHandle;
40
- } & TransportResilienceOptions) => Effect.Effect<{
39
+ } & TransportResilienceOptions) => import("effect/Effect").Effect<{
41
40
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
42
41
  readonly _tag: "Connected";
43
42
  } | {
@@ -51,11 +50,16 @@ export declare class WasmRtuTransportService extends WasmRtuTransportService_bas
51
50
  }>;
52
51
  withClient: (unitId: number, clientOptions?: {
53
52
  readonly retry?: import("./retry").ModbusRetryPolicy;
54
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
55
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
56
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
57
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
58
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
53
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
54
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
55
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
56
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
57
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
58
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
59
+ readonly retry?: import("./retry").ModbusRetryPolicy;
60
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
61
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
62
+ readonly touchedUnits: Set<number>;
59
63
  hasPendingRequests: () => boolean;
60
64
  }, never, import("effect/Scope").Scope>;
61
65
  /**
@@ -1,7 +1,6 @@
1
- import { Context, Effect, Layer } from 'effect';
1
+ import { Context, Layer } from 'effect';
2
2
  import type { WasmWsTransportOptions } from 'modbus-rs/web';
3
- import { SlaveDeviceDefinitions } from './mocks';
4
- import type { MockFaultOptions } from './mocks';
3
+ import { type MockFaultOptions, type SlaveDeviceDefinitions } from './mocks';
5
4
  import type { TransportResilienceOptions, TransportServiceApi } from './shared-transport';
6
5
  declare const WasmWsTransportService_base: Context.ServiceClass<WasmWsTransportService, "WasmWsTransportService", TransportServiceApi>;
7
6
  /**
@@ -17,14 +16,14 @@ declare const WasmWsTransportService_base: Context.ServiceClass<WasmWsTransportS
17
16
  *
18
17
  * @see WasmWsTransport — Upstream `modbus-rs` browser WebSocket transport.
19
18
  * @see WasmWsTransportOptions — Configuration for the WebSocket gateway connection.
20
- * @see makeTransportScoped — Generic lifecycle logic from shared-transport.
19
+ * @see createTransportScoped — Generic lifecycle logic from shared-transport.
21
20
  */
22
21
  export declare class WasmWsTransportService extends WasmWsTransportService_base {
23
22
  /**
24
23
  * Scoped constructor effect for the service. v4 does not auto-generate a
25
24
  * layer from this, so {@link WasmWsTransportService.make} builds one explicitly.
26
25
  */
27
- static readonly makeScoped: (options: WasmWsTransportOptions & TransportResilienceOptions) => Effect.Effect<{
26
+ static readonly makeScoped: (options: WasmWsTransportOptions & TransportResilienceOptions) => import("effect/Effect").Effect<{
28
27
  connectionState: import("effect/SubscriptionRef").SubscriptionRef<{
29
28
  readonly _tag: "Connected";
30
29
  } | {
@@ -38,11 +37,16 @@ export declare class WasmWsTransportService extends WasmWsTransportService_base
38
37
  }>;
39
38
  withClient: (unitId: number, clientOptions?: {
40
39
  readonly retry?: import("./retry").ModbusRetryPolicy;
41
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
42
- setRequestTimeout: (timeoutMs: number) => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
43
- clearRequestTimeout: () => Effect.Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
44
- reconnect: () => Effect.Effect<undefined, import("./errors").ModbusError, never>;
45
- close: () => Effect.Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
40
+ } | undefined) => import("effect/Effect").Effect<import("./modbus-client").EffectModbusClient, import("./errors").ModbusError, never>;
41
+ setRequestTimeout: (timeoutMs: number) => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
42
+ clearRequestTimeout: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusNotConnectedError, never>;
43
+ reconnect: () => import("effect/Effect").Effect<undefined, import("./errors").ModbusError, never>;
44
+ close: () => import("effect/Effect").Effect<void, import("./errors").ModbusError, import("effect/Scope").Scope>;
45
+ withBatchingClient: (unitId: number, options?: import("./batching-client").BatchingClientOptions & {
46
+ readonly retry?: import("./retry").ModbusRetryPolicy;
47
+ }) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
48
+ batchingClient: (unitId: number) => import("effect/Effect").Effect<import("./batching-client").BatchingModbusClient, import("./errors").ModbusError>;
49
+ readonly touchedUnits: Set<number>;
46
50
  hasPendingRequests: () => boolean;
47
51
  }, never, import("effect/Scope").Scope>;
48
52
  /**
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @fileoverview A client that decides the transactions, rather than issuing the
3
+ * ones a caller names.
4
+ *
5
+ * `withClient` returns an {@link EffectModbusClient}: it issues exactly the
6
+ * transaction asked for, and it is the right client when a caller knows what the
7
+ * bus should carry. This module is its sibling for the other case — a caller with
8
+ * one accessor per register, which knows what it wants to read and write but not
9
+ * what that ought to cost.
10
+ *
11
+ * @module
12
+ */
13
+ import { Duration, Effect, Scope, SubscriptionRef } from 'effect';
14
+ import { ConnectionState } from './connection';
15
+ import { type ModbusError } from './errors';
16
+ import type { EffectModbusClient, ModbusOperations } from './modbus-client';
17
+ import { type RegisterCache } from './register-cache';
18
+ import { type PlanReadsOptions, type PlanWritesOptions, type RegisterWrite } from './register-plan';
19
+ import type { ModbusRetryPolicy } from './retry';
20
+ import { type ModbusSpanAttributes } from './span-attributes';
21
+ /** How long operations are collected before they reach the bus. */
22
+ export interface BatchingDebounceOptions {
23
+ /**
24
+ * The window for writes, and the ceiling on the total hold.
25
+ *
26
+ * Omitted, writes are not debounced: each one reaches the bus on its own. The
27
+ * package holds nothing back by default, so a caller that has not asked for a
28
+ * window pays no latency for one.
29
+ */
30
+ readonly writes?: {
31
+ readonly window: Duration.Input;
32
+ readonly maxHold?: Duration.Input;
33
+ };
34
+ /**
35
+ * The window for reads.
36
+ *
37
+ * Omitted, reads are not debounced. `readAll` still plans the addresses it is
38
+ * given, so a caller that reads a group in one call needs no window.
39
+ *
40
+ * A useful window is on the order of one transaction. Measure a transaction on
41
+ * the bus in question before choosing one.
42
+ */
43
+ readonly reads?: {
44
+ readonly window: Duration.Input;
45
+ };
46
+ }
47
+ /** Options for {@link makeBatchingClient}. */
48
+ export interface BatchingClientOptions {
49
+ /**
50
+ * The write cache, which drops a write whose value the device already holds.
51
+ *
52
+ * Pass `false` to write every value, whether or not it changes anything. Pass
53
+ * a cache to share one with something else. The transport supplies its own by
54
+ * default, which is the right one: the cache is a belief about a physical
55
+ * device, and two caches on one unit disagree.
56
+ */
57
+ readonly cache?: boolean | RegisterCache;
58
+ /** The windows. Omitted, nothing is debounced. */
59
+ readonly debounce?: BatchingDebounceOptions;
60
+ /** Limits handed to the planners. */
61
+ readonly plan?: {
62
+ readonly writes?: PlanWritesOptions;
63
+ readonly reads?: PlanReadsOptions;
64
+ };
65
+ }
66
+ /** Reads over one register space. */
67
+ export interface BatchingRegisterReader {
68
+ /** Collects an address for the read window, then returns its value. */
69
+ read(address: number): Effect.Effect<number, ModbusError>;
70
+ /** Adds an address to the batch and reads immediately, answering every reader in it. */
71
+ readNow(address: number): Effect.Effect<number, ModbusError>;
72
+ /** Reads a group as one reader, planned into spans, in the order asked for. */
73
+ readAll(addresses: ReadonlyArray<number>): Effect.Effect<ReadonlyArray<number>, ModbusError>;
74
+ /** Adds a group to the batch and reads immediately. */
75
+ readAllNow(addresses: ReadonlyArray<number>): Effect.Effect<ReadonlyArray<number>, ModbusError>;
76
+ }
77
+ /**
78
+ * A client that decides its own transactions.
79
+ *
80
+ * CAUTION: This is not an {@link EffectModbusClient} and does not extend
81
+ * {@link ModbusOperations}. There is no `writeSingleRegister` and no
82
+ * `readHoldingRegisters` here. A raw write on this object would go around the
83
+ * cache and around the batch, so a value held for an address could reach the
84
+ * device after a newer value written past it. A caller that needs the raw
85
+ * surface as well gets it from `withClient`, which shares the same connection.
86
+ * Once a batching client exists for a unit, that raw client remains available
87
+ * for reads, coils, and the other non-register-write operations, but FC06,
88
+ * FC16, and FC23 fail with `ModbusInvalidArgumentError`. Mixing the two register
89
+ * write paths would bypass the pending batch and its cache.
90
+ *
91
+ * Coils are not covered. The planners pack registers, so `readCoils`,
92
+ * `writeSingleCoil`, and their relatives stay on the raw client.
93
+ */
94
+ export interface BatchingModbusClient extends BatchingRegisterReader {
95
+ /** The unit this client addresses. */
96
+ readonly unitId: number;
97
+ /** Holds a write for the write window, then issues it with whatever else arrived. */
98
+ write(write: RegisterWrite, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
99
+ /** Adds a write to the batch and issues it immediately, newest value winning. */
100
+ writeNow(write: RegisterWrite, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
101
+ /** Holds a group of writes as one caller. The group succeeds or fails together. */
102
+ writeAll(writes: ReadonlyArray<RegisterWrite>, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
103
+ /** Adds a group of writes to the batch and issues it immediately. */
104
+ writeAllNow(writes: ReadonlyArray<RegisterWrite>, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
105
+ /** Reads over the input registers (FC04). The unqualified reads are holding registers (FC03). */
106
+ readonly inputs: BatchingRegisterReader;
107
+ /**
108
+ * Issues everything pending, now. Never fails: the outcome goes to the callers
109
+ * waiting on each batch.
110
+ */
111
+ readonly flush: Effect.Effect<void>;
112
+ /** The cache this client filters against, or `undefined` when `cache` is `false`. */
113
+ readonly cache: RegisterCache | undefined;
114
+ /**
115
+ * Runs an action when the calling scope closes, while the bus is still open
116
+ * and before this client is torn down.
117
+ *
118
+ * A device's safe state is the device's own answer — zero volts for one, a
119
+ * stopped motor for another — so it is stated here, on the client that
120
+ * addresses it, rather than dispatched from one action over a set of units.
121
+ *
122
+ * A failure is logged and then raised as a defect. The other finalizers in the
123
+ * scope still run, so a device that cannot be reached does not cost the rest of
124
+ * the bus its turn. Wrap the action in `Effect.ignoreLogged` to accept the
125
+ * failure instead.
126
+ *
127
+ * @example
128
+ * const damper = yield* transport.withBatchingClient(3);
129
+ * yield* damper.onShutdown(damper.writeAllNow([{ address: 2000, value: 0 }]));
130
+ */
131
+ onShutdown(action: Effect.Effect<void, ModbusError>): Effect.Effect<void, never, Scope.Scope>;
132
+ }
133
+ /**
134
+ * Builds a batching client over an Effect-wrapped client for one unit.
135
+ *
136
+ * The transport calls this from `withBatchingClient`, which is where a consumer
137
+ * normally meets it. Call it directly to drive a client this package did not
138
+ * hand out, such as another adapter implementing `EffectModbusClient`, or a stub.
139
+ *
140
+ * @param options - The unit, the Effect-wrapped client, and the policies.
141
+ * @returns The batching client, bound to the current scope.
142
+ */
143
+ export declare const makeBatchingClient: (options: {
144
+ readonly unitId: number;
145
+ readonly client: EffectModbusClient;
146
+ readonly cache: RegisterCache | undefined;
147
+ readonly debounce?: BatchingDebounceOptions;
148
+ readonly plan?: BatchingClientOptions['plan'];
149
+ }) => Effect.Effect<BatchingModbusClient, never, Scope.Scope>;
150
+ /** What a transport gives the registry so it can build batching clients. */
151
+ export interface BatchingRegistryDeps {
152
+ /** The transport's own raw client factory. */
153
+ withClient(unitId: number, options?: {
154
+ readonly retry?: ModbusRetryPolicy;
155
+ }): Effect.Effect<EffectModbusClient, ModbusError>;
156
+ /** The link state the shared cache watches. */
157
+ readonly connectionState: SubscriptionRef.SubscriptionRef<ConnectionState>;
158
+ /** The transport's own scope, which the clients and the watcher live in. */
159
+ readonly scope: Scope.Scope;
160
+ }
161
+ /** The batching half of a transport's API. */
162
+ export interface BatchingRegistry {
163
+ /**
164
+ * Declares the batching client for a unit. Fails if the unit already has one.
165
+ *
166
+ * The declaration belongs to the registry's transport scope, not to the
167
+ * calling fiber. Construction therefore continues if the caller is
168
+ * interrupted. A shorter-lived owner that abandons this effect must recover
169
+ * the pending or completed declaration with `batchingClient` rather than
170
+ * trying to declare the unit again.
171
+ */
172
+ withBatchingClient(unitId: number, options?: BatchingClientOptions & {
173
+ readonly retry?: ModbusRetryPolicy;
174
+ }): Effect.Effect<BatchingModbusClient, ModbusError>;
175
+ /** The batching client for a unit. Fails if nothing has declared one. */
176
+ batchingClient(unitId: number): Effect.Effect<BatchingModbusClient, ModbusError>;
177
+ /** Disables raw holding-register writes once a batching client exists for the unit. */
178
+ guardRawWrites(unitId: number, operations: ModbusOperations): ModbusOperations;
179
+ }
180
+ /**
181
+ * Builds the per-unit batching clients of one transport, and the cache they
182
+ * share.
183
+ *
184
+ * Declaring a client and reaching for one are separate operations, because every
185
+ * option a client takes is a fact about the unit rather than about a caller: two
186
+ * batching clients on one unit hold two batches and coalesce neither, two caches
187
+ * on one unit hold two beliefs about one device, and one batch has one window. A
188
+ * per-caller configuration could only ever be correct when every caller passed
189
+ * the same value, so it is declared once instead of compared at every call.
190
+ *
191
+ * `withBatchingClient` declares. `batchingClient` looks up, and waits for a
192
+ * declaration already under way rather than depending on which fiber ran first.
193
+ *
194
+ * @param deps - The transport's raw client factory, link state, and scope.
195
+ * @returns The batching methods, ready to spread onto a transport API.
196
+ */
197
+ export declare const createBatchingRegistry: (deps: BatchingRegistryDeps) => BatchingRegistry;
@@ -12,8 +12,10 @@ import { type ModbusErrorTag, type ModbusRetryPolicy } from './retry';
12
12
  * - `Connected` — usable.
13
13
  * - `Reconnecting` — the supervisor is re-establishing the link. Operations are
14
14
  * refused with {@link ModbusCircuitOpenError} rather than queued on a dead bus.
15
- * - `Down` — reconnect attempts were exhausted; the supervisor is waiting out
16
- * `resetAfter` before probing again. Operations are refused.
15
+ * - `Down` — a connection failure was observed. With a supervisor, operations
16
+ * are refused while it waits to probe again. Without one, the state remains
17
+ * observable but operations retain manual behavior until `reconnect()` or a
18
+ * successful operation restores `Connected`.
17
19
  */
18
20
  export type ConnectionState = Data.TaggedEnum<{
19
21
  Disconnected: object;
@@ -115,7 +117,8 @@ export declare const ConnectionState: {
115
117
  * Supplying this on a transport hands reconnection to a supervisor fiber owned
116
118
  * by that transport: one reconnect for the whole application rather than one
117
119
  * per failing call site. Omit it and the transport keeps its manual behaviour —
118
- * `reconnect()` still works, nothing happens on its own.
120
+ * failures still update `connectionState`, but `reconnect()` must be called by
121
+ * the application and no circuit breaker is enabled.
119
122
  */
120
123
  export interface ReconnectOptions {
121
124
  /**
@@ -1,7 +1,8 @@
1
- import { Effect, Schema, SubscriptionRef } from 'effect';
1
+ import { Effect, Schema, Scope, SubscriptionRef } from 'effect';
2
2
  import { type AsciiTransportOptions, type RtuTransportOptions, type TcpTransportOptions } from 'modbus-rs';
3
3
  import type { WasmWsTransportOptions, WasmSerialTransportOptions } from 'modbus-rs/web';
4
- import { ModbusInvalidArgumentError, type ModbusError } from './errors';
4
+ import { type BatchingClientOptions } from './batching-client';
5
+ import { ModbusInvalidArgumentError, ModbusNotConnectedError, type ModbusError } from './errors';
5
6
  import type { ModbusRetryPolicy } from './retry';
6
7
  import type { TransportResilienceOptions, WithoutUpstreamRetry } from './shared-transport';
7
8
  /**
@@ -135,7 +136,7 @@ export type SlaveDeviceDefinitions = Schema.Schema.Type<typeof SlaveDeviceDefini
135
136
  * @returns A transport factory function that returns a scoped Effect
136
137
  * providing the mock transport.
137
138
  */
138
- export declare const makeMockTransport: (devices: SlaveDeviceDefinitions) => (options: (WithoutUpstreamRetry<RtuTransportOptions> | WithoutUpstreamRetry<AsciiTransportOptions> | WithoutUpstreamRetry<TcpTransportOptions> | WasmWsTransportOptions | WasmSerialTransportOptions) & TransportResilienceOptions & MockFaultOptions) => Effect.Effect<{
139
+ export declare const createMockTransport: (devices: SlaveDeviceDefinitions) => (options: (WithoutUpstreamRetry<RtuTransportOptions> | WithoutUpstreamRetry<AsciiTransportOptions> | WithoutUpstreamRetry<TcpTransportOptions> | WasmWsTransportOptions | WasmSerialTransportOptions) & TransportResilienceOptions & MockFaultOptions) => Effect.Effect<{
139
140
  connectionState: SubscriptionRef.SubscriptionRef<{
140
141
  readonly _tag: "Connected";
141
142
  } | {
@@ -149,10 +150,15 @@ export declare const makeMockTransport: (devices: SlaveDeviceDefinitions) => (op
149
150
  }>;
150
151
  withClient: (unitId: number, clientOptions?: {
151
152
  readonly retry?: ModbusRetryPolicy;
152
- } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, ModbusInvalidArgumentError, never>;
153
- setRequestTimeout: (_timeoutMs: number) => Effect.Effect<void, never, never>;
154
- clearRequestTimeout: () => Effect.Effect<void, never, never>;
153
+ } | undefined) => Effect.Effect<import("./modbus-client").EffectModbusClient, ModbusInvalidArgumentError | ModbusNotConnectedError, never>;
154
+ withBatchingClient: (unitId: number, clientOptions?: BatchingClientOptions & {
155
+ readonly retry?: ModbusRetryPolicy;
156
+ }) => Effect.Effect<import("./batching-client").BatchingModbusClient, ModbusError, never>;
157
+ batchingClient: (unitId: number) => Effect.Effect<import("./batching-client").BatchingModbusClient, ModbusError, never>;
158
+ readonly touchedUnits: Set<number>;
159
+ setRequestTimeout: (_timeoutMs: number) => Effect.Effect<void, ModbusNotConnectedError, never>;
160
+ clearRequestTimeout: () => Effect.Effect<void, ModbusNotConnectedError, never>;
155
161
  reconnect: () => Effect.Effect<void, ModbusError, never>;
156
- close: () => Effect.Effect<void, ModbusError, never>;
162
+ close: () => Effect.Effect<void, never, Scope.Scope>;
157
163
  hasPendingRequests: () => false;
158
- }, never, import("effect/Scope").Scope>;
164
+ }, never, Scope.Scope>;
@@ -188,7 +188,7 @@ export interface ModbusOperations {
188
188
  * @see AsyncSerialModbusClient — Upstream native serial client API.
189
189
  * @see AsyncTcpModbusClient — Upstream native TCP client API.
190
190
  */
191
- export declare const makeEffectModbusClient: (client: AnyModbusClient) => ModbusOperations;
191
+ export declare const createEffectModbusClient: (client: AnyModbusClient) => ModbusOperations;
192
192
  /**
193
193
  * Transport-owned resilience applied to every operation of a client.
194
194
  *
@@ -199,6 +199,8 @@ export declare const makeEffectModbusClient: (client: AnyModbusClient) => Modbus
199
199
  export interface ClientResilience {
200
200
  /** Refuses the operation while the transport's circuit is open. */
201
201
  readonly guard: Effect.Effect<void, ModbusError>;
202
+ /** Records that an operation reached the device successfully. */
203
+ readonly onSuccess?: Effect.Effect<void>;
202
204
  /** Reports a failure so the transport can decide whether to reconnect. */
203
205
  readonly report: (error: ModbusError) => Effect.Effect<void>;
204
206
  /** Retry policy applied to each operation, if any. */