@flux-control/effect-modbus-rs 0.3.0 → 0.4.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  For the complete API reference, see the [GitHub Pages documentation](https://flux-control-solutions.github.io/Effect-modbus-rs/).
6
6
 
7
- Provides scoped [`Effect.Service`](https://effect.website) constructors for RTU (serial), TCP, and ASCII Modbus transports. Clients expose a typed `Effect`-based API for all standard Modbus function codes.
7
+ Provides scoped [`Context.Service`](https://effect.website) constructors for RTU (serial), TCP, and ASCII Modbus transports. Clients expose a typed `Effect`-based API for all standard Modbus function codes.
8
8
 
9
9
  > This project is under active development. Its API may change before the 1.0 release.
10
10
 
@@ -44,7 +44,7 @@ program.pipe(
44
44
  ModbusInvalidArgumentError: (err) => Console.log(`Invalid argument: ${err.message}`),
45
45
  }),
46
46
  Effect.catchAll((err) => Console.log(`Unhandled error: ${err.message}`)),
47
- Effect.provide(RtuTransportService.Default({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
47
+ Effect.provide(RtuTransportService.make({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
48
48
  Effect.scoped,
49
49
  Effect.runPromise,
50
50
  );
@@ -64,7 +64,7 @@ const program = Effect.gen(function* () {
64
64
  });
65
65
 
66
66
  program.pipe(
67
- Effect.provide(TcpTransportService.Default({ host: '192.168.1.100', port: 502 })),
67
+ Effect.provide(TcpTransportService.make({ host: '192.168.1.100', port: 502 })),
68
68
  Effect.scoped,
69
69
  Effect.runPromise,
70
70
  );
@@ -87,7 +87,7 @@ const program = Effect.gen(function* () {
87
87
  });
88
88
 
89
89
  program.pipe(
90
- Effect.provide(AsciiTransportService.Default({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
90
+ Effect.provide(AsciiTransportService.make({ portPath: '/dev/ttyUSB0', baudRate: 9600 })),
91
91
  Effect.scoped,
92
92
  Effect.runPromise,
93
93
  );
@@ -112,7 +112,7 @@ const program = Effect.gen(function* () {
112
112
  });
113
113
 
114
114
  program.pipe(
115
- Effect.provide(WasmWsTransportService.Default({ wsUrl: 'ws://localhost:8080' })),
115
+ Effect.provide(WasmWsTransportService.make({ wsUrl: 'ws://localhost:8080' })),
116
116
  Effect.scoped,
117
117
  Effect.runPromise,
118
118
  );
@@ -129,7 +129,7 @@ connectButton.addEventListener('click', () => {
129
129
  Effect.gen(function* () {
130
130
  const port = yield* requestSerialPort();
131
131
  yield* program.pipe(
132
- Effect.provide(WasmRtuTransportService.Default({ port, baudRate: 19200 })),
132
+ Effect.provide(WasmRtuTransportService.make({ port, baudRate: 19200 })),
133
133
  Effect.scoped,
134
134
  );
135
135
  }),
@@ -150,7 +150,7 @@ Not demonstrated in `examples/wasm/` (see that app's README) — the same `impor
150
150
 
151
151
  ## Transports
152
152
 
153
- Each transport is a scoped `Effect.Service`. You provide it with `Effect.provide`, and the connection is opened on service access and closed when the scope ends.
153
+ Each transport is a scoped `Context.Service`. You provide it with `Effect.provide`, and the connection is opened on service access and closed when the scope ends.
154
154
 
155
155
  | Service | Options | Connection |
156
156
  | ------------------------------------- | ------------------------------ | ----------------------------- |
@@ -193,7 +193,32 @@ program.pipe(
193
193
  // );
194
194
  ```
195
195
 
196
- It also supports `makeMockTransport` for testing:
196
+ `WasmSerialTransportService` is the browser equivalent. Provide it with `fromRtu` or `fromAscii`, and give it a port handle from `requestSerialPort`.
197
+
198
+ Both abstract tags also have `makeMockTransport` for tests. The option set is the same as the option set of the concrete tags. Thus a test that keeps the framing abstract can also set `retry`, `reconnect`, and the mock fault hooks:
199
+
200
+ ```ts
201
+ import {
202
+ ModbusTimeoutError,
203
+ RetryPolicies,
204
+ SerialTransportService,
205
+ } from '@flux-control/effect-modbus-rs';
206
+
207
+ let attempts = 0;
208
+
209
+ const layer = SerialTransportService.makeMockTransport([device])({
210
+ portPath: '/dev/ttyUSB0',
211
+ baudRate: 9600,
212
+ retry: RetryPolicies.serial(),
213
+ // The first two attempts of each operation fail. The policy retries them.
214
+ fault: () =>
215
+ attempts++ < 2
216
+ ? new ModbusTimeoutError({ message: 'no response', cause: new Error('timeout') })
217
+ : undefined,
218
+ });
219
+ ```
220
+
221
+ See [Testing with mocks](#testing-with-mocks) for the `fault` hook and the `reconnectFault` hook.
197
222
 
198
223
  ## Client API
199
224
 
@@ -254,7 +279,7 @@ Handle with `Effect.catchTags`. The `ModbusError` union type covers all seven va
254
279
  Resilience belongs to the **transport**, not to call sites. Attach a policy where the transport is created and every client derived from it carries it:
255
280
 
256
281
  ```ts
257
- const layer = TcpTransportService.Default({
282
+ const layer = TcpTransportService.make({
258
283
  host: '192.168.1.50',
259
284
  port: 502,
260
285
  retry: RetryPolicies.tcp(), // applied to every operation
@@ -368,7 +393,7 @@ So `RetryPolicies.tcp()` waits roughly 80–120 ms before its first retry, not e
368
393
  Passing `reconnect` hands reconnection to a supervisor fiber owned by the transport — **one reconnect for the whole application**, however many fibers were in flight when the link dropped:
369
394
 
370
395
  ```ts
371
- TcpTransportService.Default({
396
+ TcpTransportService.make({
372
397
  host,
373
398
  port,
374
399
  reconnect: {
@@ -420,7 +445,7 @@ See `examples/retry-policies.ts` for a runnable walkthrough.
420
445
  `modbus-rs` exposes `retryAttempts`, `retryDelayMs`, and `retryBackoffStrategy` on its transport options. This package removes all three from every transport constructor, so setting one is a compile error rather than a documented hazard:
421
446
 
422
447
  ```ts
423
- TcpTransportService.Default({ host, port, retryAttempts: 3 });
448
+ TcpTransportService.make({ host, port, retryAttempts: 3 });
424
449
  // ^^^^^^^^^^^^^ Object literal may only specify
425
450
  // known properties, and 'retryAttempts' does not
426
451
  // exist in type 'TcpTransportOpenOptions & …'
@@ -475,10 +500,41 @@ const mockLayer = RtuTransportService.makeMockTransport([device])({
475
500
  program.pipe(Effect.provide(mockLayer), Effect.scoped, Effect.runPromise);
476
501
  ```
477
502
 
478
- The mock factory is identical for all three transports; swap `RtuTransportService` for `TcpTransportService` or `AsciiTransportService` and adjust the options shape accordingly each exposes a static `makeMockTransport` method.
503
+ The mock factory is the same for every transport. Each tag has a static `makeMockTransport` method, and each accepts the same options: the open options of that transport, the resilience options (`retry` and `reconnect`), and the two fault hooks below. To change transport, use a different tag and adjust the shape of the open options.
479
504
 
480
505
  See `examples/rtu-mock.ts`, `examples/tcp-mock.ts`, and `examples/ascii-mock.ts` for full walkthroughs covering read, write, multi-device access, and error-case testing.
481
506
 
507
+ ### Fault injection
508
+
509
+ Two mock-only hooks make a policy testable without hardware:
510
+
511
+ | Hook | When it runs | Return value |
512
+ | ---------------- | ------------------------------ | ---------------------------------------------------------------------------- |
513
+ | `fault` | Before every operation attempt | A `ModbusError` fails that attempt. `undefined` lets it through. |
514
+ | `reconnectFault` | Before every reconnect attempt | A `ModbusError` keeps the link down. `undefined` lets the reconnect succeed. |
515
+
516
+ Because `fault` runs before each _attempt_, an error from it is the same as a device that refused that attempt. A retry policy, the backoff, and the circuit breaker therefore behave as they do on a real bus:
517
+
518
+ ```ts
519
+ import {
520
+ ModbusTimeoutError,
521
+ RetryPolicies,
522
+ RtuTransportService,
523
+ } from '@flux-control/effect-modbus-rs';
524
+
525
+ let attempts = 0;
526
+
527
+ const mockLayer = RtuTransportService.makeMockTransport([device])({
528
+ portPath: '/dev/ttyUSB0',
529
+ baudRate: 9600,
530
+ retry: RetryPolicies.serial(),
531
+ fault: () =>
532
+ attempts++ < 2
533
+ ? new ModbusTimeoutError({ message: 'no response', cause: new Error('timeout') })
534
+ : undefined,
535
+ });
536
+ ```
537
+
482
538
  ### Slave device schema
483
539
 
484
540
  | Property | Type | Default |
@@ -512,17 +568,17 @@ src/
512
568
  connection.ts — Connection state machine, reconnect supervisor, circuit breaker
513
569
  retry.ts — Opt-in retry policies (backoff, jitter, per-error rules)
514
570
  shared-transport.ts — Generic scoped transport lifecycle management, WithoutUpstreamRetry
515
- RtuTransportService.ts — Scoped Effect.Service wrapping AsyncRtuTransport
516
- TcpTransportService.ts — Scoped Effect.Service wrapping AsyncTcpTransport
517
- AsciiTransportService.ts — Scoped Effect.Service wrapping AsyncAsciiTransport
571
+ RtuTransportService.ts — Scoped Context.Service wrapping AsyncRtuTransport
572
+ TcpTransportService.ts — Scoped Context.Service wrapping AsyncTcpTransport
573
+ AsciiTransportService.ts — Scoped Context.Service wrapping AsyncAsciiTransport
518
574
  SerialTransportService.ts — Abstract serial transport (RTU/ASCII) tag
519
575
  TcpModbusServerService.ts — tcpServerLayer
520
576
  SerialModbusServerService.ts — serialRtuServerLayer / serialAsciiServerLayer
521
577
  TcpGatewayService.ts — tcpGatewayLayer
522
578
  WasmSerialPort.ts — requestSerialPort() Effect helper (browser, user-gesture gated)
523
- WasmWsTransportService.ts — Scoped Effect.Service wrapping WasmWsTransport (browser, WS gateway)
524
- WasmRtuTransportService.ts — Scoped Effect.Service wrapping WasmRtuTransport (browser, Web Serial RTU)
525
- WasmAsciiTransportService.ts — Scoped Effect.Service wrapping WasmAsciiTransport (browser, Web Serial ASCII)
579
+ WasmWsTransportService.ts — Scoped Context.Service wrapping WasmWsTransport (browser, WS gateway)
580
+ WasmRtuTransportService.ts — Scoped Context.Service wrapping WasmRtuTransport (browser, Web Serial RTU)
581
+ WasmAsciiTransportService.ts — Scoped Context.Service wrapping WasmAsciiTransport (browser, Web Serial ASCII)
526
582
  WasmSerialTransportService.ts — Abstract browser serial transport (RTU/ASCII) tag
527
583
  WasmTcpServerService.ts — wasmWsServerLayer (experimental)
528
584
  WasmSerialModbusServerService.ts — wasmSerialRtuServerLayer / wasmSerialAsciiServerLayer (experimental)
package/dist/index.d.ts CHANGED
@@ -54,7 +54,7 @@
54
54
  * every client derived from it:
55
55
  *
56
56
  * ```ts
57
- * TcpTransportService.Default({
57
+ * TcpTransportService.make({
58
58
  * host, port,
59
59
  * retry: RetryPolicies.tcp(), // applied to every operation
60
60
  * reconnect: {}, // supervised reconnect + circuit breaker