@flux-control/effect-modbus-rs 0.2.0 → 0.3.1
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 +256 -4
- package/dist/index.d.ts +54 -0
- package/dist/index.js +394 -173
- package/dist/src/AsciiTransportService.d.ts +26 -5
- package/dist/src/RtuTransportService.d.ts +26 -5
- package/dist/src/SerialTransportService.d.ts +21 -10
- package/dist/src/TcpTransportService.d.ts +26 -5
- package/dist/src/WasmAsciiTransportService.d.ts +20 -3
- package/dist/src/WasmRtuTransportService.d.ts +20 -3
- package/dist/src/WasmSerialTransportService.d.ts +19 -9
- package/dist/src/WasmWsTransportService.d.ts +18 -3
- package/dist/src/connection.d.ts +170 -0
- package/dist/src/errors.d.ts +25 -1
- package/dist/src/mock-options.test.d.ts +1 -0
- package/dist/src/mocks.d.ts +46 -7
- package/dist/src/modbus-client.d.ts +54 -3
- package/dist/src/resilience.test.d.ts +1 -0
- package/dist/src/retry.d.ts +227 -0
- package/dist/src/retry.test.d.ts +1 -0
- package/dist/src/shared-transport.d.ts +107 -6
- package/dist/src/shared-transport.test.d.ts +1 -0
- package/dist/src/upstream-options.test.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,6 +25,9 @@ class ModbusInternalError extends Data.TaggedError("ModbusInternalError") {
|
|
|
25
25
|
|
|
26
26
|
class ModbusNotConnectedError extends Data.TaggedError("ModbusNotConnectedError") {
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
class ModbusCircuitOpenError extends Data.TaggedError("ModbusCircuitOpenError") {
|
|
30
|
+
}
|
|
28
31
|
function parseExceptionCode(message) {
|
|
29
32
|
const m = message.match(/\[MODBUS_EXCEPTION:(\d+)\]/);
|
|
30
33
|
return m ? Number(m[1]) : undefined;
|
|
@@ -51,14 +54,204 @@ var toModbusError = (cause) => {
|
|
|
51
54
|
return new ModbusInternalError({ cause, message });
|
|
52
55
|
}
|
|
53
56
|
};
|
|
57
|
+
// src/retry.ts
|
|
58
|
+
import { Duration, Effect, Schedule } from "effect";
|
|
59
|
+
var retryableExceptionCodes = [5, 6, 10, 11];
|
|
60
|
+
var defaultRetryableTags = {
|
|
61
|
+
ModbusTimeoutError: true,
|
|
62
|
+
ModbusTransportError: true,
|
|
63
|
+
ModbusConnectionClosedError: true,
|
|
64
|
+
ModbusExceptionError: true,
|
|
65
|
+
ModbusInvalidArgumentError: false,
|
|
66
|
+
ModbusNotConnectedError: false,
|
|
67
|
+
ModbusCircuitOpenError: true,
|
|
68
|
+
ModbusInternalError: false
|
|
69
|
+
};
|
|
70
|
+
var allTags = Object.keys(defaultRetryableTags);
|
|
71
|
+
var toMillis = (input) => Duration.toMillis(Duration.decode(input));
|
|
72
|
+
var resolveRetryableTags = (options) => {
|
|
73
|
+
const retryable = {};
|
|
74
|
+
for (const tag of allTags) {
|
|
75
|
+
const entry = options.errors?.[tag];
|
|
76
|
+
retryable[tag] = entry === undefined ? defaultRetryableTags[tag] : entry !== false;
|
|
77
|
+
}
|
|
78
|
+
return retryable;
|
|
79
|
+
};
|
|
80
|
+
var defaultCurve = { baseMs: 100, factor: 2, maxMs: 5000 };
|
|
81
|
+
var resolveCurve = (overrides, fallback) => ({
|
|
82
|
+
baseMs: overrides.baseDelay === undefined ? fallback.baseMs : toMillis(overrides.baseDelay),
|
|
83
|
+
factor: overrides.factor ?? fallback.factor,
|
|
84
|
+
maxMs: overrides.maxDelay === undefined ? fallback.maxMs : toMillis(overrides.maxDelay)
|
|
85
|
+
});
|
|
86
|
+
var resolveDelays = (options) => {
|
|
87
|
+
const policyCurve = resolveCurve(options, defaultCurve);
|
|
88
|
+
const delays = {};
|
|
89
|
+
for (const tag of allTags) {
|
|
90
|
+
const entry = options.errors?.[tag];
|
|
91
|
+
delays[tag] = resolveCurve(typeof entry === "object" ? entry : {}, policyCurve);
|
|
92
|
+
}
|
|
93
|
+
return delays;
|
|
94
|
+
};
|
|
95
|
+
var mergeOptions = (base, overrides) => overrides === undefined ? base : {
|
|
96
|
+
...base,
|
|
97
|
+
...overrides,
|
|
98
|
+
...base.errors || overrides.errors ? { errors: { ...base.errors, ...overrides.errors } } : {}
|
|
99
|
+
};
|
|
100
|
+
var makeRetryPolicy = (options = {}) => {
|
|
101
|
+
const retryable = resolveRetryableTags(options);
|
|
102
|
+
const delays = resolveDelays(options);
|
|
103
|
+
const exceptions = options.retryableExceptions ?? retryableExceptionCodes;
|
|
104
|
+
const isRetryable = (error) => {
|
|
105
|
+
if (!retryable[error._tag])
|
|
106
|
+
return false;
|
|
107
|
+
if (error._tag === "ModbusExceptionError")
|
|
108
|
+
return exceptions.includes(error.exception);
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
const delayFor = (error, retryIndex) => {
|
|
112
|
+
const { baseMs, factor, maxMs } = delays[error._tag];
|
|
113
|
+
return Duration.millis(Math.min(maxMs, baseMs * factor ** retryIndex));
|
|
114
|
+
};
|
|
115
|
+
const base = Schedule.recurs(options.maxRetries ?? 3).pipe(Schedule.intersect(Schedule.identity()), Schedule.modifyDelay(([retryIndex, error]) => delayFor(error, retryIndex)), Schedule.whileInput(isRetryable));
|
|
116
|
+
const jitter = options.jitter ?? true;
|
|
117
|
+
const jittered = jitter === false ? base : Schedule.jitteredWith(base, jitter === true ? {} : jitter);
|
|
118
|
+
const schedule = options.maxElapsed === undefined ? jittered : Schedule.upTo(jittered, options.maxElapsed);
|
|
119
|
+
return { schedule, isRetryable };
|
|
120
|
+
};
|
|
121
|
+
var RetryPolicies = {
|
|
122
|
+
none: (overrides) => makeRetryPolicy(mergeOptions({ maxRetries: 0 }, overrides)),
|
|
123
|
+
serial: (overrides) => makeRetryPolicy(mergeOptions({
|
|
124
|
+
maxRetries: 3,
|
|
125
|
+
baseDelay: "50 millis",
|
|
126
|
+
factor: 2,
|
|
127
|
+
maxDelay: "1 second",
|
|
128
|
+
errors: { ModbusTimeoutError: { baseDelay: "100 millis" } }
|
|
129
|
+
}, overrides)),
|
|
130
|
+
tcp: (overrides) => makeRetryPolicy(mergeOptions({
|
|
131
|
+
maxRetries: 4,
|
|
132
|
+
baseDelay: "100 millis",
|
|
133
|
+
factor: 2,
|
|
134
|
+
maxDelay: "5 seconds",
|
|
135
|
+
errors: {
|
|
136
|
+
ModbusConnectionClosedError: { baseDelay: "250 millis", maxDelay: "10 seconds" }
|
|
137
|
+
}
|
|
138
|
+
}, overrides)),
|
|
139
|
+
persistent: (overrides) => makeRetryPolicy(mergeOptions({
|
|
140
|
+
maxRetries: 10,
|
|
141
|
+
baseDelay: "250 millis",
|
|
142
|
+
factor: 2,
|
|
143
|
+
maxDelay: "30 seconds",
|
|
144
|
+
maxElapsed: "5 minutes"
|
|
145
|
+
}, overrides))
|
|
146
|
+
};
|
|
147
|
+
var retryModbus = (policy) => (self) => Effect.retry(self, policy.schedule);
|
|
148
|
+
// src/connection.ts
|
|
149
|
+
import { Data as Data2, Duration as Duration2, Effect as Effect2, Either, SubscriptionRef } from "effect";
|
|
150
|
+
var ConnectionState = Data2.taggedEnum();
|
|
151
|
+
var defaultReconnectPolicy = makeRetryPolicy({
|
|
152
|
+
maxRetries: 5,
|
|
153
|
+
baseDelay: "250 millis",
|
|
154
|
+
factor: 2,
|
|
155
|
+
maxDelay: "10 seconds"
|
|
156
|
+
});
|
|
157
|
+
var defaultTriggers = [
|
|
158
|
+
"ModbusConnectionClosedError",
|
|
159
|
+
"ModbusTransportError"
|
|
160
|
+
];
|
|
161
|
+
var resolveReconnect = (options) => {
|
|
162
|
+
const triggerTags = options.triggerOn ?? defaultTriggers;
|
|
163
|
+
return {
|
|
164
|
+
policy: options.policy ?? defaultReconnectPolicy,
|
|
165
|
+
resetAfter: Duration2.decode(options.resetAfter ?? "30 seconds"),
|
|
166
|
+
triggers: (error) => triggerTags.includes(error._tag)
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
var guardCircuit = (state) => Effect2.flatMap(SubscriptionRef.get(state), (current) => ConnectionState.$match(current, {
|
|
170
|
+
Disconnected: () => Effect2.void,
|
|
171
|
+
Connected: () => Effect2.void,
|
|
172
|
+
Reconnecting: ({ attempt }) => Effect2.fail(new ModbusCircuitOpenError({
|
|
173
|
+
cause: new Error("Transport is reconnecting"),
|
|
174
|
+
message: `Transport is reconnecting (attempt ${attempt}); request refused`
|
|
175
|
+
})),
|
|
176
|
+
Down: ({ cause }) => Effect2.fail(new ModbusCircuitOpenError({
|
|
177
|
+
cause: cause.cause,
|
|
178
|
+
message: `Transport is down (${cause.message}); request refused`
|
|
179
|
+
}))
|
|
180
|
+
}));
|
|
181
|
+
var superviseReconnect = (reconnect, state, resolved) => Effect2.gen(function* () {
|
|
182
|
+
while (true) {
|
|
183
|
+
const attempt = reconnect.pipe(Effect2.tapError(() => SubscriptionRef.update(state, (current) => ConnectionState.$is("Reconnecting")(current) ? ConnectionState.Reconnecting({ attempt: current.attempt + 1 }) : current)), retryModbus(resolved.policy));
|
|
184
|
+
const result = yield* Effect2.either(attempt);
|
|
185
|
+
if (Either.isRight(result)) {
|
|
186
|
+
yield* SubscriptionRef.set(state, ConnectionState.Connected());
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
yield* Effect2.logDebug(`Reconnect attempts exhausted: ${result.left.message}`);
|
|
190
|
+
yield* SubscriptionRef.set(state, ConnectionState.Down({ cause: result.left }));
|
|
191
|
+
yield* Effect2.sleep(resolved.resetAfter);
|
|
192
|
+
const probe = yield* SubscriptionRef.modify(state, (current) => ConnectionState.$is("Down")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
193
|
+
if (!probe)
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
54
197
|
// src/AsciiTransportService.ts
|
|
55
|
-
import { Effect as
|
|
198
|
+
import { Effect as Effect6, Layer } from "effect";
|
|
56
199
|
|
|
57
200
|
// src/mocks.ts
|
|
58
|
-
import { Effect, Schema } from "effect";
|
|
201
|
+
import { Effect as Effect4, Schema, SubscriptionRef as SubscriptionRef2 } from "effect";
|
|
59
202
|
import {
|
|
60
203
|
CoilState
|
|
61
204
|
} from "modbus-rs";
|
|
205
|
+
|
|
206
|
+
// src/modbus-client.ts
|
|
207
|
+
import { Effect as Effect3 } from "effect";
|
|
208
|
+
var wrap = (try_) => Effect3.tryPromise({
|
|
209
|
+
try: try_,
|
|
210
|
+
catch: (error) => toModbusError(error)
|
|
211
|
+
});
|
|
212
|
+
var makeEffectModbusClient = (client) => ({
|
|
213
|
+
readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
|
|
214
|
+
readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
|
|
215
|
+
writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
|
|
216
|
+
writeMultipleRegisters: (opts) => wrap(() => client.writeMultipleRegisters(opts)),
|
|
217
|
+
readWriteMultipleRegisters: (opts) => wrap(() => client.readWriteMultipleRegisters(opts)),
|
|
218
|
+
readCoils: (opts) => wrap(() => client.readCoils(opts)),
|
|
219
|
+
writeSingleCoil: (opts) => wrap(() => client.writeSingleCoil(opts)),
|
|
220
|
+
writeMultipleCoils: (opts) => wrap(() => client.writeMultipleCoils(opts)),
|
|
221
|
+
readDiscreteInputs: (opts) => wrap(() => client.readDiscreteInputs(opts)),
|
|
222
|
+
readFifoQueue: (opts) => wrap(() => client.readFifoQueue(opts)),
|
|
223
|
+
readFileRecord: (opts) => wrap(() => client.readFileRecord(opts)),
|
|
224
|
+
writeFileRecord: (opts) => wrap(() => client.writeFileRecord(opts)),
|
|
225
|
+
readExceptionStatus: () => wrap(() => client.readExceptionStatus()),
|
|
226
|
+
diagnostics: (opts) => wrap(() => client.diagnostics(opts)),
|
|
227
|
+
readDeviceIdentification: (opts) => wrap(() => client.readDeviceIdentification(opts))
|
|
228
|
+
});
|
|
229
|
+
var withResilience = (operations, resilience) => {
|
|
230
|
+
const run = (operation) => {
|
|
231
|
+
const attempt = Effect3.zipRight(resilience.guard, Effect3.suspend(operation)).pipe(Effect3.tapError((error) => resilience.report(error)));
|
|
232
|
+
return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
readHoldingRegisters: (opts) => run(() => operations.readHoldingRegisters(opts)),
|
|
236
|
+
readInputRegisters: (opts) => run(() => operations.readInputRegisters(opts)),
|
|
237
|
+
writeSingleRegister: (opts) => run(() => operations.writeSingleRegister(opts)),
|
|
238
|
+
writeMultipleRegisters: (opts) => run(() => operations.writeMultipleRegisters(opts)),
|
|
239
|
+
readWriteMultipleRegisters: (opts) => run(() => operations.readWriteMultipleRegisters(opts)),
|
|
240
|
+
readCoils: (opts) => run(() => operations.readCoils(opts)),
|
|
241
|
+
writeSingleCoil: (opts) => run(() => operations.writeSingleCoil(opts)),
|
|
242
|
+
writeMultipleCoils: (opts) => run(() => operations.writeMultipleCoils(opts)),
|
|
243
|
+
readDiscreteInputs: (opts) => run(() => operations.readDiscreteInputs(opts)),
|
|
244
|
+
readFifoQueue: (opts) => run(() => operations.readFifoQueue(opts)),
|
|
245
|
+
readFileRecord: (opts) => run(() => operations.readFileRecord(opts)),
|
|
246
|
+
writeFileRecord: (opts) => run(() => operations.writeFileRecord(opts)),
|
|
247
|
+
readExceptionStatus: () => run(() => operations.readExceptionStatus()),
|
|
248
|
+
diagnostics: (opts) => run(() => operations.diagnostics(opts)),
|
|
249
|
+
readDeviceIdentification: (opts) => run(() => operations.readDeviceIdentification(opts)),
|
|
250
|
+
withRetry: (policy) => withResilience(operations, { ...resilience, policy })
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/mocks.ts
|
|
62
255
|
var CoilDefinition = Schema.Struct({
|
|
63
256
|
address: Schema.Number,
|
|
64
257
|
default: Schema.Boolean
|
|
@@ -112,8 +305,8 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
|
|
|
112
305
|
message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
|
|
113
306
|
});
|
|
114
307
|
var makeMockModbusClient = (state, unitId) => ({
|
|
115
|
-
readCoils:
|
|
116
|
-
yield*
|
|
308
|
+
readCoils: Effect4.fnUntraced(function* (opts) {
|
|
309
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
|
|
117
310
|
if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
|
|
118
311
|
return yield* failOutOfRange("Coil", opts.address, opts.quantity);
|
|
119
312
|
}
|
|
@@ -123,8 +316,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
123
316
|
}
|
|
124
317
|
return result;
|
|
125
318
|
}),
|
|
126
|
-
readDiscreteInputs:
|
|
127
|
-
yield*
|
|
319
|
+
readDiscreteInputs: Effect4.fnUntraced(function* (opts) {
|
|
320
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
|
|
128
321
|
if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
|
|
129
322
|
return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
|
|
130
323
|
}
|
|
@@ -134,8 +327,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
134
327
|
}
|
|
135
328
|
return result;
|
|
136
329
|
}),
|
|
137
|
-
readHoldingRegisters:
|
|
138
|
-
yield*
|
|
330
|
+
readHoldingRegisters: Effect4.fnUntraced(function* (opts) {
|
|
331
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
|
|
139
332
|
if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
|
|
140
333
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
|
|
141
334
|
}
|
|
@@ -145,8 +338,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
145
338
|
}
|
|
146
339
|
return new Uint16Array(result);
|
|
147
340
|
}),
|
|
148
|
-
readInputRegisters:
|
|
149
|
-
yield*
|
|
341
|
+
readInputRegisters: Effect4.fnUntraced(function* (opts) {
|
|
342
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
|
|
150
343
|
if (opts.address + opts.quantity > state.maxInputAddress + 1) {
|
|
151
344
|
return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
|
|
152
345
|
}
|
|
@@ -156,15 +349,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
156
349
|
}
|
|
157
350
|
return new Uint16Array(result);
|
|
158
351
|
}),
|
|
159
|
-
writeSingleCoil:
|
|
160
|
-
yield*
|
|
352
|
+
writeSingleCoil: Effect4.fnUntraced(function* (opts) {
|
|
353
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
|
|
161
354
|
if (opts.address > state.maxCoilAddress) {
|
|
162
355
|
return yield* failOutOfRange("Coil", opts.address);
|
|
163
356
|
}
|
|
164
357
|
state.coils.set(opts.address, opts.value === CoilState.On);
|
|
165
358
|
}),
|
|
166
|
-
writeMultipleCoils:
|
|
167
|
-
yield*
|
|
359
|
+
writeMultipleCoils: Effect4.fnUntraced(function* (opts) {
|
|
360
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
|
|
168
361
|
if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
|
|
169
362
|
return yield* failOutOfRange("Coil", opts.address, opts.values.length);
|
|
170
363
|
}
|
|
@@ -172,15 +365,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
172
365
|
state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
|
|
173
366
|
}
|
|
174
367
|
}),
|
|
175
|
-
writeSingleRegister:
|
|
176
|
-
yield*
|
|
368
|
+
writeSingleRegister: Effect4.fnUntraced(function* (opts) {
|
|
369
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
|
|
177
370
|
if (opts.address > state.maxHoldingAddress) {
|
|
178
371
|
return yield* failOutOfRange("HoldingRegister", opts.address);
|
|
179
372
|
}
|
|
180
373
|
state.holdingRegisters.set(opts.address, opts.value);
|
|
181
374
|
}),
|
|
182
|
-
writeMultipleRegisters:
|
|
183
|
-
yield*
|
|
375
|
+
writeMultipleRegisters: Effect4.fnUntraced(function* (opts) {
|
|
376
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
|
|
184
377
|
if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
|
|
185
378
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
|
|
186
379
|
}
|
|
@@ -188,8 +381,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
188
381
|
state.holdingRegisters.set(opts.address + i, opts.values[i]);
|
|
189
382
|
}
|
|
190
383
|
}),
|
|
191
|
-
readWriteMultipleRegisters:
|
|
192
|
-
yield*
|
|
384
|
+
readWriteMultipleRegisters: Effect4.fnUntraced(function* (opts) {
|
|
385
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
|
|
193
386
|
if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
|
|
194
387
|
return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
|
|
195
388
|
}
|
|
@@ -205,34 +398,34 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
205
398
|
}
|
|
206
399
|
return new Uint16Array(result);
|
|
207
400
|
}),
|
|
208
|
-
readFifoQueue:
|
|
401
|
+
readFifoQueue: Effect4.fnUntraced(function* (_opts) {
|
|
209
402
|
return yield* new ModbusInvalidArgumentError({
|
|
210
403
|
cause: new Error("FIFO queue not yet supported in mock"),
|
|
211
404
|
message: "FIFO queue not yet supported in mock"
|
|
212
405
|
});
|
|
213
406
|
}),
|
|
214
|
-
readFileRecord:
|
|
407
|
+
readFileRecord: Effect4.fnUntraced(function* (_opts) {
|
|
215
408
|
return yield* new ModbusInvalidArgumentError({
|
|
216
409
|
cause: new Error("File records not yet supported in mock"),
|
|
217
410
|
message: "File records not yet supported in mock"
|
|
218
411
|
});
|
|
219
412
|
}),
|
|
220
|
-
writeFileRecord:
|
|
413
|
+
writeFileRecord: Effect4.fnUntraced(function* (_opts) {
|
|
221
414
|
return yield* new ModbusInvalidArgumentError({
|
|
222
415
|
cause: new Error("File records not yet supported in mock"),
|
|
223
416
|
message: "File records not yet supported in mock"
|
|
224
417
|
});
|
|
225
418
|
}),
|
|
226
|
-
readExceptionStatus:
|
|
227
|
-
yield*
|
|
419
|
+
readExceptionStatus: Effect4.fnUntraced(function* () {
|
|
420
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
|
|
228
421
|
return 0;
|
|
229
422
|
}),
|
|
230
|
-
diagnostics:
|
|
231
|
-
yield*
|
|
423
|
+
diagnostics: Effect4.fnUntraced(function* (opts) {
|
|
424
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
|
|
232
425
|
return { subFunction: opts.subFunction, data: new Uint16Array };
|
|
233
426
|
}),
|
|
234
|
-
readDeviceIdentification:
|
|
235
|
-
yield*
|
|
427
|
+
readDeviceIdentification: Effect4.fnUntraced(function* (opts) {
|
|
428
|
+
yield* Effect4.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
|
|
236
429
|
return {
|
|
237
430
|
conformityLevel: 1,
|
|
238
431
|
moreFollows: false,
|
|
@@ -260,10 +453,32 @@ var makeMockTransport = (devices) => {
|
|
|
260
453
|
maxInputAddress: input.maxAddress
|
|
261
454
|
});
|
|
262
455
|
}
|
|
263
|
-
return (
|
|
264
|
-
yield*
|
|
456
|
+
return (options) => Effect4.gen(function* () {
|
|
457
|
+
yield* Effect4.logDebug("Mock transport opened with devices:", deviceDefs);
|
|
458
|
+
const connectionState = yield* SubscriptionRef2.make(ConnectionState.Connected());
|
|
459
|
+
const supervised = options.reconnect ? resolveReconnect(options.reconnect) : null;
|
|
460
|
+
const serviceScope = yield* Effect4.scope;
|
|
461
|
+
const reconnectOnce = Effect4.zipRight(Effect4.logDebug("Mock: reconnecting"), Effect4.suspend(() => {
|
|
462
|
+
const injected = options.reconnectFault?.();
|
|
463
|
+
return injected ? Effect4.fail(injected) : Effect4.void;
|
|
464
|
+
}));
|
|
465
|
+
const report = (error) => {
|
|
466
|
+
if (!supervised || !supervised.triggers(error))
|
|
467
|
+
return Effect4.void;
|
|
468
|
+
return Effect4.gen(function* () {
|
|
469
|
+
const claimed = yield* SubscriptionRef2.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
470
|
+
if (!claimed)
|
|
471
|
+
return;
|
|
472
|
+
yield* Effect4.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
const guard = Effect4.zipRight(supervised ? guardCircuit(connectionState) : Effect4.void, Effect4.suspend(() => {
|
|
476
|
+
const injected = options.fault?.();
|
|
477
|
+
return injected ? Effect4.fail(injected) : Effect4.void;
|
|
478
|
+
}));
|
|
265
479
|
return {
|
|
266
|
-
|
|
480
|
+
connectionState,
|
|
481
|
+
withClient: Effect4.fnUntraced(function* (unitId, clientOptions) {
|
|
267
482
|
const state = deviceStates.get(unitId);
|
|
268
483
|
if (!state) {
|
|
269
484
|
return yield* new ModbusInvalidArgumentError({
|
|
@@ -271,115 +486,122 @@ var makeMockTransport = (devices) => {
|
|
|
271
486
|
message: `Device with unitId ${unitId} not found in mock configuration`
|
|
272
487
|
});
|
|
273
488
|
}
|
|
274
|
-
return makeMockModbusClient(state, unitId)
|
|
489
|
+
return withResilience(makeMockModbusClient(state, unitId), {
|
|
490
|
+
guard,
|
|
491
|
+
report,
|
|
492
|
+
policy: clientOptions?.retry ?? options.retry
|
|
493
|
+
});
|
|
275
494
|
}),
|
|
276
|
-
setRequestTimeout: (_timeoutMs) =>
|
|
277
|
-
clearRequestTimeout: () =>
|
|
278
|
-
reconnect: () =>
|
|
279
|
-
close: () =>
|
|
495
|
+
setRequestTimeout: (_timeoutMs) => Effect4.void,
|
|
496
|
+
clearRequestTimeout: () => Effect4.void,
|
|
497
|
+
reconnect: () => Effect4.zipRight(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
|
|
498
|
+
close: () => Effect4.zipRight(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
|
|
280
499
|
hasPendingRequests: () => false
|
|
281
500
|
};
|
|
282
501
|
});
|
|
283
502
|
};
|
|
284
503
|
|
|
285
504
|
// src/shared-transport.ts
|
|
286
|
-
import { Effect as
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
readWriteMultipleRegisters: (opts) => wrap(() => client.readWriteMultipleRegisters(opts)),
|
|
300
|
-
readCoils: (opts) => wrap(() => client.readCoils(opts)),
|
|
301
|
-
writeSingleCoil: (opts) => wrap(() => client.writeSingleCoil(opts)),
|
|
302
|
-
writeMultipleCoils: (opts) => wrap(() => client.writeMultipleCoils(opts)),
|
|
303
|
-
readDiscreteInputs: (opts) => wrap(() => client.readDiscreteInputs(opts)),
|
|
304
|
-
readFifoQueue: (opts) => wrap(() => client.readFifoQueue(opts)),
|
|
305
|
-
readFileRecord: (opts) => wrap(() => client.readFileRecord(opts)),
|
|
306
|
-
writeFileRecord: (opts) => wrap(() => client.writeFileRecord(opts)),
|
|
307
|
-
readExceptionStatus: () => wrap(() => client.readExceptionStatus()),
|
|
308
|
-
diagnostics: (opts) => wrap(() => client.diagnostics(opts)),
|
|
309
|
-
readDeviceIdentification: (opts) => wrap(() => client.readDeviceIdentification(opts))
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// src/shared-transport.ts
|
|
505
|
+
import { Deferred, Effect as Effect5, Exit, Option, Ref, Scope, SubscriptionRef as SubscriptionRef3 } from "effect";
|
|
506
|
+
var makeInFlight = () => Ref.make(Option.none());
|
|
507
|
+
var singleFlight = (inFlight, work) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
|
|
508
|
+
const fresh = yield* Deferred.make();
|
|
509
|
+
const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
|
|
510
|
+
onSome: (existing) => [[existing, false], current],
|
|
511
|
+
onNone: () => [[fresh, true], Option.some(fresh)]
|
|
512
|
+
}));
|
|
513
|
+
if (isLeader) {
|
|
514
|
+
yield* Effect5.forkDaemon(Effect5.interruptible(work).pipe(Effect5.onExit((exit) => Effect5.zipRight(Ref.set(inFlight, Option.none()), Deferred.done(deferred, exit)))));
|
|
515
|
+
}
|
|
516
|
+
return yield* restore(Deferred.await(deferred));
|
|
517
|
+
}));
|
|
313
518
|
function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
314
|
-
return
|
|
315
|
-
const
|
|
519
|
+
return Effect5.fnUntraced(function* (options) {
|
|
520
|
+
const { retry: transportRetry, reconnect: reconnectOptions, ...rest } = options;
|
|
521
|
+
const openOptions = rest;
|
|
522
|
+
const mod = config?.moduleSpecifier === "modbus-rs/web" ? yield* Effect5.promise(() => import("modbus-rs/web")) : yield* Effect5.promise(() => import("modbus-rs"));
|
|
316
523
|
const TC = mod[transportKey];
|
|
317
524
|
let transport = null;
|
|
318
|
-
|
|
319
|
-
|
|
525
|
+
const opening = yield* makeInFlight();
|
|
526
|
+
const reconnecting = yield* makeInFlight();
|
|
527
|
+
const connectionState = yield* SubscriptionRef3.make(ConnectionState.Disconnected());
|
|
528
|
+
const supervised = reconnectOptions ? resolveReconnect(reconnectOptions) : null;
|
|
529
|
+
const serviceScope = yield* Effect5.scope;
|
|
320
530
|
const clientSet = new Map;
|
|
321
531
|
let closed = false;
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
if (closed) {
|
|
333
|
-
return yield* new ModbusNotConnectedError({
|
|
334
|
-
cause: new Error("Transport has been closed"),
|
|
335
|
-
message: "Transport has been closed"
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
if (!connectPromise) {
|
|
339
|
-
connectPromise = openMethod(TC, options);
|
|
340
|
-
}
|
|
341
|
-
const t = yield* Effect3.tryPromise({
|
|
342
|
-
try: () => connectPromise,
|
|
343
|
-
catch: (error) => toModbusError(error)
|
|
344
|
-
}).pipe(Effect3.catchAll((err) => {
|
|
345
|
-
connectPromise = null;
|
|
346
|
-
return Effect3.fail(err);
|
|
347
|
-
}));
|
|
348
|
-
if (closed) {
|
|
349
|
-
connectPromise = null;
|
|
350
|
-
yield* Effect3.fork(Effect3.promise(() => t.close()).pipe(Effect3.catchAll(() => Effect3.void)));
|
|
351
|
-
return yield* new ModbusNotConnectedError({
|
|
352
|
-
cause: new Error("Transport has been closed"),
|
|
353
|
-
message: "Transport has been closed"
|
|
354
|
-
});
|
|
355
|
-
}
|
|
532
|
+
const transportClosed = () => new ModbusNotConnectedError({
|
|
533
|
+
cause: new Error("Transport has been closed"),
|
|
534
|
+
message: "Transport has been closed"
|
|
535
|
+
});
|
|
536
|
+
const closeOrphan = (t) => Effect5.forkDaemon(Effect5.ignore(Effect5.tryPromise(() => t.close())));
|
|
537
|
+
const openTransport = Effect5.tryPromise({
|
|
538
|
+
try: () => openMethod(TC, openOptions),
|
|
539
|
+
catch: (error) => toModbusError(error)
|
|
540
|
+
}).pipe(Effect5.tap((t) => closed ? closeOrphan(t) : Effect5.zipRight(Effect5.sync(() => {
|
|
356
541
|
transport = t;
|
|
542
|
+
}), SubscriptionRef3.set(connectionState, ConnectionState.Connected()))));
|
|
543
|
+
const ensureOpen = Effect5.fnUntraced(function* () {
|
|
544
|
+
if (closed)
|
|
545
|
+
return yield* transportClosed();
|
|
546
|
+
if (transport)
|
|
547
|
+
return transport;
|
|
548
|
+
const t = yield* singleFlight(opening, openTransport);
|
|
549
|
+
if (closed)
|
|
550
|
+
return yield* transportClosed();
|
|
357
551
|
return t;
|
|
358
552
|
});
|
|
359
|
-
yield*
|
|
553
|
+
yield* Effect5.addFinalizer(() => {
|
|
360
554
|
if (closed)
|
|
361
|
-
return
|
|
555
|
+
return Effect5.void;
|
|
362
556
|
closed = true;
|
|
363
557
|
const t = transport;
|
|
364
558
|
if (!t)
|
|
365
|
-
return
|
|
366
|
-
return
|
|
559
|
+
return SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
|
|
560
|
+
return Effect5.andThen(Effect5.logDebug(`Closing ${serviceName}`), Effect5.zipRight(Effect5.promise(() => t.close()), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected())));
|
|
367
561
|
});
|
|
368
562
|
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
563
|
+
const reconnectOnce = Effect5.suspend(() => {
|
|
564
|
+
const t = transport;
|
|
565
|
+
if (closed || !t)
|
|
566
|
+
return transportClosed();
|
|
567
|
+
return singleFlight(reconnecting, Effect5.tryPromise({
|
|
568
|
+
try: () => t.reconnect(),
|
|
569
|
+
catch: (error) => toModbusError(error)
|
|
570
|
+
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
571
|
+
});
|
|
572
|
+
const report = (error) => {
|
|
573
|
+
if (!supervised || closed || !supervised.triggers(error))
|
|
574
|
+
return Effect5.void;
|
|
575
|
+
return Effect5.gen(function* () {
|
|
576
|
+
const claimed = yield* SubscriptionRef3.modify(connectionState, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
577
|
+
if (!claimed)
|
|
578
|
+
return;
|
|
579
|
+
yield* Effect5.logDebug(`${serviceName}: reconnecting after ${error.message}`);
|
|
580
|
+
yield* Effect5.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
const resilience = {
|
|
584
|
+
guard: supervised ? guardCircuit(connectionState) : Effect5.void,
|
|
585
|
+
report
|
|
586
|
+
};
|
|
369
587
|
return {
|
|
370
|
-
|
|
588
|
+
connectionState,
|
|
589
|
+
withClient: Effect5.fnUntraced(function* (unitId, clientOptions) {
|
|
371
590
|
const t = yield* ensureOpen();
|
|
372
591
|
let client = clientSet.get(unitId);
|
|
373
592
|
if (!client) {
|
|
374
|
-
client = yield*
|
|
593
|
+
client = yield* Effect5.try({
|
|
375
594
|
try: () => t.createClient({ unitId }),
|
|
376
595
|
catch: (error) => toModbusError(error)
|
|
377
596
|
});
|
|
378
597
|
clientSet.set(unitId, client);
|
|
379
598
|
}
|
|
380
|
-
return makeEffectModbusClient(client)
|
|
599
|
+
return withResilience(makeEffectModbusClient(client), {
|
|
600
|
+
...resilience,
|
|
601
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
602
|
+
});
|
|
381
603
|
}),
|
|
382
|
-
setRequestTimeout:
|
|
604
|
+
setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
|
|
383
605
|
const t = transport;
|
|
384
606
|
if (!t || closed) {
|
|
385
607
|
return yield* new ModbusNotConnectedError({
|
|
@@ -389,7 +611,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
389
611
|
}
|
|
390
612
|
t.setRequestTimeout(timeoutMs);
|
|
391
613
|
}),
|
|
392
|
-
clearRequestTimeout:
|
|
614
|
+
clearRequestTimeout: Effect5.fnUntraced(function* () {
|
|
393
615
|
const t = transport;
|
|
394
616
|
if (!t || closed) {
|
|
395
617
|
return yield* new ModbusNotConnectedError({
|
|
@@ -399,42 +621,35 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
399
621
|
}
|
|
400
622
|
t.clearRequestTimeout();
|
|
401
623
|
}),
|
|
402
|
-
reconnect:
|
|
403
|
-
if (closed)
|
|
404
|
-
return yield*
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
if (transport) {
|
|
410
|
-
if (!reconnectPromise) {
|
|
411
|
-
reconnectPromise = transport.reconnect().then(() => {
|
|
412
|
-
reconnectPromise = null;
|
|
413
|
-
}).catch((err) => {
|
|
414
|
-
reconnectPromise = null;
|
|
415
|
-
throw err;
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
yield* Effect3.tryPromise({
|
|
419
|
-
try: () => reconnectPromise,
|
|
420
|
-
catch: (error) => toModbusError(error)
|
|
421
|
-
});
|
|
422
|
-
} else {
|
|
624
|
+
reconnect: Effect5.fnUntraced(function* () {
|
|
625
|
+
if (closed)
|
|
626
|
+
return yield* transportClosed();
|
|
627
|
+
const t = transport;
|
|
628
|
+
if (!t) {
|
|
423
629
|
yield* ensureOpen();
|
|
630
|
+
return;
|
|
424
631
|
}
|
|
632
|
+
yield* singleFlight(reconnecting, Effect5.tryPromise({
|
|
633
|
+
try: () => t.reconnect(),
|
|
634
|
+
catch: (error) => toModbusError(error)
|
|
635
|
+
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
636
|
+
if (closed)
|
|
637
|
+
return yield* transportClosed();
|
|
638
|
+
yield* SubscriptionRef3.set(connectionState, ConnectionState.Connected());
|
|
425
639
|
}),
|
|
426
|
-
close:
|
|
640
|
+
close: Effect5.fnUntraced(function* () {
|
|
427
641
|
if (closed)
|
|
428
642
|
return;
|
|
429
643
|
closed = true;
|
|
644
|
+
yield* SubscriptionRef3.set(connectionState, ConnectionState.Disconnected());
|
|
430
645
|
const t = transport;
|
|
431
646
|
if (t) {
|
|
432
|
-
yield*
|
|
647
|
+
yield* Effect5.tryPromise({
|
|
433
648
|
try: () => t.close(),
|
|
434
649
|
catch: (error) => toModbusError(error)
|
|
435
650
|
});
|
|
436
651
|
}
|
|
437
|
-
const scope = yield*
|
|
652
|
+
const scope = yield* Effect5.scope;
|
|
438
653
|
yield* Scope.close(scope, Exit.void);
|
|
439
654
|
}),
|
|
440
655
|
hasPendingRequests: () => {
|
|
@@ -450,7 +665,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
450
665
|
}
|
|
451
666
|
|
|
452
667
|
// src/AsciiTransportService.ts
|
|
453
|
-
class AsciiTransportService extends
|
|
668
|
+
class AsciiTransportService extends Effect6.Service()("AsciiTransportService", {
|
|
454
669
|
scoped: makeTransportScoped("AsyncAsciiTransport", (TC, options) => TC.open(options), "AsciiTransportService")
|
|
455
670
|
}) {
|
|
456
671
|
static makeMockTransport = (devices) => {
|
|
@@ -462,8 +677,8 @@ class AsciiTransportService extends Effect4.Service()("AsciiTransportService", {
|
|
|
462
677
|
import { Context, Layer as Layer3 } from "effect";
|
|
463
678
|
|
|
464
679
|
// src/RtuTransportService.ts
|
|
465
|
-
import { Effect as
|
|
466
|
-
class RtuTransportService extends
|
|
680
|
+
import { Effect as Effect7, Layer as Layer2 } from "effect";
|
|
681
|
+
class RtuTransportService extends Effect7.Service()("RtuTransportService", {
|
|
467
682
|
scoped: makeTransportScoped("AsyncRtuTransport", (TC, options) => TC.open(options), "RtuTransportService")
|
|
468
683
|
}) {
|
|
469
684
|
static makeMockTransport = (devices) => {
|
|
@@ -486,8 +701,8 @@ class SerialTransportService extends Context.Tag("SerialTransportService")() {
|
|
|
486
701
|
};
|
|
487
702
|
}
|
|
488
703
|
// src/TcpTransportService.ts
|
|
489
|
-
import { Effect as
|
|
490
|
-
class TcpTransportService extends
|
|
704
|
+
import { Effect as Effect8, Layer as Layer4 } from "effect";
|
|
705
|
+
class TcpTransportService extends Effect8.Service()("TcpTransportService", {
|
|
491
706
|
scoped: makeTransportScoped("AsyncTcpTransport", (TC, options) => TC.connect(options), "TcpTransportService")
|
|
492
707
|
}) {
|
|
493
708
|
static makeMockTransport = (devices) => {
|
|
@@ -496,62 +711,62 @@ class TcpTransportService extends Effect7.Service()("TcpTransportService", {
|
|
|
496
711
|
};
|
|
497
712
|
}
|
|
498
713
|
// src/SerialModbusServerService.ts
|
|
499
|
-
import { Effect as
|
|
500
|
-
var serialRtuServerLayer = (options, handlers) => Layer5.scopedDiscard(
|
|
501
|
-
const { AsyncSerialModbusServer } = yield*
|
|
502
|
-
const server = yield*
|
|
714
|
+
import { Effect as Effect9, Layer as Layer5 } from "effect";
|
|
715
|
+
var serialRtuServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect9.gen(function* () {
|
|
716
|
+
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
717
|
+
const server = yield* Effect9.tryPromise({
|
|
503
718
|
try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
|
|
504
719
|
catch: (error) => toModbusError(error)
|
|
505
720
|
});
|
|
506
|
-
yield*
|
|
507
|
-
yield*
|
|
721
|
+
yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
|
|
722
|
+
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
508
723
|
try: () => server.shutdown(),
|
|
509
724
|
catch: (error) => toModbusError(error)
|
|
510
|
-
})),
|
|
725
|
+
})), Effect9.catchAll(() => Effect9.void)));
|
|
511
726
|
}));
|
|
512
|
-
var serialAsciiServerLayer = (options, handlers) => Layer5.scopedDiscard(
|
|
513
|
-
const { AsyncSerialModbusServer } = yield*
|
|
514
|
-
const server = yield*
|
|
727
|
+
var serialAsciiServerLayer = (options, handlers) => Layer5.scopedDiscard(Effect9.gen(function* () {
|
|
728
|
+
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
729
|
+
const server = yield* Effect9.tryPromise({
|
|
515
730
|
try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
|
|
516
731
|
catch: (error) => toModbusError(error)
|
|
517
732
|
});
|
|
518
|
-
yield*
|
|
519
|
-
yield*
|
|
733
|
+
yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
|
|
734
|
+
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
520
735
|
try: () => server.shutdown(),
|
|
521
736
|
catch: (error) => toModbusError(error)
|
|
522
|
-
})),
|
|
737
|
+
})), Effect9.catchAll(() => Effect9.void)));
|
|
523
738
|
}));
|
|
524
739
|
// src/TcpModbusServerService.ts
|
|
525
|
-
import { Effect as
|
|
526
|
-
var tcpServerLayer = (options, handlers) => Layer6.scopedDiscard(
|
|
527
|
-
const { AsyncTcpModbusServer } = yield*
|
|
528
|
-
const server = yield*
|
|
740
|
+
import { Effect as Effect10, Layer as Layer6 } from "effect";
|
|
741
|
+
var tcpServerLayer = (options, handlers) => Layer6.scopedDiscard(Effect10.gen(function* () {
|
|
742
|
+
const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
|
|
743
|
+
const server = yield* Effect10.tryPromise({
|
|
529
744
|
try: () => AsyncTcpModbusServer.bind(options, handlers),
|
|
530
745
|
catch: (error) => toModbusError(error)
|
|
531
746
|
});
|
|
532
|
-
yield*
|
|
533
|
-
yield*
|
|
747
|
+
yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
|
|
748
|
+
yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
|
|
534
749
|
try: () => server.shutdown(),
|
|
535
750
|
catch: (error) => toModbusError(error)
|
|
536
|
-
})),
|
|
751
|
+
})), Effect10.catchAll(() => Effect10.void)));
|
|
537
752
|
}));
|
|
538
753
|
// src/TcpGatewayService.ts
|
|
539
|
-
import { Effect as
|
|
540
|
-
var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(
|
|
541
|
-
const { AsyncTcpGateway } = yield*
|
|
542
|
-
const gateway = yield*
|
|
754
|
+
import { Effect as Effect11, Layer as Layer7 } from "effect";
|
|
755
|
+
var tcpGatewayLayer = (options, gatewayConfig) => Layer7.scopedDiscard(Effect11.gen(function* () {
|
|
756
|
+
const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
|
|
757
|
+
const gateway = yield* Effect11.tryPromise({
|
|
543
758
|
try: () => AsyncTcpGateway.bind(options, gatewayConfig),
|
|
544
759
|
catch: (error) => toModbusError(error)
|
|
545
760
|
});
|
|
546
|
-
yield*
|
|
547
|
-
yield*
|
|
761
|
+
yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
|
|
762
|
+
yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
|
|
548
763
|
try: () => gateway.shutdown(),
|
|
549
764
|
catch: (error) => toModbusError(error)
|
|
550
|
-
})),
|
|
765
|
+
})), Effect11.catchAll(() => Effect11.void)));
|
|
551
766
|
}));
|
|
552
767
|
// src/WasmWsTransportService.ts
|
|
553
|
-
import { Effect as
|
|
554
|
-
class WasmWsTransportService extends
|
|
768
|
+
import { Effect as Effect12, Layer as Layer8 } from "effect";
|
|
769
|
+
class WasmWsTransportService extends Effect12.Service()("WasmWsTransportService", {
|
|
555
770
|
scoped: makeTransportScoped("WasmWsTransport", (TC, options) => TC.connect(options), "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
556
771
|
}) {
|
|
557
772
|
static makeMockTransport = (devices) => {
|
|
@@ -560,8 +775,8 @@ class WasmWsTransportService extends Effect11.Service()("WasmWsTransportService"
|
|
|
560
775
|
};
|
|
561
776
|
}
|
|
562
777
|
// src/WasmRtuTransportService.ts
|
|
563
|
-
import { Effect as
|
|
564
|
-
class WasmRtuTransportService extends
|
|
778
|
+
import { Effect as Effect13, Layer as Layer9 } from "effect";
|
|
779
|
+
class WasmRtuTransportService extends Effect13.Service()("WasmRtuTransportService", {
|
|
565
780
|
scoped: makeTransportScoped("WasmRtuTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
566
781
|
}) {
|
|
567
782
|
static makeMockTransport = (devices) => {
|
|
@@ -570,8 +785,8 @@ class WasmRtuTransportService extends Effect12.Service()("WasmRtuTransportServic
|
|
|
570
785
|
};
|
|
571
786
|
}
|
|
572
787
|
// src/WasmAsciiTransportService.ts
|
|
573
|
-
import { Effect as
|
|
574
|
-
class WasmAsciiTransportService extends
|
|
788
|
+
import { Effect as Effect14, Layer as Layer10 } from "effect";
|
|
789
|
+
class WasmAsciiTransportService extends Effect14.Service()("WasmAsciiTransportService", {
|
|
575
790
|
scoped: makeTransportScoped("WasmAsciiTransport", (TC, { port, ...rest }) => TC.open(port, rest), "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" })
|
|
576
791
|
}) {
|
|
577
792
|
static makeMockTransport = (devices) => {
|
|
@@ -663,7 +878,10 @@ export {
|
|
|
663
878
|
tcpGatewayLayer,
|
|
664
879
|
serialRtuServerLayer,
|
|
665
880
|
serialAsciiServerLayer,
|
|
881
|
+
retryableExceptionCodes,
|
|
882
|
+
retryModbus,
|
|
666
883
|
requestSerialPort,
|
|
884
|
+
makeRetryPolicy,
|
|
667
885
|
WasmWsTransportService,
|
|
668
886
|
WasmSerialTransportService,
|
|
669
887
|
WasmRtuTransportService,
|
|
@@ -671,6 +889,7 @@ export {
|
|
|
671
889
|
TcpTransportService,
|
|
672
890
|
SerialTransportService,
|
|
673
891
|
RtuTransportService,
|
|
892
|
+
RetryPolicies,
|
|
674
893
|
ModbusTransportError,
|
|
675
894
|
ModbusTimeoutError,
|
|
676
895
|
ModbusNotConnectedError,
|
|
@@ -678,5 +897,7 @@ export {
|
|
|
678
897
|
ModbusInternalError,
|
|
679
898
|
ModbusExceptionError,
|
|
680
899
|
ModbusConnectionClosedError,
|
|
900
|
+
ModbusCircuitOpenError,
|
|
901
|
+
ConnectionState,
|
|
681
902
|
AsciiTransportService
|
|
682
903
|
};
|