@flux-control/effect-modbus-rs 0.4.0 → 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 +1043 -225
- 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 +31 -4
- 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 +6 -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
package/dist/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
-
|
|
4
1
|
// src/errors.ts
|
|
5
2
|
import { Data } from "effect";
|
|
6
3
|
import { getModbusErrorCode, ModbusErrorCode } from "modbus-rs";
|
|
@@ -67,10 +64,19 @@ var defaultRetryableTags = {
|
|
|
67
64
|
ModbusCircuitOpenError: true,
|
|
68
65
|
ModbusInternalError: false
|
|
69
66
|
};
|
|
70
|
-
var allTags =
|
|
67
|
+
var allTags = [
|
|
68
|
+
"ModbusTimeoutError",
|
|
69
|
+
"ModbusTransportError",
|
|
70
|
+
"ModbusConnectionClosedError",
|
|
71
|
+
"ModbusExceptionError",
|
|
72
|
+
"ModbusInvalidArgumentError",
|
|
73
|
+
"ModbusNotConnectedError",
|
|
74
|
+
"ModbusCircuitOpenError",
|
|
75
|
+
"ModbusInternalError"
|
|
76
|
+
];
|
|
71
77
|
var toMillis = (input) => Duration.toMillis(Duration.fromInputUnsafe(input));
|
|
72
78
|
var resolveRetryableTags = (options) => {
|
|
73
|
-
const retryable = {};
|
|
79
|
+
const retryable = { ...defaultRetryableTags };
|
|
74
80
|
for (const tag of allTags) {
|
|
75
81
|
const entry = options.errors?.[tag];
|
|
76
82
|
retryable[tag] = entry === undefined ? defaultRetryableTags[tag] : entry !== false;
|
|
@@ -85,19 +91,25 @@ var resolveCurve = (overrides, fallback) => ({
|
|
|
85
91
|
});
|
|
86
92
|
var resolveDelays = (options) => {
|
|
87
93
|
const policyCurve = resolveCurve(options, defaultCurve);
|
|
88
|
-
const delays = {
|
|
94
|
+
const delays = {
|
|
95
|
+
ModbusTimeoutError: policyCurve,
|
|
96
|
+
ModbusTransportError: policyCurve,
|
|
97
|
+
ModbusConnectionClosedError: policyCurve,
|
|
98
|
+
ModbusExceptionError: policyCurve,
|
|
99
|
+
ModbusInvalidArgumentError: policyCurve,
|
|
100
|
+
ModbusNotConnectedError: policyCurve,
|
|
101
|
+
ModbusCircuitOpenError: policyCurve,
|
|
102
|
+
ModbusInternalError: policyCurve
|
|
103
|
+
};
|
|
89
104
|
for (const tag of allTags) {
|
|
90
105
|
const entry = options.errors?.[tag];
|
|
91
|
-
|
|
106
|
+
const overrides = entry === undefined || entry === true || entry === false ? {} : entry;
|
|
107
|
+
delays[tag] = resolveCurve(overrides, policyCurve);
|
|
92
108
|
}
|
|
93
109
|
return delays;
|
|
94
110
|
};
|
|
95
|
-
var mergeOptions = (base, overrides) => overrides === undefined ? base : {
|
|
96
|
-
|
|
97
|
-
...overrides,
|
|
98
|
-
...base.errors || overrides.errors ? { errors: { ...base.errors, ...overrides.errors } } : {}
|
|
99
|
-
};
|
|
100
|
-
var makeRetryPolicy = (options = {}) => {
|
|
111
|
+
var mergeOptions = (base, overrides) => overrides === undefined ? base : base.errors || overrides.errors ? { ...base, ...overrides, errors: { ...base.errors, ...overrides.errors } } : { ...base, ...overrides };
|
|
112
|
+
var createRetryPolicy = (options = {}) => {
|
|
101
113
|
const retryable = resolveRetryableTags(options);
|
|
102
114
|
const delays = resolveDelays(options);
|
|
103
115
|
const exceptions = options.retryableExceptions ?? retryableExceptionCodes;
|
|
@@ -123,15 +135,15 @@ var makeRetryPolicy = (options = {}) => {
|
|
|
123
135
|
return { schedule, isRetryable };
|
|
124
136
|
};
|
|
125
137
|
var RetryPolicies = {
|
|
126
|
-
none: (overrides) =>
|
|
127
|
-
serial: (overrides) =>
|
|
138
|
+
none: (overrides) => createRetryPolicy(mergeOptions({ maxRetries: 0 }, overrides)),
|
|
139
|
+
serial: (overrides) => createRetryPolicy(mergeOptions({
|
|
128
140
|
maxRetries: 3,
|
|
129
141
|
baseDelay: "50 millis",
|
|
130
142
|
factor: 2,
|
|
131
143
|
maxDelay: "1 second",
|
|
132
144
|
errors: { ModbusTimeoutError: { baseDelay: "100 millis" } }
|
|
133
145
|
}, overrides)),
|
|
134
|
-
tcp: (overrides) =>
|
|
146
|
+
tcp: (overrides) => createRetryPolicy(mergeOptions({
|
|
135
147
|
maxRetries: 4,
|
|
136
148
|
baseDelay: "100 millis",
|
|
137
149
|
factor: 2,
|
|
@@ -140,7 +152,7 @@ var RetryPolicies = {
|
|
|
140
152
|
ModbusConnectionClosedError: { baseDelay: "250 millis", maxDelay: "10 seconds" }
|
|
141
153
|
}
|
|
142
154
|
}, overrides)),
|
|
143
|
-
persistent: (overrides) =>
|
|
155
|
+
persistent: (overrides) => createRetryPolicy(mergeOptions({
|
|
144
156
|
maxRetries: 10,
|
|
145
157
|
baseDelay: "250 millis",
|
|
146
158
|
factor: 2,
|
|
@@ -152,7 +164,7 @@ var retryModbus = (policy) => (self) => Effect.retry(self, policy.schedule);
|
|
|
152
164
|
// src/connection.ts
|
|
153
165
|
import { Data as Data2, Duration as Duration2, Effect as Effect2, Result, SubscriptionRef } from "effect";
|
|
154
166
|
var ConnectionState = Data2.taggedEnum();
|
|
155
|
-
var defaultReconnectPolicy =
|
|
167
|
+
var defaultReconnectPolicy = createRetryPolicy({
|
|
156
168
|
maxRetries: 5,
|
|
157
169
|
baseDelay: "250 millis",
|
|
158
170
|
factor: 2,
|
|
@@ -198,22 +210,716 @@ var superviseReconnect = (reconnect, state, resolved) => Effect2.gen(function* (
|
|
|
198
210
|
return;
|
|
199
211
|
}
|
|
200
212
|
});
|
|
213
|
+
var claimReconnect = (reconnect, state, resolved, scope, onClaim) => Effect2.gen(function* () {
|
|
214
|
+
const claimed = yield* SubscriptionRef.modify(state, (current) => ConnectionState.$is("Connected")(current) ? [true, ConnectionState.Reconnecting({ attempt: 0 })] : [false, current]);
|
|
215
|
+
if (!claimed)
|
|
216
|
+
return;
|
|
217
|
+
if (onClaim)
|
|
218
|
+
yield* onClaim;
|
|
219
|
+
yield* Effect2.forkIn(superviseReconnect(reconnect, state, resolved), scope);
|
|
220
|
+
});
|
|
221
|
+
// src/register-plan.ts
|
|
222
|
+
var MODBUS_MAX_WRITE_REGISTERS = 123;
|
|
223
|
+
var MODBUS_MAX_READ_REGISTERS = 125;
|
|
224
|
+
var MAX_REGISTER_VALUE = 65535;
|
|
225
|
+
var MAX_REGISTER_ADDRESS = 65535;
|
|
226
|
+
var MIN_REGISTER_VALUE = -32768;
|
|
227
|
+
var assertPositiveInteger = (name, value) => {
|
|
228
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
229
|
+
throw new RangeError(`${name} must be an integer of 1 or more, got ${value}`);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var assertTransactionLimit = (name, value, maximum) => {
|
|
233
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
234
|
+
throw new RangeError(`${name} must be an integer from 1 to ${maximum}, got ${value}`);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var assertAddress = (address) => {
|
|
238
|
+
if (!Number.isInteger(address) || address < 0 || address > MAX_REGISTER_ADDRESS) {
|
|
239
|
+
throw new RangeError(`address must be an integer from 0 to ${MAX_REGISTER_ADDRESS}, got ${address}`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var encodeRegisterValue = (value) => {
|
|
243
|
+
if (!Number.isInteger(value) || value < MIN_REGISTER_VALUE || value > MAX_REGISTER_VALUE) {
|
|
244
|
+
throw new RangeError(`value must be an integer from ${MIN_REGISTER_VALUE} to ${MAX_REGISTER_VALUE}, got ${value}`);
|
|
245
|
+
}
|
|
246
|
+
return value & MAX_REGISTER_VALUE;
|
|
247
|
+
};
|
|
248
|
+
var planWrites = (writes, options = {}) => {
|
|
249
|
+
const maxRegistersPerWrite = options.maxRegistersPerWrite ?? MODBUS_MAX_WRITE_REGISTERS;
|
|
250
|
+
const minRunLength = options.minRunLength ?? 2;
|
|
251
|
+
assertTransactionLimit("maxRegistersPerWrite", maxRegistersPerWrite, MODBUS_MAX_WRITE_REGISTERS);
|
|
252
|
+
assertPositiveInteger("minRunLength", minRunLength);
|
|
253
|
+
const latest = new Map;
|
|
254
|
+
for (const write of writes) {
|
|
255
|
+
assertAddress(write.address);
|
|
256
|
+
latest.set(write.address, encodeRegisterValue(write.value));
|
|
257
|
+
}
|
|
258
|
+
if (latest.size === 0)
|
|
259
|
+
return [];
|
|
260
|
+
const addresses = Array.from(latest.keys()).sort((left, right) => left - right);
|
|
261
|
+
const steps = [];
|
|
262
|
+
const emitRun = (start, end) => {
|
|
263
|
+
const length = end - start;
|
|
264
|
+
if (length >= minRunLength) {
|
|
265
|
+
const values = new Uint16Array(length);
|
|
266
|
+
for (let index = 0;index < length; index += 1) {
|
|
267
|
+
values[index] = latest.get(addresses[start + index]);
|
|
268
|
+
}
|
|
269
|
+
steps.push({ kind: "multiple", address: addresses[start], values });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (let index = start;index < end; index += 1) {
|
|
273
|
+
const address = addresses[index];
|
|
274
|
+
steps.push({ kind: "single", address, value: latest.get(address) });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
let runStart = 0;
|
|
278
|
+
for (let index = 1;index <= addresses.length; index += 1) {
|
|
279
|
+
const endsHere = index === addresses.length || addresses[index] !== addresses[index - 1] + 1 || index - runStart >= maxRegistersPerWrite;
|
|
280
|
+
if (endsHere) {
|
|
281
|
+
emitRun(runStart, index);
|
|
282
|
+
runStart = index;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return steps;
|
|
286
|
+
};
|
|
287
|
+
var planReads = (addresses, options = {}) => {
|
|
288
|
+
const maxRegistersPerRead = options.maxRegistersPerRead ?? MODBUS_MAX_READ_REGISTERS;
|
|
289
|
+
const maxGap = options.maxGap ?? 0;
|
|
290
|
+
assertTransactionLimit("maxRegistersPerRead", maxRegistersPerRead, MODBUS_MAX_READ_REGISTERS);
|
|
291
|
+
if (!Number.isInteger(maxGap) || maxGap < 0) {
|
|
292
|
+
throw new RangeError(`maxGap must be a non-negative integer, got ${maxGap}`);
|
|
293
|
+
}
|
|
294
|
+
for (const address of addresses)
|
|
295
|
+
assertAddress(address);
|
|
296
|
+
const sorted = Array.from(new Set(addresses)).sort((left, right) => left - right);
|
|
297
|
+
const spans = [];
|
|
298
|
+
if (sorted.length > 0) {
|
|
299
|
+
let start = sorted[0];
|
|
300
|
+
let end = sorted[0];
|
|
301
|
+
for (let index = 1;index < sorted.length; index += 1) {
|
|
302
|
+
const address = sorted[index];
|
|
303
|
+
const fitsGap = address - end - 1 <= maxGap;
|
|
304
|
+
const fitsSpan = address - start + 1 <= maxRegistersPerRead;
|
|
305
|
+
if (fitsGap && fitsSpan) {
|
|
306
|
+
end = address;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
spans.push({ address: start, quantity: end - start + 1 });
|
|
310
|
+
start = address;
|
|
311
|
+
end = address;
|
|
312
|
+
}
|
|
313
|
+
spans.push({ address: start, quantity: end - start + 1 });
|
|
314
|
+
}
|
|
315
|
+
const locate = (address) => {
|
|
316
|
+
if (!Number.isInteger(address) || address < 0 || address > MAX_REGISTER_ADDRESS) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
let low = 0;
|
|
320
|
+
let high = spans.length - 1;
|
|
321
|
+
while (low <= high) {
|
|
322
|
+
const middle = low + high >> 1;
|
|
323
|
+
const span = spans[middle];
|
|
324
|
+
if (address < span.address)
|
|
325
|
+
high = middle - 1;
|
|
326
|
+
else if (address >= span.address + span.quantity)
|
|
327
|
+
low = middle + 1;
|
|
328
|
+
else
|
|
329
|
+
return { span: middle, offset: address - span.address };
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
};
|
|
333
|
+
return { spans, locate };
|
|
334
|
+
};
|
|
335
|
+
// src/register-cache.ts
|
|
336
|
+
var assertRegisterAddress = (address) => {
|
|
337
|
+
if (!Number.isInteger(address) || address < 0 || address > 65535) {
|
|
338
|
+
throw new RangeError(`address must be an integer from 0 to 65535, got ${address}`);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var createRegisterCache = () => {
|
|
342
|
+
const held = new Map;
|
|
343
|
+
const keyOf = (unitId, address) => `${unitId}:${address}`;
|
|
344
|
+
const unitGenerations = new Map;
|
|
345
|
+
let busGeneration = 0;
|
|
346
|
+
const generationOf = (unitId) => (unitGenerations.get(unitId) ?? 0) + busGeneration;
|
|
347
|
+
return {
|
|
348
|
+
filter: (unitId, writes) => {
|
|
349
|
+
const latest = new Map;
|
|
350
|
+
const suppressed = [];
|
|
351
|
+
for (const write of writes) {
|
|
352
|
+
assertRegisterAddress(write.address);
|
|
353
|
+
const encoded = encodeRegisterValue(write.value);
|
|
354
|
+
const superseded = latest.get(write.address);
|
|
355
|
+
if (superseded !== undefined)
|
|
356
|
+
suppressed.push(superseded.write);
|
|
357
|
+
latest.set(write.address, { write, encoded });
|
|
358
|
+
}
|
|
359
|
+
const pending = [];
|
|
360
|
+
for (const { encoded, write } of latest.values()) {
|
|
361
|
+
if (held.get(keyOf(unitId, write.address)) === encoded) {
|
|
362
|
+
suppressed.push(write);
|
|
363
|
+
} else {
|
|
364
|
+
pending.push(write);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { pending, suppressed };
|
|
368
|
+
},
|
|
369
|
+
generationOf,
|
|
370
|
+
observe: (unitId, address, value, generation) => {
|
|
371
|
+
assertRegisterAddress(address);
|
|
372
|
+
if (generation !== undefined && generation !== generationOf(unitId))
|
|
373
|
+
return;
|
|
374
|
+
held.set(keyOf(unitId, address), encodeRegisterValue(value));
|
|
375
|
+
},
|
|
376
|
+
invalidate: (unitId) => {
|
|
377
|
+
if (unitId === undefined) {
|
|
378
|
+
held.clear();
|
|
379
|
+
busGeneration += 1;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
unitGenerations.set(unitId, (unitGenerations.get(unitId) ?? 0) + 1);
|
|
383
|
+
const prefix = `${unitId}:`;
|
|
384
|
+
for (const key of held.keys()) {
|
|
385
|
+
if (key.startsWith(prefix))
|
|
386
|
+
held.delete(key);
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
get size() {
|
|
390
|
+
return held.size;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
// src/write-debouncer.ts
|
|
395
|
+
import { Clock, Deferred, Duration as Duration3, Effect as Effect3, Fiber, Semaphore } from "effect";
|
|
396
|
+
var createWriteDebouncer = (options) => Effect3.gen(function* () {
|
|
397
|
+
const windowMs = Duration3.toMillis(Duration3.fromInputUnsafe(options.window));
|
|
398
|
+
const maxHoldMs = options.maxHold === undefined ? windowMs * 4 : Duration3.toMillis(Duration3.fromInputUnsafe(options.maxHold));
|
|
399
|
+
const scope = yield* Effect3.scope;
|
|
400
|
+
const lock = yield* Semaphore.make(1);
|
|
401
|
+
const flushLock = yield* Semaphore.make(1);
|
|
402
|
+
const held = new Map;
|
|
403
|
+
let timer;
|
|
404
|
+
let nextTimerId = 0;
|
|
405
|
+
let openedAtMs;
|
|
406
|
+
let closed = false;
|
|
407
|
+
const scopeClosedError = () => {
|
|
408
|
+
const message = "The write debouncer scope has been closed";
|
|
409
|
+
return new ModbusNotConnectedError({ cause: new Error(message), message });
|
|
410
|
+
};
|
|
411
|
+
const ensureOpen = Effect3.suspend(() => closed ? Effect3.fail(scopeClosedError()) : Effect3.void);
|
|
412
|
+
const validateWrites = (writes) => Effect3.try({
|
|
413
|
+
try: () => {
|
|
414
|
+
for (const write of writes) {
|
|
415
|
+
if (!Number.isInteger(write.address) || write.address < 0 || write.address > 65535) {
|
|
416
|
+
throw new RangeError(`address must be an integer from 0 to 65535, got ${write.address}`);
|
|
417
|
+
}
|
|
418
|
+
encodeRegisterValue(write.value);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
catch: (cause) => {
|
|
422
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
423
|
+
return new ModbusInvalidArgumentError({ cause: error, message: error.message });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const flushPending = (expectedTimer) => flushLock.withPermits(1)(Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
|
|
427
|
+
const taken = yield* lock.withPermits(1)(Effect3.sync(() => {
|
|
428
|
+
if (expectedTimer !== undefined && timer?.id !== expectedTimer.id)
|
|
429
|
+
return;
|
|
430
|
+
const activeTimer = timer;
|
|
431
|
+
timer = undefined;
|
|
432
|
+
const batch = Array.from(held.values());
|
|
433
|
+
held.clear();
|
|
434
|
+
openedAtMs = undefined;
|
|
435
|
+
return { activeTimer, batch };
|
|
436
|
+
}));
|
|
437
|
+
if (taken === undefined)
|
|
438
|
+
return;
|
|
439
|
+
if (expectedTimer === undefined)
|
|
440
|
+
taken.activeTimer?.fiber?.interruptUnsafe();
|
|
441
|
+
if (taken.batch.length === 0)
|
|
442
|
+
return;
|
|
443
|
+
const exit = yield* Effect3.exit(restore(options.flush(taken.batch.map((entry) => ({
|
|
444
|
+
address: entry.write.address,
|
|
445
|
+
value: entry.write.value,
|
|
446
|
+
attributes: entry.attributes
|
|
447
|
+
})))));
|
|
448
|
+
yield* Effect3.forEach(taken.batch.flatMap((entry) => entry.waiters), (waiter) => Deferred.done(waiter, exit), { discard: true });
|
|
449
|
+
})));
|
|
450
|
+
const enqueue = (writes, attributes, restartTimer) => Effect3.gen(function* () {
|
|
451
|
+
const waiter = yield* Deferred.make();
|
|
452
|
+
yield* lock.withPermits(1)(Effect3.gen(function* () {
|
|
453
|
+
if (closed)
|
|
454
|
+
return yield* scopeClosedError();
|
|
455
|
+
for (const write of writes) {
|
|
456
|
+
const superseded = held.get(write.address);
|
|
457
|
+
held.set(write.address, {
|
|
458
|
+
write,
|
|
459
|
+
attributes,
|
|
460
|
+
waiters: [...superseded?.waiters ?? [], waiter]
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const now = yield* Clock.currentTimeMillis;
|
|
464
|
+
openedAtMs ??= now;
|
|
465
|
+
if (!restartTimer)
|
|
466
|
+
return;
|
|
467
|
+
const delayMs = Math.max(0, Math.min(windowMs, openedAtMs + maxHoldMs - now));
|
|
468
|
+
const previous = timer;
|
|
469
|
+
const state = { id: nextTimerId++ };
|
|
470
|
+
timer = state;
|
|
471
|
+
const forked = yield* Effect3.forkIn(Effect3.sleep(Duration3.millis(delayMs)).pipe(Effect3.andThen(flushPending(state))), scope);
|
|
472
|
+
state.fiber = forked;
|
|
473
|
+
if (timer?.id !== state.id)
|
|
474
|
+
forked.interruptUnsafe();
|
|
475
|
+
previous?.fiber?.interruptUnsafe();
|
|
476
|
+
}));
|
|
477
|
+
return waiter;
|
|
478
|
+
});
|
|
479
|
+
const issueDirectly = (writes, attributes) => flushLock.withPermits(1)(options.flush(writes.map((write) => ({ address: write.address, value: write.value, attributes }))));
|
|
480
|
+
const writeAll = (writes, attributes) => {
|
|
481
|
+
if (writes.length === 0)
|
|
482
|
+
return Effect3.void;
|
|
483
|
+
return Effect3.andThen(ensureOpen, Effect3.andThen(validateWrites(writes), windowMs <= 0 ? issueDirectly(writes, attributes) : Effect3.flatMap(enqueue(writes, attributes, true), Deferred.await)));
|
|
484
|
+
};
|
|
485
|
+
const writeAllNow = (writes, attributes) => {
|
|
486
|
+
if (writes.length === 0)
|
|
487
|
+
return Effect3.void;
|
|
488
|
+
return Effect3.andThen(ensureOpen, Effect3.andThen(validateWrites(writes), windowMs <= 0 ? issueDirectly(writes, attributes) : Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
|
|
489
|
+
const waiter = yield* enqueue(writes, attributes, false);
|
|
490
|
+
yield* Effect3.forkIn(flushPending(), scope);
|
|
491
|
+
return yield* restore(Deferred.await(waiter));
|
|
492
|
+
}))));
|
|
493
|
+
};
|
|
494
|
+
const write = (write, attributes) => writeAll([write], attributes);
|
|
495
|
+
const writeNow = (write, attributes) => writeAllNow([write], attributes);
|
|
496
|
+
const flush = Effect3.uninterruptibleMask((restore) => Effect3.gen(function* () {
|
|
497
|
+
const fiber = yield* Effect3.forkIn(flushPending(), scope);
|
|
498
|
+
yield* restore(Fiber.await(fiber));
|
|
499
|
+
}));
|
|
500
|
+
yield* Effect3.addFinalizer(() => Effect3.andThen(Effect3.sync(() => {
|
|
501
|
+
closed = true;
|
|
502
|
+
}), lock.withPermits(1)(Effect3.sync(() => {
|
|
503
|
+
const abandoned = Array.from(held.values());
|
|
504
|
+
held.clear();
|
|
505
|
+
openedAtMs = undefined;
|
|
506
|
+
return abandoned;
|
|
507
|
+
})).pipe(Effect3.flatMap((abandoned) => abandoned.length === 0 ? Effect3.void : Effect3.logWarning(`Discarding ${abandoned.length} pending register write(s) at shutdown`).pipe(Effect3.andThen(Effect3.forEach(abandoned.flatMap((entry) => entry.waiters), Deferred.interrupt, { discard: true })))))));
|
|
508
|
+
return {
|
|
509
|
+
write,
|
|
510
|
+
writeNow,
|
|
511
|
+
writeAll,
|
|
512
|
+
writeAllNow,
|
|
513
|
+
flush,
|
|
514
|
+
get pending() {
|
|
515
|
+
return held.size;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
// src/read-debouncer.ts
|
|
520
|
+
import { Deferred as Deferred2, Duration as Duration4, Effect as Effect4, Exit, Fiber as Fiber2, Semaphore as Semaphore2 } from "effect";
|
|
521
|
+
var malformedResponse = (message) => new ModbusTransportError({ cause: new Error(message), message });
|
|
522
|
+
var shortResponse = (address) => malformedResponse(`Response did not carry a value for register ${address}`);
|
|
523
|
+
var settleWith = (value, address) => (waiter) => value === undefined ? Deferred2.fail(waiter, shortResponse(address)) : Deferred2.succeed(waiter, value);
|
|
524
|
+
var createReadDebouncer = (options) => Effect4.gen(function* () {
|
|
525
|
+
const windowMs = Duration4.toMillis(Duration4.fromInputUnsafe(options.window));
|
|
526
|
+
const scope = yield* Effect4.scope;
|
|
527
|
+
const lock = yield* Semaphore2.make(1);
|
|
528
|
+
const collected = new Map;
|
|
529
|
+
let timer;
|
|
530
|
+
let nextTimerId = 0;
|
|
531
|
+
let closed = false;
|
|
532
|
+
const scopeClosedError = () => {
|
|
533
|
+
const message = "The read debouncer scope has been closed";
|
|
534
|
+
return new ModbusNotConnectedError({ cause: new Error(message), message });
|
|
535
|
+
};
|
|
536
|
+
const ensureOpen = Effect4.suspend(() => closed ? Effect4.fail(scopeClosedError()) : Effect4.void);
|
|
537
|
+
const validateAddresses = (addresses) => Effect4.try({
|
|
538
|
+
try: () => {
|
|
539
|
+
for (const address of addresses) {
|
|
540
|
+
if (!Number.isInteger(address) || address < 0 || address > 65535) {
|
|
541
|
+
throw new RangeError(`address must be an integer from 0 to 65535, got ${address}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
catch: (cause) => {
|
|
546
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
547
|
+
return new ModbusInvalidArgumentError({ cause: error, message: error.message });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
const issue = (addresses) => Effect4.gen(function* () {
|
|
551
|
+
const plan = yield* Effect4.try({
|
|
552
|
+
try: () => planReads(addresses, options.plan),
|
|
553
|
+
catch: (cause) => {
|
|
554
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
555
|
+
return new ModbusInvalidArgumentError({ cause: error, message: error.message });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
const responses = yield* options.fetch(plan.spans);
|
|
559
|
+
if (responses.length !== plan.spans.length) {
|
|
560
|
+
return yield* malformedResponse(`Expected ${plan.spans.length} read response(s), received ${responses.length}`);
|
|
561
|
+
}
|
|
562
|
+
for (let index = 0;index < plan.spans.length; index += 1) {
|
|
563
|
+
const span = plan.spans[index];
|
|
564
|
+
const response = responses[index];
|
|
565
|
+
if (response.length !== span.quantity) {
|
|
566
|
+
return yield* malformedResponse(`Expected ${span.quantity} register(s) for span at address ${span.address}, ` + `received ${response.length}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return (address) => {
|
|
570
|
+
const location = plan.locate(address);
|
|
571
|
+
return location === undefined ? undefined : responses[location.span]?.[location.offset];
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
const issueDirectly = (addresses) => Effect4.flatMap(issue(addresses), (valueOf) => Effect4.forEach(addresses, (address) => {
|
|
575
|
+
const value = valueOf(address);
|
|
576
|
+
return value === undefined ? Effect4.fail(shortResponse(address)) : Effect4.succeed(value);
|
|
577
|
+
}));
|
|
578
|
+
const flushPending = (expectedTimer) => Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
|
|
579
|
+
const taken = yield* lock.withPermits(1)(Effect4.sync(() => {
|
|
580
|
+
if (expectedTimer !== undefined && timer?.id !== expectedTimer.id)
|
|
581
|
+
return;
|
|
582
|
+
const activeTimer = timer;
|
|
583
|
+
timer = undefined;
|
|
584
|
+
const batch = Array.from(collected.entries());
|
|
585
|
+
collected.clear();
|
|
586
|
+
return { activeTimer, batch };
|
|
587
|
+
}));
|
|
588
|
+
if (taken === undefined)
|
|
589
|
+
return;
|
|
590
|
+
if (expectedTimer === undefined)
|
|
591
|
+
taken.activeTimer?.fiber?.interruptUnsafe();
|
|
592
|
+
if (taken.batch.length === 0)
|
|
593
|
+
return;
|
|
594
|
+
const exit = yield* Effect4.exit(restore(issue(taken.batch.map(([address]) => address))));
|
|
595
|
+
yield* Effect4.forEach(taken.batch, ([address, waiters]) => {
|
|
596
|
+
const settle = Exit.isSuccess(exit) ? settleWith(exit.value(address), address) : (waiter) => Deferred2.failCause(waiter, exit.cause);
|
|
597
|
+
return Effect4.forEach(waiters, settle, { discard: true });
|
|
598
|
+
}, { discard: true });
|
|
599
|
+
}));
|
|
600
|
+
const enqueue = (addresses, startTimer) => Effect4.gen(function* () {
|
|
601
|
+
const waiters = yield* Effect4.forEach(addresses, () => Deferred2.make());
|
|
602
|
+
yield* lock.withPermits(1)(Effect4.gen(function* () {
|
|
603
|
+
if (closed)
|
|
604
|
+
return yield* scopeClosedError();
|
|
605
|
+
addresses.forEach((address, index) => {
|
|
606
|
+
const existing = collected.get(address);
|
|
607
|
+
if (existing === undefined)
|
|
608
|
+
collected.set(address, [waiters[index]]);
|
|
609
|
+
else
|
|
610
|
+
existing.push(waiters[index]);
|
|
611
|
+
});
|
|
612
|
+
}).pipe(Effect4.andThen(Effect4.suspend(() => {
|
|
613
|
+
if (!startTimer || timer !== undefined)
|
|
614
|
+
return Effect4.void;
|
|
615
|
+
const state = { id: nextTimerId++ };
|
|
616
|
+
timer = state;
|
|
617
|
+
return Effect4.map(Effect4.forkIn(Effect4.sleep(Duration4.millis(windowMs)).pipe(Effect4.andThen(flushPending(state))), scope), (forked) => {
|
|
618
|
+
state.fiber = forked;
|
|
619
|
+
if (timer?.id !== state.id)
|
|
620
|
+
forked.interruptUnsafe();
|
|
621
|
+
});
|
|
622
|
+
}))));
|
|
623
|
+
return waiters;
|
|
624
|
+
});
|
|
625
|
+
const readAll = (addresses) => {
|
|
626
|
+
if (addresses.length === 0)
|
|
627
|
+
return Effect4.succeed([]);
|
|
628
|
+
return Effect4.andThen(ensureOpen, Effect4.andThen(validateAddresses(addresses), windowMs <= 0 ? issueDirectly(addresses) : Effect4.flatMap(enqueue(addresses, true), (waiters) => Effect4.forEach(waiters, Deferred2.await))));
|
|
629
|
+
};
|
|
630
|
+
const readAllNow = (addresses) => {
|
|
631
|
+
if (addresses.length === 0)
|
|
632
|
+
return Effect4.succeed([]);
|
|
633
|
+
return Effect4.andThen(ensureOpen, Effect4.andThen(validateAddresses(addresses), windowMs <= 0 ? issueDirectly(addresses) : Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
|
|
634
|
+
const waiters = yield* enqueue(addresses, false);
|
|
635
|
+
yield* Effect4.forkIn(flushPending(), scope);
|
|
636
|
+
return yield* restore(Effect4.forEach(waiters, Deferred2.await));
|
|
637
|
+
}))));
|
|
638
|
+
};
|
|
639
|
+
const read = (address) => Effect4.map(readAll([address]), (values) => values[0]);
|
|
640
|
+
const readNow = (address) => Effect4.map(readAllNow([address]), (values) => values[0]);
|
|
641
|
+
const flush = Effect4.uninterruptibleMask((restore) => Effect4.gen(function* () {
|
|
642
|
+
const fiber = yield* Effect4.forkIn(flushPending(), scope);
|
|
643
|
+
yield* restore(Fiber2.await(fiber));
|
|
644
|
+
}));
|
|
645
|
+
yield* Effect4.addFinalizer(() => Effect4.andThen(Effect4.sync(() => {
|
|
646
|
+
closed = true;
|
|
647
|
+
}), lock.withPermits(1)(Effect4.sync(() => {
|
|
648
|
+
const abandoned = Array.from(collected.values()).flat();
|
|
649
|
+
collected.clear();
|
|
650
|
+
return abandoned;
|
|
651
|
+
})).pipe(Effect4.flatMap((abandoned) => abandoned.length === 0 ? Effect4.void : Effect4.logWarning(`Discarding ${abandoned.length} pending register read(s) at shutdown`).pipe(Effect4.andThen(Effect4.forEach(abandoned, Deferred2.interrupt, { discard: true })))))));
|
|
652
|
+
return {
|
|
653
|
+
read,
|
|
654
|
+
readNow,
|
|
655
|
+
readAll,
|
|
656
|
+
readAllNow,
|
|
657
|
+
flush,
|
|
658
|
+
get pending() {
|
|
659
|
+
return collected.size;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
// src/batching-client.ts
|
|
664
|
+
import {
|
|
665
|
+
Deferred as Deferred3,
|
|
666
|
+
Effect as Effect5,
|
|
667
|
+
Exit as Exit2,
|
|
668
|
+
Fiber as Fiber3,
|
|
669
|
+
Scope,
|
|
670
|
+
Semaphore as Semaphore3,
|
|
671
|
+
Stream,
|
|
672
|
+
SubscriptionRef as SubscriptionRef2
|
|
673
|
+
} from "effect";
|
|
674
|
+
|
|
675
|
+
// src/span-attributes.ts
|
|
676
|
+
var mergeSpanAttributes = (sources) => {
|
|
677
|
+
const collected = new Map;
|
|
678
|
+
for (const source of sources) {
|
|
679
|
+
if (source === undefined)
|
|
680
|
+
continue;
|
|
681
|
+
for (const [key, value] of Object.entries(source)) {
|
|
682
|
+
const entry = collected.get(key);
|
|
683
|
+
if (entry === undefined)
|
|
684
|
+
collected.set(key, { first: value, values: new Set([String(value)]) });
|
|
685
|
+
else
|
|
686
|
+
entry.values.add(String(value));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return Object.fromEntries(Array.from(collected, ([key, entry]) => [
|
|
690
|
+
key,
|
|
691
|
+
entry.values.size === 1 ? entry.first : Array.from(entry.values).join(",")
|
|
692
|
+
]));
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/batching-client.ts
|
|
696
|
+
var makeBatchingClient = (options) => Effect5.gen(function* () {
|
|
697
|
+
const { unitId, client, cache } = options;
|
|
698
|
+
const scope = yield* Effect5.scope;
|
|
699
|
+
const invalidArgument = (cause) => {
|
|
700
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
701
|
+
return new ModbusInvalidArgumentError({ cause: error, message: error.message });
|
|
702
|
+
};
|
|
703
|
+
const own = (registerCount, transactionCount, suppressedCount) => ({
|
|
704
|
+
"modbus.unit_ids": String(unitId),
|
|
705
|
+
"modbus.register_count": registerCount,
|
|
706
|
+
"modbus.transaction_count": transactionCount,
|
|
707
|
+
"modbus.suppressed_count": suppressedCount
|
|
708
|
+
});
|
|
709
|
+
const issueWrites = (batch) => Effect5.gen(function* () {
|
|
710
|
+
const proposed = batch.map((entry) => ({ address: entry.address, value: entry.value }));
|
|
711
|
+
const filtered = yield* Effect5.try({
|
|
712
|
+
try: () => cache?.filter(unitId, proposed),
|
|
713
|
+
catch: invalidArgument
|
|
714
|
+
});
|
|
715
|
+
const pending = filtered?.pending ?? proposed;
|
|
716
|
+
if (pending.length === 0)
|
|
717
|
+
return;
|
|
718
|
+
const plan = yield* Effect5.try({
|
|
719
|
+
try: () => {
|
|
720
|
+
const steps = planWrites(pending, options.plan?.writes);
|
|
721
|
+
if (pending.length === proposed.length)
|
|
722
|
+
return { steps, written: proposed };
|
|
723
|
+
const whole = planWrites(proposed, options.plan?.writes);
|
|
724
|
+
return whole.length < steps.length ? { steps: whole, written: proposed } : { steps, written: pending };
|
|
725
|
+
},
|
|
726
|
+
catch: invalidArgument
|
|
727
|
+
});
|
|
728
|
+
const { steps, written } = plan;
|
|
729
|
+
const carried = new Set(written.map((write) => write.address));
|
|
730
|
+
const suppressed = proposed.length - written.length;
|
|
731
|
+
yield* Effect5.forEach(steps, (step) => Effect5.suspend(() => {
|
|
732
|
+
const generation = cache?.generationOf(unitId);
|
|
733
|
+
return step.kind === "single" ? Effect5.tap(client.writeSingleRegister({ address: step.address, value: step.value }), () => Effect5.sync(() => cache?.observe(unitId, step.address, step.value, generation))) : Effect5.tap(client.writeMultipleRegisters({ address: step.address, values: step.values }), () => Effect5.sync(() => step.values.forEach((value, index) => cache?.observe(unitId, step.address + index, value, generation))));
|
|
734
|
+
}), { discard: true }).pipe(Effect5.tapError(() => Effect5.sync(() => cache?.invalidate(unitId))), Effect5.withSpan("modbus.write", {
|
|
735
|
+
kind: "client",
|
|
736
|
+
attributes: {
|
|
737
|
+
...mergeSpanAttributes(batch.filter((entry) => carried.has(entry.address)).map((entry) => entry.attributes)),
|
|
738
|
+
...own(written.length, steps.length, suppressed)
|
|
739
|
+
}
|
|
740
|
+
}));
|
|
741
|
+
});
|
|
742
|
+
const issueReads = (name, read) => (spans) => Effect5.forEach(spans, (span) => read({ address: span.address, quantity: span.quantity })).pipe(Effect5.withSpan(name, {
|
|
743
|
+
kind: "client",
|
|
744
|
+
attributes: own(spans.reduce((total, span) => total + span.quantity, 0), spans.length, 0)
|
|
745
|
+
}));
|
|
746
|
+
const writes = yield* createWriteDebouncer({
|
|
747
|
+
window: options.debounce?.writes?.window ?? 0,
|
|
748
|
+
maxHold: options.debounce?.writes?.maxHold,
|
|
749
|
+
flush: issueWrites
|
|
750
|
+
});
|
|
751
|
+
const holding = yield* createReadDebouncer({
|
|
752
|
+
window: options.debounce?.reads?.window ?? 0,
|
|
753
|
+
plan: options.plan?.reads,
|
|
754
|
+
fetch: issueReads("modbus.read", client.readHoldingRegisters)
|
|
755
|
+
});
|
|
756
|
+
const inputs = yield* createReadDebouncer({
|
|
757
|
+
window: options.debounce?.reads?.window ?? 0,
|
|
758
|
+
plan: options.plan?.reads,
|
|
759
|
+
fetch: issueReads("modbus.read", client.readInputRegisters)
|
|
760
|
+
});
|
|
761
|
+
const readerOf = (debouncer) => ({
|
|
762
|
+
read: debouncer.read,
|
|
763
|
+
readNow: debouncer.readNow,
|
|
764
|
+
readAll: debouncer.readAll,
|
|
765
|
+
readAllNow: debouncer.readAllNow
|
|
766
|
+
});
|
|
767
|
+
const flush = Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
|
|
768
|
+
const fiber = yield* Effect5.forkIn(Effect5.andThen(writes.flush, Effect5.andThen(holding.flush, inputs.flush)), scope);
|
|
769
|
+
yield* restore(Fiber3.await(fiber));
|
|
770
|
+
}));
|
|
771
|
+
const onShutdown = (action) => Effect5.addFinalizer(() => action.pipe(Effect5.tapError((error) => Effect5.logError(`Shutdown action for unit ${unitId} failed: ${error.message}`)), Effect5.orDie));
|
|
772
|
+
return {
|
|
773
|
+
unitId,
|
|
774
|
+
cache,
|
|
775
|
+
write: writes.write,
|
|
776
|
+
writeNow: writes.writeNow,
|
|
777
|
+
writeAll: writes.writeAll,
|
|
778
|
+
writeAllNow: writes.writeAllNow,
|
|
779
|
+
...readerOf(holding),
|
|
780
|
+
inputs: readerOf(inputs),
|
|
781
|
+
flush,
|
|
782
|
+
onShutdown
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
var createBatchingRegistry = (deps) => {
|
|
786
|
+
const clientScope = Scope.forkUnsafe(deps.scope);
|
|
787
|
+
const clients = new Map;
|
|
788
|
+
const creating = new Map;
|
|
789
|
+
const writeLocks = new Map;
|
|
790
|
+
let shared;
|
|
791
|
+
let closed = false;
|
|
792
|
+
let watchingScope = false;
|
|
793
|
+
const scopeClosedError = () => {
|
|
794
|
+
const message = "The batching registry scope has been closed";
|
|
795
|
+
return new ModbusNotConnectedError({ cause: new Error(message), message });
|
|
796
|
+
};
|
|
797
|
+
const watchScope = Effect5.suspend(() => {
|
|
798
|
+
if (watchingScope)
|
|
799
|
+
return Effect5.void;
|
|
800
|
+
watchingScope = true;
|
|
801
|
+
return Scope.addFinalizer(clientScope, Effect5.gen(function* () {
|
|
802
|
+
closed = true;
|
|
803
|
+
const pending = Array.from(creating.values());
|
|
804
|
+
creating.clear();
|
|
805
|
+
yield* Effect5.forEach(pending, (deferred) => Deferred3.fail(deferred, scopeClosedError()), {
|
|
806
|
+
discard: true
|
|
807
|
+
});
|
|
808
|
+
}));
|
|
809
|
+
});
|
|
810
|
+
const sharedCache = Effect5.fnUntraced(function* () {
|
|
811
|
+
if (shared)
|
|
812
|
+
return shared;
|
|
813
|
+
const cache = createRegisterCache();
|
|
814
|
+
shared = cache;
|
|
815
|
+
yield* Effect5.forkIn(Stream.runForEach(SubscriptionRef2.changes(deps.connectionState), (state) => Effect5.sync(() => {
|
|
816
|
+
if (!ConnectionState.$is("Connected")(state))
|
|
817
|
+
cache.invalidate();
|
|
818
|
+
})), clientScope);
|
|
819
|
+
return cache;
|
|
820
|
+
});
|
|
821
|
+
const writeLockFor = (unitId) => {
|
|
822
|
+
const existing = writeLocks.get(unitId);
|
|
823
|
+
if (existing)
|
|
824
|
+
return existing;
|
|
825
|
+
const created = Semaphore3.makeUnsafe(1);
|
|
826
|
+
writeLocks.set(unitId, created);
|
|
827
|
+
return created;
|
|
828
|
+
};
|
|
829
|
+
const guardRawWrites = (unitId, operations) => {
|
|
830
|
+
const write = (operation) => writeLockFor(unitId).withPermits(1)(Effect5.suspend(() => {
|
|
831
|
+
if (!clients.has(unitId) && !creating.has(unitId))
|
|
832
|
+
return operation();
|
|
833
|
+
const message = `Raw register writes are disabled for unit ${unitId} because a batching client exists. ` + `Use the batching client for every holding-register write on that unit.`;
|
|
834
|
+
return Effect5.fail(new ModbusInvalidArgumentError({ cause: new Error(message), message }));
|
|
835
|
+
}));
|
|
836
|
+
return {
|
|
837
|
+
...operations,
|
|
838
|
+
writeSingleRegister: (options) => write(() => operations.writeSingleRegister(options)),
|
|
839
|
+
writeMultipleRegisters: (options) => write(() => operations.writeMultipleRegisters(options)),
|
|
840
|
+
readWriteMultipleRegisters: (options) => write(() => operations.readWriteMultipleRegisters(options))
|
|
841
|
+
};
|
|
842
|
+
};
|
|
843
|
+
const alreadyDeclaredError = (unitId) => {
|
|
844
|
+
const message = `Unit ${unitId} already has a batching client. One unit has one batch, so a unit ` + `is declared once. Use batchingClient(${unitId}) to reach the existing one.`;
|
|
845
|
+
return new ModbusInvalidArgumentError({ cause: new Error(message), message });
|
|
846
|
+
};
|
|
847
|
+
const notDeclaredError = (unitId) => {
|
|
848
|
+
const message = `No batching client is declared for unit ${unitId}. Declare one with ` + `withBatchingClient(${unitId}) before reaching for it.`;
|
|
849
|
+
return new ModbusInvalidArgumentError({ cause: new Error(message), message });
|
|
850
|
+
};
|
|
851
|
+
return {
|
|
852
|
+
withBatchingClient: (unitId, options) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
|
|
853
|
+
yield* watchScope;
|
|
854
|
+
if (closed)
|
|
855
|
+
return yield* scopeClosedError();
|
|
856
|
+
const configuredCache = options?.cache;
|
|
857
|
+
const injected = configuredCache === undefined || configuredCache === true || configuredCache === false ? undefined : configuredCache;
|
|
858
|
+
const fresh = yield* Deferred3.make();
|
|
859
|
+
const election = yield* Effect5.sync(() => {
|
|
860
|
+
if (closed)
|
|
861
|
+
return { _tag: "Closed" };
|
|
862
|
+
if (clients.has(unitId) || creating.has(unitId)) {
|
|
863
|
+
return { _tag: "Declared" };
|
|
864
|
+
}
|
|
865
|
+
creating.set(unitId, fresh);
|
|
866
|
+
return { _tag: "Leader" };
|
|
867
|
+
});
|
|
868
|
+
if (election._tag === "Closed")
|
|
869
|
+
return yield* scopeClosedError();
|
|
870
|
+
if (election._tag === "Declared")
|
|
871
|
+
return yield* alreadyDeclaredError(unitId);
|
|
872
|
+
const build = writeLockFor(unitId).withPermits(1)(Effect5.gen(function* () {
|
|
873
|
+
const raw = yield* deps.withClient(unitId, { retry: options?.retry });
|
|
874
|
+
let cache;
|
|
875
|
+
if (options?.cache !== false)
|
|
876
|
+
cache = injected ?? (yield* sharedCache());
|
|
877
|
+
const client = yield* Scope.provide(makeBatchingClient({
|
|
878
|
+
unitId,
|
|
879
|
+
client: raw,
|
|
880
|
+
cache,
|
|
881
|
+
debounce: options?.debounce,
|
|
882
|
+
plan: options?.plan
|
|
883
|
+
}), clientScope);
|
|
884
|
+
return { cache, client };
|
|
885
|
+
}));
|
|
886
|
+
yield* Effect5.forkIn(Effect5.uninterruptible(Effect5.flatMap(Effect5.exit(Effect5.interruptible(build)), (exit) => Effect5.andThen(Effect5.sync(() => {
|
|
887
|
+
creating.delete(unitId);
|
|
888
|
+
if (Exit2.isSuccess(exit))
|
|
889
|
+
clients.set(unitId, exit.value.client);
|
|
890
|
+
}), Deferred3.done(fresh, Exit2.hasInterrupts(exit) ? Exit2.fail(scopeClosedError()) : Exit2.map(exit, ({ client }) => client))))), clientScope);
|
|
891
|
+
return yield* restore(Deferred3.await(fresh));
|
|
892
|
+
})),
|
|
893
|
+
batchingClient: (unitId) => Effect5.uninterruptibleMask((restore) => Effect5.gen(function* () {
|
|
894
|
+
if (closed)
|
|
895
|
+
return yield* scopeClosedError();
|
|
896
|
+
const existing = clients.get(unitId);
|
|
897
|
+
if (existing)
|
|
898
|
+
return existing;
|
|
899
|
+
const pending = creating.get(unitId);
|
|
900
|
+
if (pending)
|
|
901
|
+
return yield* restore(Deferred3.await(pending));
|
|
902
|
+
return yield* notDeclaredError(unitId);
|
|
903
|
+
})),
|
|
904
|
+
guardRawWrites
|
|
905
|
+
};
|
|
906
|
+
};
|
|
201
907
|
// src/AsciiTransportService.ts
|
|
202
908
|
import { Context, Layer } from "effect";
|
|
203
909
|
|
|
204
910
|
// src/mocks.ts
|
|
205
|
-
import { Effect as
|
|
911
|
+
import { Effect as Effect7, Exit as Exit3, Schema, Scope as Scope2, SubscriptionRef as SubscriptionRef3 } from "effect";
|
|
206
912
|
import {
|
|
207
913
|
CoilState
|
|
208
914
|
} from "modbus-rs";
|
|
209
915
|
|
|
210
916
|
// src/modbus-client.ts
|
|
211
|
-
import { Effect as
|
|
212
|
-
var wrap = (try_) =>
|
|
917
|
+
import { Effect as Effect6 } from "effect";
|
|
918
|
+
var wrap = (try_) => Effect6.tryPromise({
|
|
213
919
|
try: try_,
|
|
214
|
-
catch: (
|
|
920
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
215
921
|
});
|
|
216
|
-
var
|
|
922
|
+
var createEffectModbusClient = (client) => ({
|
|
217
923
|
readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
|
|
218
924
|
readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
|
|
219
925
|
writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
|
|
@@ -232,7 +938,7 @@ var makeEffectModbusClient = (client) => ({
|
|
|
232
938
|
});
|
|
233
939
|
var withResilience = (operations, resilience) => {
|
|
234
940
|
const run = (operation) => {
|
|
235
|
-
const attempt =
|
|
941
|
+
const attempt = Effect6.andThen(resilience.guard, Effect6.suspend(operation)).pipe(Effect6.tap(() => resilience.onSuccess ?? Effect6.void), Effect6.tapError((error) => resilience.report(error)));
|
|
236
942
|
return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
|
|
237
943
|
};
|
|
238
944
|
return {
|
|
@@ -270,10 +976,10 @@ var RegisterDefinition = Schema.Struct({
|
|
|
270
976
|
});
|
|
271
977
|
var SlaveDeviceDefinition = Schema.Struct({
|
|
272
978
|
unitId: Schema.Number,
|
|
273
|
-
coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(
|
|
274
|
-
discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(
|
|
275
|
-
holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(
|
|
276
|
-
inputRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(
|
|
979
|
+
coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
|
|
980
|
+
discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
|
|
981
|
+
holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([]))),
|
|
982
|
+
inputRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(Effect7.succeed([])))
|
|
277
983
|
});
|
|
278
984
|
var SlaveDeviceDefinitions = Schema.Array(SlaveDeviceDefinition);
|
|
279
985
|
var buildCoils = (defs) => {
|
|
@@ -301,8 +1007,8 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
|
|
|
301
1007
|
message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
|
|
302
1008
|
});
|
|
303
1009
|
var makeMockModbusClient = (state, unitId) => ({
|
|
304
|
-
readCoils:
|
|
305
|
-
yield*
|
|
1010
|
+
readCoils: Effect7.fnUntraced(function* (opts) {
|
|
1011
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
|
|
306
1012
|
if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
|
|
307
1013
|
return yield* failOutOfRange("Coil", opts.address, opts.quantity);
|
|
308
1014
|
}
|
|
@@ -312,8 +1018,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
312
1018
|
}
|
|
313
1019
|
return result;
|
|
314
1020
|
}),
|
|
315
|
-
readDiscreteInputs:
|
|
316
|
-
yield*
|
|
1021
|
+
readDiscreteInputs: Effect7.fnUntraced(function* (opts) {
|
|
1022
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
|
|
317
1023
|
if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
|
|
318
1024
|
return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
|
|
319
1025
|
}
|
|
@@ -323,8 +1029,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
323
1029
|
}
|
|
324
1030
|
return result;
|
|
325
1031
|
}),
|
|
326
|
-
readHoldingRegisters:
|
|
327
|
-
yield*
|
|
1032
|
+
readHoldingRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1033
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
|
|
328
1034
|
if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
|
|
329
1035
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
|
|
330
1036
|
}
|
|
@@ -334,8 +1040,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
334
1040
|
}
|
|
335
1041
|
return new Uint16Array(result);
|
|
336
1042
|
}),
|
|
337
|
-
readInputRegisters:
|
|
338
|
-
yield*
|
|
1043
|
+
readInputRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1044
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
|
|
339
1045
|
if (opts.address + opts.quantity > state.maxInputAddress + 1) {
|
|
340
1046
|
return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
|
|
341
1047
|
}
|
|
@@ -345,15 +1051,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
345
1051
|
}
|
|
346
1052
|
return new Uint16Array(result);
|
|
347
1053
|
}),
|
|
348
|
-
writeSingleCoil:
|
|
349
|
-
yield*
|
|
1054
|
+
writeSingleCoil: Effect7.fnUntraced(function* (opts) {
|
|
1055
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
|
|
350
1056
|
if (opts.address > state.maxCoilAddress) {
|
|
351
1057
|
return yield* failOutOfRange("Coil", opts.address);
|
|
352
1058
|
}
|
|
353
1059
|
state.coils.set(opts.address, opts.value === CoilState.On);
|
|
354
1060
|
}),
|
|
355
|
-
writeMultipleCoils:
|
|
356
|
-
yield*
|
|
1061
|
+
writeMultipleCoils: Effect7.fnUntraced(function* (opts) {
|
|
1062
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
|
|
357
1063
|
if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
|
|
358
1064
|
return yield* failOutOfRange("Coil", opts.address, opts.values.length);
|
|
359
1065
|
}
|
|
@@ -361,15 +1067,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
361
1067
|
state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
|
|
362
1068
|
}
|
|
363
1069
|
}),
|
|
364
|
-
writeSingleRegister:
|
|
365
|
-
yield*
|
|
1070
|
+
writeSingleRegister: Effect7.fnUntraced(function* (opts) {
|
|
1071
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
|
|
366
1072
|
if (opts.address > state.maxHoldingAddress) {
|
|
367
1073
|
return yield* failOutOfRange("HoldingRegister", opts.address);
|
|
368
1074
|
}
|
|
369
1075
|
state.holdingRegisters.set(opts.address, opts.value);
|
|
370
1076
|
}),
|
|
371
|
-
writeMultipleRegisters:
|
|
372
|
-
yield*
|
|
1077
|
+
writeMultipleRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1078
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
|
|
373
1079
|
if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
|
|
374
1080
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
|
|
375
1081
|
}
|
|
@@ -377,8 +1083,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
377
1083
|
state.holdingRegisters.set(opts.address + i, opts.values[i]);
|
|
378
1084
|
}
|
|
379
1085
|
}),
|
|
380
|
-
readWriteMultipleRegisters:
|
|
381
|
-
yield*
|
|
1086
|
+
readWriteMultipleRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1087
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
|
|
382
1088
|
if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
|
|
383
1089
|
return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
|
|
384
1090
|
}
|
|
@@ -394,34 +1100,34 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
394
1100
|
}
|
|
395
1101
|
return new Uint16Array(result);
|
|
396
1102
|
}),
|
|
397
|
-
readFifoQueue:
|
|
1103
|
+
readFifoQueue: Effect7.fnUntraced(function* (_opts) {
|
|
398
1104
|
return yield* new ModbusInvalidArgumentError({
|
|
399
1105
|
cause: new Error("FIFO queue not yet supported in mock"),
|
|
400
1106
|
message: "FIFO queue not yet supported in mock"
|
|
401
1107
|
});
|
|
402
1108
|
}),
|
|
403
|
-
readFileRecord:
|
|
1109
|
+
readFileRecord: Effect7.fnUntraced(function* (_opts) {
|
|
404
1110
|
return yield* new ModbusInvalidArgumentError({
|
|
405
1111
|
cause: new Error("File records not yet supported in mock"),
|
|
406
1112
|
message: "File records not yet supported in mock"
|
|
407
1113
|
});
|
|
408
1114
|
}),
|
|
409
|
-
writeFileRecord:
|
|
1115
|
+
writeFileRecord: Effect7.fnUntraced(function* (_opts) {
|
|
410
1116
|
return yield* new ModbusInvalidArgumentError({
|
|
411
1117
|
cause: new Error("File records not yet supported in mock"),
|
|
412
1118
|
message: "File records not yet supported in mock"
|
|
413
1119
|
});
|
|
414
1120
|
}),
|
|
415
|
-
readExceptionStatus:
|
|
416
|
-
yield*
|
|
1121
|
+
readExceptionStatus: Effect7.fnUntraced(function* () {
|
|
1122
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
|
|
417
1123
|
return 0;
|
|
418
1124
|
}),
|
|
419
|
-
diagnostics:
|
|
420
|
-
yield*
|
|
1125
|
+
diagnostics: Effect7.fnUntraced(function* (opts) {
|
|
1126
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
|
|
421
1127
|
return { subFunction: opts.subFunction, data: new Uint16Array };
|
|
422
1128
|
}),
|
|
423
|
-
readDeviceIdentification:
|
|
424
|
-
yield*
|
|
1129
|
+
readDeviceIdentification: Effect7.fnUntraced(function* (opts) {
|
|
1130
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
|
|
425
1131
|
return {
|
|
426
1132
|
conformityLevel: 1,
|
|
427
1133
|
moreFollows: false,
|
|
@@ -430,7 +1136,7 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
430
1136
|
};
|
|
431
1137
|
})
|
|
432
1138
|
});
|
|
433
|
-
var
|
|
1139
|
+
var createMockTransport = (devices) => {
|
|
434
1140
|
const deviceDefs = Schema.decodeUnknownSync(SlaveDeviceDefinitions)(devices);
|
|
435
1141
|
const deviceStates = new Map;
|
|
436
1142
|
for (const def of deviceDefs) {
|
|
@@ -449,94 +1155,168 @@ var makeMockTransport = (devices) => {
|
|
|
449
1155
|
maxInputAddress: input.maxAddress
|
|
450
1156
|
});
|
|
451
1157
|
}
|
|
452
|
-
return (options) =>
|
|
453
|
-
yield*
|
|
454
|
-
const connectionState = yield*
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
1158
|
+
return (options) => Effect7.gen(function* () {
|
|
1159
|
+
yield* Effect7.logDebug("Mock transport opened with devices:", deviceDefs);
|
|
1160
|
+
const connectionState = yield* SubscriptionRef3.make(ConnectionState.Connected());
|
|
1161
|
+
const resolvedReconnect = resolveReconnect(options.reconnect ?? {});
|
|
1162
|
+
const supervised = options.reconnect ? resolvedReconnect : null;
|
|
1163
|
+
const serviceScope = yield* Effect7.scope;
|
|
1164
|
+
let closed = false;
|
|
1165
|
+
const transportClosed = () => new ModbusNotConnectedError({
|
|
1166
|
+
cause: new Error("Mock transport has been closed"),
|
|
1167
|
+
message: "Mock transport has been closed"
|
|
1168
|
+
});
|
|
1169
|
+
const ensureOpen = Effect7.suspend(() => closed ? Effect7.fail(transportClosed()) : Effect7.void);
|
|
1170
|
+
const closeTransport = Effect7.suspend(() => {
|
|
1171
|
+
if (closed)
|
|
1172
|
+
return Effect7.void;
|
|
1173
|
+
closed = true;
|
|
1174
|
+
return Effect7.andThen(Effect7.logDebug("Mock: closing transport"), SubscriptionRef3.set(connectionState, ConnectionState.Disconnected()));
|
|
1175
|
+
});
|
|
1176
|
+
yield* Effect7.addFinalizer(() => closeTransport);
|
|
1177
|
+
const reconnectOnce = Effect7.andThen(ensureOpen, Effect7.andThen(Effect7.logDebug("Mock: reconnecting"), Effect7.suspend(() => {
|
|
458
1178
|
const injected = options.reconnectFault?.();
|
|
459
|
-
return injected ?
|
|
460
|
-
}));
|
|
1179
|
+
return injected ? Effect7.fail(injected) : Effect7.void;
|
|
1180
|
+
})));
|
|
461
1181
|
const report = (error) => {
|
|
462
|
-
if (
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
yield* Effect4.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
469
|
-
});
|
|
1182
|
+
if (closed || !resolvedReconnect.triggers(error))
|
|
1183
|
+
return Effect7.void;
|
|
1184
|
+
if (!supervised) {
|
|
1185
|
+
return SubscriptionRef3.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
|
|
1186
|
+
}
|
|
1187
|
+
return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope);
|
|
470
1188
|
};
|
|
471
|
-
const guard =
|
|
1189
|
+
const guard = Effect7.andThen(ensureOpen, Effect7.andThen(supervised ? guardCircuit(connectionState) : Effect7.void, Effect7.suspend(() => {
|
|
472
1190
|
const injected = options.fault?.();
|
|
473
|
-
return injected ?
|
|
474
|
-
}));
|
|
1191
|
+
return injected ? Effect7.fail(injected) : Effect7.void;
|
|
1192
|
+
})));
|
|
1193
|
+
const onSuccess = supervised ? undefined : SubscriptionRef3.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current);
|
|
1194
|
+
const touchedUnits = new Set;
|
|
1195
|
+
const makeOperations = Effect7.fnUntraced(function* (unitId) {
|
|
1196
|
+
yield* ensureOpen;
|
|
1197
|
+
const state = deviceStates.get(unitId);
|
|
1198
|
+
if (!state) {
|
|
1199
|
+
return yield* new ModbusInvalidArgumentError({
|
|
1200
|
+
cause: new Error(`Device with unitId ${unitId} not found in mock configuration`),
|
|
1201
|
+
message: `Device with unitId ${unitId} not found in mock configuration`
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
touchedUnits.add(unitId);
|
|
1205
|
+
return makeMockModbusClient(state, unitId);
|
|
1206
|
+
});
|
|
1207
|
+
const makeClient = Effect7.fnUntraced(function* (unitId, clientOptions) {
|
|
1208
|
+
const operations = yield* makeOperations(unitId);
|
|
1209
|
+
return withResilience(operations, {
|
|
1210
|
+
guard,
|
|
1211
|
+
onSuccess,
|
|
1212
|
+
report,
|
|
1213
|
+
policy: clientOptions?.retry ?? options.retry
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
const batching = createBatchingRegistry({
|
|
1217
|
+
withClient: makeClient,
|
|
1218
|
+
connectionState,
|
|
1219
|
+
scope: serviceScope
|
|
1220
|
+
});
|
|
1221
|
+
const withClient = Effect7.fnUntraced(function* (unitId, clientOptions) {
|
|
1222
|
+
const operations = yield* makeOperations(unitId);
|
|
1223
|
+
return withResilience(batching.guardRawWrites(unitId, operations), {
|
|
1224
|
+
guard,
|
|
1225
|
+
onSuccess,
|
|
1226
|
+
report,
|
|
1227
|
+
policy: clientOptions?.retry ?? options.retry
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
475
1230
|
return {
|
|
476
1231
|
connectionState,
|
|
477
|
-
withClient
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1232
|
+
withClient,
|
|
1233
|
+
withBatchingClient: (unitId, clientOptions) => Effect7.andThen(ensureOpen, batching.withBatchingClient(unitId, clientOptions)),
|
|
1234
|
+
batchingClient: (unitId) => Effect7.andThen(ensureOpen, batching.batchingClient(unitId)),
|
|
1235
|
+
get touchedUnits() {
|
|
1236
|
+
return new Set(touchedUnits);
|
|
1237
|
+
},
|
|
1238
|
+
setRequestTimeout: (_timeoutMs) => ensureOpen,
|
|
1239
|
+
clearRequestTimeout: () => ensureOpen,
|
|
1240
|
+
reconnect: () => Effect7.andThen(reconnectOnce, SubscriptionRef3.set(connectionState, ConnectionState.Connected())),
|
|
1241
|
+
close: () => Effect7.gen(function* () {
|
|
1242
|
+
if (closed)
|
|
1243
|
+
return;
|
|
1244
|
+
const scope = yield* Effect7.scope;
|
|
1245
|
+
yield* Scope2.close(scope, Exit3.void).pipe(Effect7.onExit(() => closeTransport));
|
|
490
1246
|
}),
|
|
491
|
-
setRequestTimeout: (_timeoutMs) => Effect4.void,
|
|
492
|
-
clearRequestTimeout: () => Effect4.void,
|
|
493
|
-
reconnect: () => Effect4.andThen(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
|
|
494
|
-
close: () => Effect4.andThen(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
|
|
495
1247
|
hasPendingRequests: () => false
|
|
496
1248
|
};
|
|
497
1249
|
});
|
|
498
1250
|
};
|
|
499
1251
|
|
|
500
1252
|
// src/shared-transport.ts
|
|
501
|
-
import { Deferred, Effect as
|
|
1253
|
+
import { Deferred as Deferred4, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
|
|
502
1254
|
var makeInFlight = () => Ref.make(Option.none());
|
|
503
|
-
var singleFlight = (inFlight, work) =>
|
|
504
|
-
const fresh = yield*
|
|
1255
|
+
var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
|
|
1256
|
+
const fresh = yield* Deferred4.make();
|
|
505
1257
|
const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
|
|
506
1258
|
onSome: (existing) => [[existing, false], current],
|
|
507
1259
|
onNone: () => [[fresh, true], Option.some(fresh)]
|
|
508
1260
|
}));
|
|
509
1261
|
if (isLeader) {
|
|
510
|
-
yield*
|
|
1262
|
+
yield* Effect8.forkDetach(Effect8.interruptible(work).pipe(Effect8.onExit((exit) => Effect8.andThen(Ref.set(inFlight, Option.none()), Deferred4.done(deferred, exit)))));
|
|
511
1263
|
}
|
|
512
|
-
return yield* restore(
|
|
1264
|
+
return yield* restore(Deferred4.await(deferred));
|
|
513
1265
|
}));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1266
|
+
var loadTransportConstructor = async (transportKey, moduleSpecifier) => {
|
|
1267
|
+
if (moduleSpecifier === "modbus-rs/web") {
|
|
1268
|
+
const mod = await import("modbus-rs/web");
|
|
1269
|
+
switch (transportKey) {
|
|
1270
|
+
case "WasmAsciiTransport":
|
|
1271
|
+
return mod.WasmAsciiTransport;
|
|
1272
|
+
case "WasmRtuTransport":
|
|
1273
|
+
return mod.WasmRtuTransport;
|
|
1274
|
+
case "WasmWsTransport":
|
|
1275
|
+
return mod.WasmWsTransport;
|
|
1276
|
+
default:
|
|
1277
|
+
throw new Error(`${transportKey} is not exported by modbus-rs/web`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const mod = await import("modbus-rs");
|
|
1281
|
+
switch (transportKey) {
|
|
1282
|
+
case "AsyncAsciiTransport":
|
|
1283
|
+
return mod.AsyncAsciiTransport;
|
|
1284
|
+
case "AsyncRtuTransport":
|
|
1285
|
+
return mod.AsyncRtuTransport;
|
|
1286
|
+
case "AsyncTcpTransport":
|
|
1287
|
+
return mod.AsyncTcpTransport;
|
|
1288
|
+
default:
|
|
1289
|
+
throw new Error(`${transportKey} is not exported by modbus-rs`);
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
function createTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
1293
|
+
return Effect8.fnUntraced(function* (options) {
|
|
1294
|
+
const { retry: transportRetry, reconnect: reconnectOptions } = options;
|
|
1295
|
+
const openOptions = { ...options };
|
|
1296
|
+
Reflect.deleteProperty(openOptions, "retry");
|
|
1297
|
+
Reflect.deleteProperty(openOptions, "reconnect");
|
|
1298
|
+
const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
|
|
520
1299
|
let transport = null;
|
|
521
1300
|
const opening = yield* makeInFlight();
|
|
522
1301
|
const reconnecting = yield* makeInFlight();
|
|
523
|
-
const connectionState = yield*
|
|
524
|
-
const
|
|
525
|
-
const
|
|
1302
|
+
const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
|
|
1303
|
+
const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
|
|
1304
|
+
const supervised = reconnectOptions ? resolvedReconnect : null;
|
|
1305
|
+
const serviceScope = yield* Effect8.scope;
|
|
526
1306
|
const clientSet = new Map;
|
|
527
1307
|
let closed = false;
|
|
528
1308
|
const transportClosed = () => new ModbusNotConnectedError({
|
|
529
1309
|
cause: new Error("Transport has been closed"),
|
|
530
1310
|
message: "Transport has been closed"
|
|
531
1311
|
});
|
|
532
|
-
const closeOrphan = (t) =>
|
|
533
|
-
const openTransport =
|
|
1312
|
+
const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
|
|
1313
|
+
const openTransport = Effect8.tryPromise({
|
|
534
1314
|
try: () => openMethod(TC, openOptions),
|
|
535
|
-
catch: (
|
|
536
|
-
}).pipe(
|
|
1315
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1316
|
+
}).pipe(Effect8.tap((t) => closed ? closeOrphan(t) : Effect8.andThen(Effect8.sync(() => {
|
|
537
1317
|
transport = t;
|
|
538
|
-
}),
|
|
539
|
-
const ensureOpen =
|
|
1318
|
+
}), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
|
|
1319
|
+
const ensureOpen = Effect8.fnUntraced(function* () {
|
|
540
1320
|
if (closed)
|
|
541
1321
|
return yield* transportClosed();
|
|
542
1322
|
if (transport)
|
|
@@ -546,58 +1326,77 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
546
1326
|
return yield* transportClosed();
|
|
547
1327
|
return t;
|
|
548
1328
|
});
|
|
549
|
-
|
|
1329
|
+
const closeTransport = Effect8.suspend(() => {
|
|
550
1330
|
if (closed)
|
|
551
|
-
return
|
|
1331
|
+
return Effect8.void;
|
|
552
1332
|
closed = true;
|
|
553
1333
|
const t = transport;
|
|
554
1334
|
if (!t)
|
|
555
|
-
return
|
|
556
|
-
return
|
|
1335
|
+
return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
|
|
1336
|
+
return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
|
|
1337
|
+
try: () => t.close(),
|
|
1338
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1339
|
+
}), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
|
|
557
1340
|
});
|
|
1341
|
+
yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
|
|
558
1342
|
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
559
|
-
const reconnectOnce =
|
|
1343
|
+
const reconnectOnce = Effect8.suspend(() => {
|
|
560
1344
|
const t = transport;
|
|
561
1345
|
if (closed || !t)
|
|
562
1346
|
return transportClosed();
|
|
563
|
-
return singleFlight(reconnecting,
|
|
1347
|
+
return singleFlight(reconnecting, Effect8.tryPromise({
|
|
564
1348
|
try: () => t.reconnect(),
|
|
565
|
-
catch: (
|
|
566
|
-
}).pipe(
|
|
1349
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1350
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
567
1351
|
});
|
|
568
1352
|
const report = (error) => {
|
|
569
|
-
if (
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
yield* Effect5.logDebug(`${serviceName}: reconnecting after ${error.message}`);
|
|
576
|
-
yield* Effect5.forkIn(superviseReconnect(reconnectOnce, connectionState, supervised), serviceScope);
|
|
577
|
-
});
|
|
1353
|
+
if (closed || !resolvedReconnect.triggers(error))
|
|
1354
|
+
return Effect8.void;
|
|
1355
|
+
if (!supervised) {
|
|
1356
|
+
return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
|
|
1357
|
+
}
|
|
1358
|
+
return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect8.logDebug(`${serviceName}: reconnecting after ${error.message}`));
|
|
578
1359
|
};
|
|
579
1360
|
const resilience = {
|
|
580
|
-
guard: supervised ? guardCircuit(connectionState) :
|
|
1361
|
+
guard: supervised ? guardCircuit(connectionState) : Effect8.void,
|
|
1362
|
+
onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
|
|
581
1363
|
report
|
|
582
1364
|
};
|
|
1365
|
+
const makeOperations = Effect8.fnUntraced(function* (unitId) {
|
|
1366
|
+
const t = yield* ensureOpen();
|
|
1367
|
+
let client = clientSet.get(unitId);
|
|
1368
|
+
if (!client) {
|
|
1369
|
+
client = yield* Effect8.try({
|
|
1370
|
+
try: () => t.createClient({ unitId }),
|
|
1371
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1372
|
+
});
|
|
1373
|
+
clientSet.set(unitId, client);
|
|
1374
|
+
}
|
|
1375
|
+
return createEffectModbusClient(client);
|
|
1376
|
+
});
|
|
1377
|
+
const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
|
|
1378
|
+
const operations = yield* makeOperations(unitId);
|
|
1379
|
+
return withResilience(operations, {
|
|
1380
|
+
...resilience,
|
|
1381
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
const batching = createBatchingRegistry({
|
|
1385
|
+
withClient: makeClient,
|
|
1386
|
+
connectionState,
|
|
1387
|
+
scope: serviceScope
|
|
1388
|
+
});
|
|
1389
|
+
const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
|
|
1390
|
+
const operations = yield* makeOperations(unitId);
|
|
1391
|
+
return withResilience(batching.guardRawWrites(unitId, operations), {
|
|
1392
|
+
...resilience,
|
|
1393
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
583
1396
|
return {
|
|
584
1397
|
connectionState,
|
|
585
|
-
withClient
|
|
586
|
-
|
|
587
|
-
let client = clientSet.get(unitId);
|
|
588
|
-
if (!client) {
|
|
589
|
-
client = yield* Effect5.try({
|
|
590
|
-
try: () => t.createClient({ unitId }),
|
|
591
|
-
catch: (error) => toModbusError(error)
|
|
592
|
-
});
|
|
593
|
-
clientSet.set(unitId, client);
|
|
594
|
-
}
|
|
595
|
-
return withResilience(makeEffectModbusClient(client), {
|
|
596
|
-
...resilience,
|
|
597
|
-
policy: clientOptions?.retry ?? transportRetry
|
|
598
|
-
});
|
|
599
|
-
}),
|
|
600
|
-
setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
|
|
1398
|
+
withClient,
|
|
1399
|
+
setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
|
|
601
1400
|
const t = transport;
|
|
602
1401
|
if (!t || closed) {
|
|
603
1402
|
return yield* new ModbusNotConnectedError({
|
|
@@ -607,7 +1406,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
607
1406
|
}
|
|
608
1407
|
t.setRequestTimeout(timeoutMs);
|
|
609
1408
|
}),
|
|
610
|
-
clearRequestTimeout:
|
|
1409
|
+
clearRequestTimeout: Effect8.fnUntraced(function* () {
|
|
611
1410
|
const t = transport;
|
|
612
1411
|
if (!t || closed) {
|
|
613
1412
|
return yield* new ModbusNotConnectedError({
|
|
@@ -617,7 +1416,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
617
1416
|
}
|
|
618
1417
|
t.clearRequestTimeout();
|
|
619
1418
|
}),
|
|
620
|
-
reconnect:
|
|
1419
|
+
reconnect: Effect8.fnUntraced(function* () {
|
|
621
1420
|
if (closed)
|
|
622
1421
|
return yield* transportClosed();
|
|
623
1422
|
const t = transport;
|
|
@@ -625,29 +1424,25 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
625
1424
|
yield* ensureOpen();
|
|
626
1425
|
return;
|
|
627
1426
|
}
|
|
628
|
-
yield* singleFlight(reconnecting,
|
|
1427
|
+
yield* singleFlight(reconnecting, Effect8.tryPromise({
|
|
629
1428
|
try: () => t.reconnect(),
|
|
630
|
-
catch: (
|
|
631
|
-
}).pipe(
|
|
1429
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1430
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
632
1431
|
if (closed)
|
|
633
1432
|
return yield* transportClosed();
|
|
634
|
-
yield*
|
|
1433
|
+
yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
|
|
635
1434
|
}),
|
|
636
|
-
close:
|
|
1435
|
+
close: Effect8.fnUntraced(function* () {
|
|
637
1436
|
if (closed)
|
|
638
1437
|
return;
|
|
639
|
-
|
|
640
|
-
yield*
|
|
641
|
-
const t = transport;
|
|
642
|
-
if (t) {
|
|
643
|
-
yield* Effect5.tryPromise({
|
|
644
|
-
try: () => t.close(),
|
|
645
|
-
catch: (error) => toModbusError(error)
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
const scope = yield* Effect5.scope;
|
|
649
|
-
yield* Scope.close(scope, Exit.void);
|
|
1438
|
+
const scope = yield* Effect8.scope;
|
|
1439
|
+
yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
|
|
650
1440
|
}),
|
|
1441
|
+
withBatchingClient: batching.withBatchingClient,
|
|
1442
|
+
batchingClient: batching.batchingClient,
|
|
1443
|
+
get touchedUnits() {
|
|
1444
|
+
return new Set(clientSet.keys());
|
|
1445
|
+
},
|
|
651
1446
|
hasPendingRequests: () => {
|
|
652
1447
|
if (closed)
|
|
653
1448
|
return false;
|
|
@@ -662,10 +1457,12 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
662
1457
|
|
|
663
1458
|
// src/AsciiTransportService.ts
|
|
664
1459
|
class AsciiTransportService extends Context.Service()("AsciiTransportService") {
|
|
665
|
-
static makeScoped =
|
|
1460
|
+
static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
|
|
1461
|
+
return transportConstructor.open(options);
|
|
1462
|
+
}, "AsciiTransportService");
|
|
666
1463
|
static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
|
|
667
1464
|
static makeMockTransport = (devices) => {
|
|
668
|
-
const factory =
|
|
1465
|
+
const factory = createMockTransport(devices);
|
|
669
1466
|
return (options) => Layer.effect(AsciiTransportService, factory(options));
|
|
670
1467
|
};
|
|
671
1468
|
}
|
|
@@ -675,10 +1472,12 @@ import { Context as Context3, Layer as Layer3 } from "effect";
|
|
|
675
1472
|
// src/RtuTransportService.ts
|
|
676
1473
|
import { Context as Context2, Layer as Layer2 } from "effect";
|
|
677
1474
|
class RtuTransportService extends Context2.Service()("RtuTransportService") {
|
|
678
|
-
static makeScoped =
|
|
1475
|
+
static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
|
|
1476
|
+
return transportConstructor.open(options);
|
|
1477
|
+
}, "RtuTransportService");
|
|
679
1478
|
static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
|
|
680
1479
|
static makeMockTransport = (devices) => {
|
|
681
|
-
const factory =
|
|
1480
|
+
const factory = createMockTransport(devices);
|
|
682
1481
|
return (options) => Layer2.effect(RtuTransportService, factory(options));
|
|
683
1482
|
};
|
|
684
1483
|
}
|
|
@@ -692,17 +1491,19 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
|
|
|
692
1491
|
return Layer3.flatMap(RtuTransportService.make(options), (context) => Layer3.succeed(SerialTransportService, Context3.get(context, RtuTransportService)));
|
|
693
1492
|
}
|
|
694
1493
|
static makeMockTransport = (devices) => {
|
|
695
|
-
const factory =
|
|
1494
|
+
const factory = createMockTransport(devices);
|
|
696
1495
|
return (options) => Layer3.effect(SerialTransportService, factory(options));
|
|
697
1496
|
};
|
|
698
1497
|
}
|
|
699
1498
|
// src/TcpTransportService.ts
|
|
700
1499
|
import { Context as Context4, Layer as Layer4 } from "effect";
|
|
701
1500
|
class TcpTransportService extends Context4.Service()("TcpTransportService") {
|
|
702
|
-
static makeScoped =
|
|
1501
|
+
static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
|
|
1502
|
+
return transportConstructor.connect(options);
|
|
1503
|
+
}, "TcpTransportService");
|
|
703
1504
|
static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
|
|
704
1505
|
static makeMockTransport = (devices) => {
|
|
705
|
-
const factory =
|
|
1506
|
+
const factory = createMockTransport(devices);
|
|
706
1507
|
return (options) => Layer4.effect(TcpTransportService, factory(options));
|
|
707
1508
|
};
|
|
708
1509
|
}
|
|
@@ -712,24 +1513,24 @@ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.g
|
|
|
712
1513
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
713
1514
|
const server = yield* Effect9.tryPromise({
|
|
714
1515
|
try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
|
|
715
|
-
catch: (error) => toModbusError(error)
|
|
1516
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
716
1517
|
});
|
|
717
1518
|
yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
|
|
718
1519
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
719
1520
|
try: () => server.shutdown(),
|
|
720
|
-
catch: (error) => toModbusError(error)
|
|
1521
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
721
1522
|
})), Effect9.catch(() => Effect9.void)));
|
|
722
1523
|
}));
|
|
723
1524
|
var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
|
|
724
1525
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
725
1526
|
const server = yield* Effect9.tryPromise({
|
|
726
1527
|
try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
|
|
727
|
-
catch: (error) => toModbusError(error)
|
|
1528
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
728
1529
|
});
|
|
729
1530
|
yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
|
|
730
1531
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
731
1532
|
try: () => server.shutdown(),
|
|
732
|
-
catch: (error) => toModbusError(error)
|
|
1533
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
733
1534
|
})), Effect9.catch(() => Effect9.void)));
|
|
734
1535
|
}));
|
|
735
1536
|
// src/TcpModbusServerService.ts
|
|
@@ -738,12 +1539,12 @@ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(fu
|
|
|
738
1539
|
const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
|
|
739
1540
|
const server = yield* Effect10.tryPromise({
|
|
740
1541
|
try: () => AsyncTcpModbusServer.bind(options, handlers),
|
|
741
|
-
catch: (error) => toModbusError(error)
|
|
1542
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
742
1543
|
});
|
|
743
1544
|
yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
|
|
744
1545
|
yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
|
|
745
1546
|
try: () => server.shutdown(),
|
|
746
|
-
catch: (error) => toModbusError(error)
|
|
1547
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
747
1548
|
})), Effect10.catch(() => Effect10.void)));
|
|
748
1549
|
}));
|
|
749
1550
|
// src/TcpGatewayService.ts
|
|
@@ -752,41 +1553,47 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.
|
|
|
752
1553
|
const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
|
|
753
1554
|
const gateway = yield* Effect11.tryPromise({
|
|
754
1555
|
try: () => AsyncTcpGateway.bind(options, gatewayConfig),
|
|
755
|
-
catch: (error) => toModbusError(error)
|
|
1556
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
756
1557
|
});
|
|
757
1558
|
yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
|
|
758
1559
|
yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
|
|
759
1560
|
try: () => gateway.shutdown(),
|
|
760
|
-
catch: (error) => toModbusError(error)
|
|
1561
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
761
1562
|
})), Effect11.catch(() => Effect11.void)));
|
|
762
1563
|
}));
|
|
763
1564
|
// src/WasmWsTransportService.ts
|
|
764
1565
|
import { Context as Context5, Layer as Layer8 } from "effect";
|
|
765
1566
|
class WasmWsTransportService extends Context5.Service()("WasmWsTransportService") {
|
|
766
|
-
static makeScoped =
|
|
1567
|
+
static makeScoped = createTransportScoped("WasmWsTransport", (transportConstructor, options) => {
|
|
1568
|
+
return transportConstructor.connect(options);
|
|
1569
|
+
}, "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
767
1570
|
static make = (options) => Layer8.effect(WasmWsTransportService, WasmWsTransportService.makeScoped(options));
|
|
768
1571
|
static makeMockTransport = (devices) => {
|
|
769
|
-
const factory =
|
|
1572
|
+
const factory = createMockTransport(devices);
|
|
770
1573
|
return (options) => Layer8.effect(WasmWsTransportService, factory(options));
|
|
771
1574
|
};
|
|
772
1575
|
}
|
|
773
1576
|
// src/WasmRtuTransportService.ts
|
|
774
1577
|
import { Context as Context6, Layer as Layer9 } from "effect";
|
|
775
1578
|
class WasmRtuTransportService extends Context6.Service()("WasmRtuTransportService") {
|
|
776
|
-
static makeScoped =
|
|
1579
|
+
static makeScoped = createTransportScoped("WasmRtuTransport", (transportConstructor, { port, ...rest }) => {
|
|
1580
|
+
return transportConstructor.open(port, rest);
|
|
1581
|
+
}, "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
777
1582
|
static make = (options) => Layer9.effect(WasmRtuTransportService, WasmRtuTransportService.makeScoped(options));
|
|
778
1583
|
static makeMockTransport = (devices) => {
|
|
779
|
-
const factory =
|
|
1584
|
+
const factory = createMockTransport(devices);
|
|
780
1585
|
return (options) => Layer9.effect(WasmRtuTransportService, factory(options));
|
|
781
1586
|
};
|
|
782
1587
|
}
|
|
783
1588
|
// src/WasmAsciiTransportService.ts
|
|
784
1589
|
import { Context as Context7, Layer as Layer10 } from "effect";
|
|
785
1590
|
class WasmAsciiTransportService extends Context7.Service()("WasmAsciiTransportService") {
|
|
786
|
-
static makeScoped =
|
|
1591
|
+
static makeScoped = createTransportScoped("WasmAsciiTransport", (transportConstructor, { port, ...rest }) => {
|
|
1592
|
+
return transportConstructor.open(port, rest);
|
|
1593
|
+
}, "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
787
1594
|
static make = (options) => Layer10.effect(WasmAsciiTransportService, WasmAsciiTransportService.makeScoped(options));
|
|
788
1595
|
static makeMockTransport = (devices) => {
|
|
789
|
-
const factory =
|
|
1596
|
+
const factory = createMockTransport(devices);
|
|
790
1597
|
return (options) => Layer10.effect(WasmAsciiTransportService, factory(options));
|
|
791
1598
|
};
|
|
792
1599
|
}
|
|
@@ -800,74 +1607,76 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
|
|
|
800
1607
|
return Layer11.flatMap(WasmRtuTransportService.make(options), (context) => Layer11.succeed(WasmSerialTransportService, Context8.get(context, WasmRtuTransportService)));
|
|
801
1608
|
}
|
|
802
1609
|
static makeMockTransport = (devices) => {
|
|
803
|
-
const factory =
|
|
1610
|
+
const factory = createMockTransport(devices);
|
|
804
1611
|
return (options) => Layer11.effect(WasmSerialTransportService, factory(options));
|
|
805
1612
|
};
|
|
806
1613
|
}
|
|
807
1614
|
// src/WasmSerialPort.ts
|
|
808
|
-
import { Effect as
|
|
809
|
-
var requestSerialPort = () =>
|
|
1615
|
+
import { Effect as Effect12 } from "effect";
|
|
1616
|
+
var requestSerialPort = () => Effect12.tryPromise({
|
|
810
1617
|
try: async () => {
|
|
811
1618
|
const mod = await import("modbus-rs/web");
|
|
812
1619
|
return mod.requestSerialPort();
|
|
813
1620
|
},
|
|
814
|
-
catch: (error) => toModbusError(error)
|
|
1621
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
815
1622
|
});
|
|
816
1623
|
// src/WasmTcpServerService.ts
|
|
817
|
-
import { Effect as
|
|
818
|
-
var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(
|
|
819
|
-
const { WasmWsModbusServer } = yield*
|
|
820
|
-
const server = yield*
|
|
1624
|
+
import { Effect as Effect13, Layer as Layer12 } from "effect";
|
|
1625
|
+
var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
|
|
1626
|
+
const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
|
|
1627
|
+
const server = yield* Effect13.tryPromise({
|
|
821
1628
|
try: () => WasmWsModbusServer.bind(options, handlers),
|
|
822
|
-
catch: (error) => toModbusError(error)
|
|
1629
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
823
1630
|
});
|
|
824
|
-
yield*
|
|
825
|
-
yield*
|
|
1631
|
+
yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
|
|
1632
|
+
yield* Effect13.forkScoped(Effect13.tryPromise({
|
|
826
1633
|
try: () => server.serve(),
|
|
827
|
-
catch: (error) => toModbusError(error)
|
|
828
|
-
}).pipe(
|
|
829
|
-
yield*
|
|
1634
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1635
|
+
}).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
|
|
1636
|
+
yield* Effect13.addFinalizer(() => Effect13.logDebug("WASM WS server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
|
|
830
1637
|
try: () => server.shutdown(),
|
|
831
|
-
catch: (error) => toModbusError(error)
|
|
832
|
-
})),
|
|
1638
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1639
|
+
})), Effect13.catch(() => Effect13.void)));
|
|
833
1640
|
}));
|
|
834
1641
|
// src/WasmSerialModbusServerService.ts
|
|
835
|
-
import { Effect as
|
|
836
|
-
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
837
|
-
const { WasmSerialModbusServer } = yield*
|
|
838
|
-
const server = yield*
|
|
1642
|
+
import { Effect as Effect14, Layer as Layer13 } from "effect";
|
|
1643
|
+
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
|
|
1644
|
+
const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
|
|
1645
|
+
const server = yield* Effect14.tryPromise({
|
|
839
1646
|
try: () => WasmSerialModbusServer.bindRtu(options, handlers),
|
|
840
|
-
catch: (error) => toModbusError(error)
|
|
1647
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
841
1648
|
});
|
|
842
|
-
yield*
|
|
843
|
-
yield*
|
|
1649
|
+
yield* Effect14.logDebug("WASM serial RTU server bound");
|
|
1650
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
844
1651
|
try: () => server.serve(),
|
|
845
|
-
catch: (error) => toModbusError(error)
|
|
846
|
-
}).pipe(
|
|
847
|
-
yield*
|
|
1652
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1653
|
+
}).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
|
|
1654
|
+
yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial RTU server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
|
|
848
1655
|
try: () => server.shutdown(),
|
|
849
|
-
catch: (error) => toModbusError(error)
|
|
850
|
-
})),
|
|
1656
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1657
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
851
1658
|
}));
|
|
852
|
-
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
853
|
-
const { WasmSerialModbusServer } = yield*
|
|
854
|
-
const server = yield*
|
|
1659
|
+
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
|
|
1660
|
+
const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
|
|
1661
|
+
const server = yield* Effect14.tryPromise({
|
|
855
1662
|
try: () => WasmSerialModbusServer.bindAscii(options, handlers),
|
|
856
|
-
catch: (error) => toModbusError(error)
|
|
1663
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
857
1664
|
});
|
|
858
|
-
yield*
|
|
859
|
-
yield*
|
|
1665
|
+
yield* Effect14.logDebug("WASM serial ASCII server bound");
|
|
1666
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
860
1667
|
try: () => server.serve(),
|
|
861
|
-
catch: (error) => toModbusError(error)
|
|
862
|
-
}).pipe(
|
|
863
|
-
yield*
|
|
1668
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1669
|
+
}).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
|
|
1670
|
+
yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial ASCII server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
|
|
864
1671
|
try: () => server.shutdown(),
|
|
865
|
-
catch: (error) => toModbusError(error)
|
|
866
|
-
})),
|
|
1672
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1673
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
867
1674
|
}));
|
|
868
1675
|
export {
|
|
869
1676
|
AsciiTransportService,
|
|
870
1677
|
ConnectionState,
|
|
1678
|
+
MODBUS_MAX_READ_REGISTERS,
|
|
1679
|
+
MODBUS_MAX_WRITE_REGISTERS,
|
|
871
1680
|
ModbusCircuitOpenError,
|
|
872
1681
|
ModbusConnectionClosedError,
|
|
873
1682
|
ModbusExceptionError,
|
|
@@ -884,7 +1693,16 @@ export {
|
|
|
884
1693
|
WasmRtuTransportService,
|
|
885
1694
|
WasmSerialTransportService,
|
|
886
1695
|
WasmWsTransportService,
|
|
887
|
-
|
|
1696
|
+
createBatchingRegistry,
|
|
1697
|
+
createReadDebouncer,
|
|
1698
|
+
createRegisterCache,
|
|
1699
|
+
createRetryPolicy,
|
|
1700
|
+
createWriteDebouncer,
|
|
1701
|
+
encodeRegisterValue,
|
|
1702
|
+
makeBatchingClient,
|
|
1703
|
+
mergeSpanAttributes,
|
|
1704
|
+
planReads,
|
|
1705
|
+
planWrites,
|
|
888
1706
|
requestSerialPort,
|
|
889
1707
|
retryModbus,
|
|
890
1708
|
retryableExceptionCodes,
|