@flux-control/effect-modbus-rs 0.4.1 → 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.
Files changed (36) hide show
  1. package/README.md +343 -20
  2. package/dist/index.d.ts +62 -1
  3. package/dist/index.js +1046 -216
  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/errors.d.ts +3 -0
  13. package/dist/src/mocks.d.ts +14 -8
  14. package/dist/src/modbus-client.d.ts +3 -1
  15. package/dist/src/read-debouncer.d.ts +100 -0
  16. package/dist/src/register-cache.d.ts +90 -0
  17. package/dist/src/register-plan.d.ts +212 -0
  18. package/dist/src/retry.d.ts +4 -4
  19. package/dist/src/shared-transport.d.ts +79 -5
  20. package/dist/src/span-attributes.d.ts +34 -0
  21. package/dist/src/write-debouncer.d.ts +134 -0
  22. package/dist/test/retry.test.d.ts +1 -0
  23. package/dist/test/shared-transport.test.d.ts +1 -0
  24. package/dist/test/span-attributes.test.d.ts +1 -0
  25. package/dist/test/upstream-options.test.d.ts +1 -0
  26. package/dist/test/wasm-mocks.test.d.ts +1 -0
  27. package/dist/test/write-debouncer.test.d.ts +1 -0
  28. package/package.json +5 -4
  29. /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
  30. /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
  31. /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
  32. /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
  33. /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
  34. /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
  35. /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
  36. /package/dist/{src/wasm-mocks.test.d.ts → test/resilience.test.d.ts} +0 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @fileoverview Coalesces register writes that arrive separately into one batch.
3
+ *
4
+ * `planWrites` packs the writes a caller holds at one moment. A caller that
5
+ * derives each register independently — one fiber per output, each settling at
6
+ * its own pace — never holds two values at the same moment, so a planner alone
7
+ * never sees a run longer than one. Batching those would need a collection point
8
+ * upstream that the caller does not have.
9
+ *
10
+ * This module is that collection point, and it collects on time rather than on
11
+ * structure. A write is held for `window`, each new arrival restarts the window,
12
+ * and `maxHold` caps the total hold so a register that updates without a pause
13
+ * still reaches the wire.
14
+ *
15
+ * Callers still await their own write. Each caller gets its own `Deferred`,
16
+ * completed with the exit of the flush that carried its value, so a write that
17
+ * fails still fails for the caller that issued it. That keeps "the effect
18
+ * succeeded" meaning "the value reached the device", which is the invariant a
19
+ * caller uses to decide whether it may report an actuation.
20
+ *
21
+ * @module
22
+ */
23
+ import { Duration, Effect, type Scope } from 'effect';
24
+ import { type ModbusError } from './errors';
25
+ import { type RegisterWrite } from './register-plan';
26
+ import type { ModbusSpanAttributes } from './span-attributes';
27
+ /** One write in a flushed batch, with the attributes of the caller that issued it. */
28
+ export interface DebouncedWrite {
29
+ /** Register address. */
30
+ readonly address: number;
31
+ /** Value to write, as the caller gave it. */
32
+ readonly value: number;
33
+ /** Attributes of the caller that issued the newest value for this address. */
34
+ readonly attributes: ModbusSpanAttributes | undefined;
35
+ }
36
+ /** Options for {@link createWriteDebouncer}. */
37
+ export interface WriteDebouncerOptions {
38
+ /**
39
+ * How long a write is held before it reaches the wire. Each new arrival
40
+ * restarts this window.
41
+ *
42
+ * A window of zero disables the debouncer: every write is flushed on its own,
43
+ * with no timer and no batch. That is the escape hatch for a harness that
44
+ * settles by yielding rather than by advancing a clock, which would otherwise
45
+ * never reach a flush that runs on a forked fiber.
46
+ */
47
+ readonly window: Duration.Input;
48
+ /**
49
+ * Longest total hold, measured from the arrival that opened the batch.
50
+ *
51
+ * Without a ceiling, a register that updates faster than the window never
52
+ * reaches the wire at all, because every arrival restarts the wait.
53
+ *
54
+ * @defaultValue four times `window`
55
+ */
56
+ readonly maxHold?: Duration.Input;
57
+ /**
58
+ * Issues one batch.
59
+ *
60
+ * The batch holds at most one entry per address, newest value. The callback
61
+ * owns everything below it: the cache filter, the planning, and the span.
62
+ */
63
+ flush(batch: ReadonlyArray<DebouncedWrite>): Effect.Effect<void, ModbusError>;
64
+ }
65
+ /** A collection point for writes that arrive separately. */
66
+ export interface WriteDebouncer {
67
+ /**
68
+ * Holds a write for the window, then issues it with whatever else arrived.
69
+ *
70
+ * Succeeds when the value reached the device, and fails with the error of the
71
+ * flush that carried it.
72
+ */
73
+ write(write: RegisterWrite, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
74
+ /**
75
+ * Adds a write to the batch and flushes immediately, rather than waiting.
76
+ *
77
+ * This is an enqueue and then a flush, not a path around the batch. A write
78
+ * already pending for the same address is superseded, so the newest value is
79
+ * the one that reaches the device. A path that went around the batch would let
80
+ * an older held value be written after a newer immediate one.
81
+ */
82
+ writeNow(write: RegisterWrite, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
83
+ /**
84
+ * Holds several writes for the window, as one caller.
85
+ *
86
+ * A caller that already holds every value does not need a collection point,
87
+ * but it still must not go around the batch: a write held for one of these
88
+ * addresses would otherwise reach the device after the newer value. The whole
89
+ * group succeeds or fails together.
90
+ *
91
+ * With a window of zero the group is issued at once, so a caller of this
92
+ * method gets packed transactions with no debouncer at all.
93
+ */
94
+ writeAll(writes: ReadonlyArray<RegisterWrite>, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
95
+ /** Adds several writes to the batch and flushes immediately. */
96
+ writeAllNow(writes: ReadonlyArray<RegisterWrite>, attributes?: ModbusSpanAttributes): Effect.Effect<void, ModbusError>;
97
+ /**
98
+ * Issues whatever is pending, now.
99
+ *
100
+ * Never fails: the outcome goes to the callers waiting on the batch, not to
101
+ * whoever asked for the flush.
102
+ */
103
+ readonly flush: Effect.Effect<void>;
104
+ /** Addresses currently held. Intended for tests. */
105
+ readonly pending: number;
106
+ }
107
+ /**
108
+ * Creates a write debouncer bound to the current scope.
109
+ *
110
+ * The flush runs in that scope, not in the caller's. A caller interrupted while
111
+ * waiting — an output recomputed before its write landed — must not take the
112
+ * pending batch down with it, and a caller must not have to carry a `Scope` to
113
+ * issue a write.
114
+ *
115
+ * When the scope closes, callers still waiting are interrupted and the batch is
116
+ * discarded. Their writes never reached the device, and reporting success for
117
+ * them would break the invariant the `Deferred` exists to hold.
118
+ *
119
+ * @param options - The window, the ceiling, and the callback that issues a batch.
120
+ * @returns The debouncer.
121
+ *
122
+ * @example
123
+ * const debouncer = yield* createWriteDebouncer({
124
+ * window: '250 millis',
125
+ * maxHold: '1 second',
126
+ * flush: (batch) => issue(batch),
127
+ * });
128
+ * // Two callers that never meet, one transaction:
129
+ * yield* Effect.all([
130
+ * debouncer.write({ address: 2000, value: 10 }),
131
+ * debouncer.write({ address: 2001, value: 20 }),
132
+ * ], { concurrency: 'unbounded' });
133
+ */
134
+ export declare const createWriteDebouncer: (options: WriteDebouncerOptions) => Effect.Effect<WriteDebouncer, never, Scope.Scope>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flux-control/effect-modbus-rs",
3
- "version": "0.4.1",
3
+ "version": "0.6.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": {
@@ -33,8 +33,8 @@
33
33
  "lock": "bash tools/refresh-lockfile.sh",
34
34
  "prepare": "husky",
35
35
  "release": "changeset publish",
36
- "test": "bun test ./src/ ./examples/",
37
- "test:coverage": "bun test --coverage ./src/ ./examples/",
36
+ "test": "bun test ./test/ ./examples/",
37
+ "test:coverage": "bun test --coverage ./test/ ./examples/",
38
38
  "typecheck": "tsc --noEmit",
39
39
  "version": "changeset version"
40
40
  },
@@ -45,12 +45,13 @@
45
45
  "@changesets/cli": "^2.31.1",
46
46
  "@effect/language-service": "0.87.2",
47
47
  "@effect/platform-bun": "^4.0.0-rc.109",
48
+ "@oxlint/plugins": "1.76.0",
48
49
  "@types/bun": "latest",
49
50
  "effect": "^4.0.0-rc.109",
50
51
  "fallow": "^2.104.0",
51
52
  "husky": "^9.1.7",
52
53
  "oxfmt": "^0.60.0",
53
- "oxlint": "^1.75.0",
54
+ "oxlint": "1.76.0",
54
55
  "typescript": "^7.0.2"
55
56
  },
56
57
  "peerDependencies": {