@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.
- package/README.md +317 -20
- package/dist/index.d.ts +62 -1
- package/dist/index.js +1034 -213
- package/dist/src/AsciiTransportService.d.ts +14 -10
- package/dist/src/RtuTransportService.d.ts +14 -10
- package/dist/src/TcpTransportService.d.ts +14 -10
- package/dist/src/WasmAsciiTransportService.d.ts +15 -11
- package/dist/src/WasmRtuTransportService.d.ts +15 -11
- package/dist/src/WasmWsTransportService.d.ts +14 -10
- package/dist/src/batching-client.d.ts +197 -0
- package/dist/src/connection.d.ts +6 -3
- package/dist/src/mocks.d.ts +14 -8
- package/dist/src/modbus-client.d.ts +3 -1
- package/dist/src/read-debouncer.d.ts +100 -0
- package/dist/src/register-cache.d.ts +90 -0
- package/dist/src/register-plan.d.ts +212 -0
- package/dist/src/retry.d.ts +4 -4
- package/dist/src/shared-transport.d.ts +60 -4
- package/dist/src/span-attributes.d.ts +34 -0
- package/dist/src/write-debouncer.d.ts +134 -0
- package/dist/test/retry.test.d.ts +1 -0
- package/dist/test/shared-transport.test.d.ts +1 -0
- package/dist/test/span-attributes.test.d.ts +1 -0
- package/dist/test/upstream-options.test.d.ts +1 -0
- package/dist/test/wasm-mocks.test.d.ts +1 -0
- package/dist/test/write-debouncer.test.d.ts +1 -0
- package/package.json +5 -4
- /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
- /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
- /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
- /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
- /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
- /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
- /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
- /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.
|
|
3
|
+
"version": "0.5.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 ./
|
|
37
|
-
"test:coverage": "bun test --coverage ./
|
|
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": "
|
|
54
|
+
"oxlint": "1.76.0",
|
|
54
55
|
"typescript": "^7.0.2"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|