@flux-control/effect-modbus-rs 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -20
- package/dist/index.d.ts +62 -1
- package/dist/index.js +1046 -216
- package/dist/src/AsciiTransportService.d.ts +14 -10
- package/dist/src/RtuTransportService.d.ts +14 -10
- package/dist/src/TcpTransportService.d.ts +14 -10
- package/dist/src/WasmAsciiTransportService.d.ts +15 -11
- package/dist/src/WasmRtuTransportService.d.ts +15 -11
- package/dist/src/WasmWsTransportService.d.ts +14 -10
- package/dist/src/batching-client.d.ts +197 -0
- package/dist/src/connection.d.ts +6 -3
- package/dist/src/errors.d.ts +3 -0
- 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 +79 -5
- package/dist/src/span-attributes.d.ts +34 -0
- package/dist/src/write-debouncer.d.ts +134 -0
- package/dist/test/retry.test.d.ts +1 -0
- package/dist/test/shared-transport.test.d.ts +1 -0
- package/dist/test/span-attributes.test.d.ts +1 -0
- package/dist/test/upstream-options.test.d.ts +1 -0
- package/dist/test/wasm-mocks.test.d.ts +1 -0
- package/dist/test/write-debouncer.test.d.ts +1 -0
- package/package.json +5 -4
- /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
- /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
- /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
- /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
- /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
- /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
- /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
- /package/dist/{src/wasm-mocks.test.d.ts → test/resilience.test.d.ts} +0 -0
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,
|
|
@@ -206,22 +218,708 @@ var claimReconnect = (reconnect, state, resolved, scope, onClaim) => Effect2.gen
|
|
|
206
218
|
yield* onClaim;
|
|
207
219
|
yield* Effect2.forkIn(superviseReconnect(reconnect, state, resolved), scope);
|
|
208
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
|
+
};
|
|
209
907
|
// src/AsciiTransportService.ts
|
|
210
908
|
import { Context, Layer } from "effect";
|
|
211
909
|
|
|
212
910
|
// src/mocks.ts
|
|
213
|
-
import { Effect as
|
|
911
|
+
import { Effect as Effect7, Exit as Exit3, Schema, Scope as Scope2, SubscriptionRef as SubscriptionRef3 } from "effect";
|
|
214
912
|
import {
|
|
215
913
|
CoilState
|
|
216
914
|
} from "modbus-rs";
|
|
217
915
|
|
|
218
916
|
// src/modbus-client.ts
|
|
219
|
-
import { Effect as
|
|
220
|
-
var wrap = (try_) =>
|
|
917
|
+
import { Effect as Effect6 } from "effect";
|
|
918
|
+
var wrap = (try_) => Effect6.tryPromise({
|
|
221
919
|
try: try_,
|
|
222
|
-
catch: (
|
|
920
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
223
921
|
});
|
|
224
|
-
var
|
|
922
|
+
var createEffectModbusClient = (client) => ({
|
|
225
923
|
readHoldingRegisters: (opts) => wrap(() => client.readHoldingRegisters(opts)),
|
|
226
924
|
readInputRegisters: (opts) => wrap(() => client.readInputRegisters(opts)),
|
|
227
925
|
writeSingleRegister: (opts) => wrap(() => client.writeSingleRegister(opts)),
|
|
@@ -240,7 +938,7 @@ var makeEffectModbusClient = (client) => ({
|
|
|
240
938
|
});
|
|
241
939
|
var withResilience = (operations, resilience) => {
|
|
242
940
|
const run = (operation) => {
|
|
243
|
-
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)));
|
|
244
942
|
return resilience.policy ? retryModbus(resilience.policy)(attempt) : attempt;
|
|
245
943
|
};
|
|
246
944
|
return {
|
|
@@ -278,10 +976,10 @@ var RegisterDefinition = Schema.Struct({
|
|
|
278
976
|
});
|
|
279
977
|
var SlaveDeviceDefinition = Schema.Struct({
|
|
280
978
|
unitId: Schema.Number,
|
|
281
|
-
coils: Schema.Array(CoilDefinition).pipe(Schema.withDecodingDefaultType(
|
|
282
|
-
discreteInputs: Schema.Array(DiscreteInputDefinition).pipe(Schema.withDecodingDefaultType(
|
|
283
|
-
holdingRegisters: Schema.Array(RegisterDefinition).pipe(Schema.withDecodingDefaultType(
|
|
284
|
-
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([])))
|
|
285
983
|
});
|
|
286
984
|
var SlaveDeviceDefinitions = Schema.Array(SlaveDeviceDefinition);
|
|
287
985
|
var buildCoils = (defs) => {
|
|
@@ -309,8 +1007,8 @@ var failOutOfRange = (label, address, quantity) => new ModbusInvalidArgumentErro
|
|
|
309
1007
|
message: quantity !== undefined ? `${label} read out of range: address=${address}, quantity=${quantity}` : `${label} write out of range: address=${address}`
|
|
310
1008
|
});
|
|
311
1009
|
var makeMockModbusClient = (state, unitId) => ({
|
|
312
|
-
readCoils:
|
|
313
|
-
yield*
|
|
1010
|
+
readCoils: Effect7.fnUntraced(function* (opts) {
|
|
1011
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readCoils`, opts);
|
|
314
1012
|
if (opts.address + opts.quantity > state.maxCoilAddress + 1) {
|
|
315
1013
|
return yield* failOutOfRange("Coil", opts.address, opts.quantity);
|
|
316
1014
|
}
|
|
@@ -320,8 +1018,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
320
1018
|
}
|
|
321
1019
|
return result;
|
|
322
1020
|
}),
|
|
323
|
-
readDiscreteInputs:
|
|
324
|
-
yield*
|
|
1021
|
+
readDiscreteInputs: Effect7.fnUntraced(function* (opts) {
|
|
1022
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDiscreteInputs`, opts);
|
|
325
1023
|
if (opts.address + opts.quantity > state.maxDiscreteAddress + 1) {
|
|
326
1024
|
return yield* failOutOfRange("DiscreteInput", opts.address, opts.quantity);
|
|
327
1025
|
}
|
|
@@ -331,8 +1029,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
331
1029
|
}
|
|
332
1030
|
return result;
|
|
333
1031
|
}),
|
|
334
|
-
readHoldingRegisters:
|
|
335
|
-
yield*
|
|
1032
|
+
readHoldingRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1033
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readHoldingRegisters`, opts);
|
|
336
1034
|
if (opts.address + opts.quantity > state.maxHoldingAddress + 1) {
|
|
337
1035
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.quantity);
|
|
338
1036
|
}
|
|
@@ -342,8 +1040,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
342
1040
|
}
|
|
343
1041
|
return new Uint16Array(result);
|
|
344
1042
|
}),
|
|
345
|
-
readInputRegisters:
|
|
346
|
-
yield*
|
|
1043
|
+
readInputRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1044
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readInputRegisters`, opts);
|
|
347
1045
|
if (opts.address + opts.quantity > state.maxInputAddress + 1) {
|
|
348
1046
|
return yield* failOutOfRange("InputRegister", opts.address, opts.quantity);
|
|
349
1047
|
}
|
|
@@ -353,15 +1051,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
353
1051
|
}
|
|
354
1052
|
return new Uint16Array(result);
|
|
355
1053
|
}),
|
|
356
|
-
writeSingleCoil:
|
|
357
|
-
yield*
|
|
1054
|
+
writeSingleCoil: Effect7.fnUntraced(function* (opts) {
|
|
1055
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleCoil`, opts);
|
|
358
1056
|
if (opts.address > state.maxCoilAddress) {
|
|
359
1057
|
return yield* failOutOfRange("Coil", opts.address);
|
|
360
1058
|
}
|
|
361
1059
|
state.coils.set(opts.address, opts.value === CoilState.On);
|
|
362
1060
|
}),
|
|
363
|
-
writeMultipleCoils:
|
|
364
|
-
yield*
|
|
1061
|
+
writeMultipleCoils: Effect7.fnUntraced(function* (opts) {
|
|
1062
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleCoils`, opts);
|
|
365
1063
|
if (opts.address + opts.values.length > state.maxCoilAddress + 1) {
|
|
366
1064
|
return yield* failOutOfRange("Coil", opts.address, opts.values.length);
|
|
367
1065
|
}
|
|
@@ -369,15 +1067,15 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
369
1067
|
state.coils.set(opts.address + i, opts.values[i] === CoilState.On);
|
|
370
1068
|
}
|
|
371
1069
|
}),
|
|
372
|
-
writeSingleRegister:
|
|
373
|
-
yield*
|
|
1070
|
+
writeSingleRegister: Effect7.fnUntraced(function* (opts) {
|
|
1071
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeSingleRegister`, opts);
|
|
374
1072
|
if (opts.address > state.maxHoldingAddress) {
|
|
375
1073
|
return yield* failOutOfRange("HoldingRegister", opts.address);
|
|
376
1074
|
}
|
|
377
1075
|
state.holdingRegisters.set(opts.address, opts.value);
|
|
378
1076
|
}),
|
|
379
|
-
writeMultipleRegisters:
|
|
380
|
-
yield*
|
|
1077
|
+
writeMultipleRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1078
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} writeMultipleRegisters`, opts);
|
|
381
1079
|
if (opts.address + opts.values.length > state.maxHoldingAddress + 1) {
|
|
382
1080
|
return yield* failOutOfRange("HoldingRegister", opts.address, opts.values.length);
|
|
383
1081
|
}
|
|
@@ -385,8 +1083,8 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
385
1083
|
state.holdingRegisters.set(opts.address + i, opts.values[i]);
|
|
386
1084
|
}
|
|
387
1085
|
}),
|
|
388
|
-
readWriteMultipleRegisters:
|
|
389
|
-
yield*
|
|
1086
|
+
readWriteMultipleRegisters: Effect7.fnUntraced(function* (opts) {
|
|
1087
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readWriteMultipleRegisters`, opts);
|
|
390
1088
|
if (opts.writeAddress + opts.writeValues.length > state.maxHoldingAddress + 1) {
|
|
391
1089
|
return yield* failOutOfRange("HoldingRegister", opts.writeAddress, opts.writeValues.length);
|
|
392
1090
|
}
|
|
@@ -402,34 +1100,34 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
402
1100
|
}
|
|
403
1101
|
return new Uint16Array(result);
|
|
404
1102
|
}),
|
|
405
|
-
readFifoQueue:
|
|
1103
|
+
readFifoQueue: Effect7.fnUntraced(function* (_opts) {
|
|
406
1104
|
return yield* new ModbusInvalidArgumentError({
|
|
407
1105
|
cause: new Error("FIFO queue not yet supported in mock"),
|
|
408
1106
|
message: "FIFO queue not yet supported in mock"
|
|
409
1107
|
});
|
|
410
1108
|
}),
|
|
411
|
-
readFileRecord:
|
|
1109
|
+
readFileRecord: Effect7.fnUntraced(function* (_opts) {
|
|
412
1110
|
return yield* new ModbusInvalidArgumentError({
|
|
413
1111
|
cause: new Error("File records not yet supported in mock"),
|
|
414
1112
|
message: "File records not yet supported in mock"
|
|
415
1113
|
});
|
|
416
1114
|
}),
|
|
417
|
-
writeFileRecord:
|
|
1115
|
+
writeFileRecord: Effect7.fnUntraced(function* (_opts) {
|
|
418
1116
|
return yield* new ModbusInvalidArgumentError({
|
|
419
1117
|
cause: new Error("File records not yet supported in mock"),
|
|
420
1118
|
message: "File records not yet supported in mock"
|
|
421
1119
|
});
|
|
422
1120
|
}),
|
|
423
|
-
readExceptionStatus:
|
|
424
|
-
yield*
|
|
1121
|
+
readExceptionStatus: Effect7.fnUntraced(function* () {
|
|
1122
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readExceptionStatus`);
|
|
425
1123
|
return 0;
|
|
426
1124
|
}),
|
|
427
|
-
diagnostics:
|
|
428
|
-
yield*
|
|
1125
|
+
diagnostics: Effect7.fnUntraced(function* (opts) {
|
|
1126
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} diagnostics`, opts);
|
|
429
1127
|
return { subFunction: opts.subFunction, data: new Uint16Array };
|
|
430
1128
|
}),
|
|
431
|
-
readDeviceIdentification:
|
|
432
|
-
yield*
|
|
1129
|
+
readDeviceIdentification: Effect7.fnUntraced(function* (opts) {
|
|
1130
|
+
yield* Effect7.logDebug(`[Mock] unitId=${unitId} readDeviceIdentification`, opts);
|
|
433
1131
|
return {
|
|
434
1132
|
conformityLevel: 1,
|
|
435
1133
|
moreFollows: false,
|
|
@@ -438,7 +1136,7 @@ var makeMockModbusClient = (state, unitId) => ({
|
|
|
438
1136
|
};
|
|
439
1137
|
})
|
|
440
1138
|
});
|
|
441
|
-
var
|
|
1139
|
+
var createMockTransport = (devices) => {
|
|
442
1140
|
const deviceDefs = Schema.decodeUnknownSync(SlaveDeviceDefinitions)(devices);
|
|
443
1141
|
const deviceStates = new Map;
|
|
444
1142
|
for (const def of deviceDefs) {
|
|
@@ -457,144 +1155,260 @@ var makeMockTransport = (devices) => {
|
|
|
457
1155
|
maxInputAddress: input.maxAddress
|
|
458
1156
|
});
|
|
459
1157
|
}
|
|
460
|
-
return (options) =>
|
|
461
|
-
yield*
|
|
462
|
-
const connectionState = yield*
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
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(() => {
|
|
466
1178
|
const injected = options.reconnectFault?.();
|
|
467
|
-
return injected ?
|
|
468
|
-
}));
|
|
1179
|
+
return injected ? Effect7.fail(injected) : Effect7.void;
|
|
1180
|
+
})));
|
|
469
1181
|
const report = (error) => {
|
|
470
|
-
if (
|
|
471
|
-
return
|
|
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
|
+
}
|
|
472
1187
|
return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope);
|
|
473
1188
|
};
|
|
474
|
-
const guard =
|
|
1189
|
+
const guard = Effect7.andThen(ensureOpen, Effect7.andThen(supervised ? guardCircuit(connectionState) : Effect7.void, Effect7.suspend(() => {
|
|
475
1190
|
const injected = options.fault?.();
|
|
476
|
-
return injected ?
|
|
477
|
-
}));
|
|
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
|
+
});
|
|
478
1230
|
return {
|
|
479
1231
|
connectionState,
|
|
480
|
-
withClient
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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));
|
|
493
1246
|
}),
|
|
494
|
-
setRequestTimeout: (_timeoutMs) => Effect4.void,
|
|
495
|
-
clearRequestTimeout: () => Effect4.void,
|
|
496
|
-
reconnect: () => Effect4.andThen(reconnectOnce, SubscriptionRef2.set(connectionState, ConnectionState.Connected())),
|
|
497
|
-
close: () => Effect4.andThen(Effect4.logDebug("Mock: closing transport"), SubscriptionRef2.set(connectionState, ConnectionState.Disconnected())),
|
|
498
1247
|
hasPendingRequests: () => false
|
|
499
1248
|
};
|
|
500
1249
|
});
|
|
501
1250
|
};
|
|
502
1251
|
|
|
503
1252
|
// src/shared-transport.ts
|
|
504
|
-
import { Deferred, Effect as
|
|
1253
|
+
import { Deferred as Deferred4, Duration as Duration6, Effect as Effect8, Exit as Exit4, Option, Ref, Scope as Scope3, SubscriptionRef as SubscriptionRef4 } from "effect";
|
|
505
1254
|
var makeInFlight = () => Ref.make(Option.none());
|
|
506
|
-
var singleFlight = (inFlight, work) =>
|
|
507
|
-
const fresh = yield*
|
|
1255
|
+
var singleFlight = (inFlight, work) => Effect8.uninterruptibleMask((restore) => Effect8.gen(function* () {
|
|
1256
|
+
const fresh = yield* Deferred4.make();
|
|
508
1257
|
const [deferred, isLeader] = yield* Ref.modify(inFlight, (current) => Option.match(current, {
|
|
509
1258
|
onSome: (existing) => [[existing, false], current],
|
|
510
1259
|
onNone: () => [[fresh, true], Option.some(fresh)]
|
|
511
1260
|
}));
|
|
512
1261
|
if (isLeader) {
|
|
513
|
-
yield*
|
|
1262
|
+
yield* Effect8.forkDetach(Effect8.interruptible(work).pipe(Effect8.onExit((exit) => Effect8.andThen(Ref.set(inFlight, Option.none()), Deferred4.done(deferred, exit)))));
|
|
514
1263
|
}
|
|
515
|
-
return yield* restore(
|
|
1264
|
+
return yield* restore(Deferred4.await(deferred));
|
|
516
1265
|
}));
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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 connectTimeout = options.connectTimeout === undefined ? undefined : Duration6.fromInputUnsafe(options.connectTimeout);
|
|
1296
|
+
const openOptions = { ...options };
|
|
1297
|
+
Reflect.deleteProperty(openOptions, "retry");
|
|
1298
|
+
Reflect.deleteProperty(openOptions, "reconnect");
|
|
1299
|
+
Reflect.deleteProperty(openOptions, "connectTimeout");
|
|
1300
|
+
const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
|
|
523
1301
|
let transport = null;
|
|
524
1302
|
const opening = yield* makeInFlight();
|
|
525
1303
|
const reconnecting = yield* makeInFlight();
|
|
526
|
-
const connectionState = yield*
|
|
527
|
-
const
|
|
528
|
-
const
|
|
1304
|
+
const connectionState = yield* SubscriptionRef4.make(ConnectionState.Disconnected());
|
|
1305
|
+
const resolvedReconnect = resolveReconnect(reconnectOptions ?? {});
|
|
1306
|
+
const supervised = reconnectOptions ? resolvedReconnect : null;
|
|
1307
|
+
const serviceScope = yield* Effect8.scope;
|
|
529
1308
|
const clientSet = new Map;
|
|
530
1309
|
let closed = false;
|
|
531
1310
|
const transportClosed = () => new ModbusNotConnectedError({
|
|
532
1311
|
cause: new Error("Transport has been closed"),
|
|
533
1312
|
message: "Transport has been closed"
|
|
534
1313
|
});
|
|
535
|
-
const closeOrphan = (t) =>
|
|
536
|
-
const
|
|
1314
|
+
const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
|
|
1315
|
+
const limitConnect = (wait) => {
|
|
1316
|
+
if (connectTimeout === undefined)
|
|
1317
|
+
return wait;
|
|
1318
|
+
const message = `${serviceName}: connect did not complete within ${Duration6.format(connectTimeout)}`;
|
|
1319
|
+
return Effect8.timeoutOrElse(wait, {
|
|
1320
|
+
duration: connectTimeout,
|
|
1321
|
+
orElse: () => Effect8.fail(new ModbusTimeoutError({ cause: new Error(message), message }))
|
|
1322
|
+
});
|
|
1323
|
+
};
|
|
1324
|
+
const openTransport = Effect8.tryPromise({
|
|
537
1325
|
try: () => openMethod(TC, openOptions),
|
|
538
|
-
catch: (
|
|
539
|
-
}).pipe(
|
|
1326
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1327
|
+
}).pipe(Effect8.tap((t) => closed ? closeOrphan(t) : Effect8.andThen(Effect8.sync(() => {
|
|
540
1328
|
transport = t;
|
|
541
|
-
}),
|
|
542
|
-
const ensureOpen =
|
|
1329
|
+
}), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
|
|
1330
|
+
const ensureOpen = Effect8.fnUntraced(function* () {
|
|
543
1331
|
if (closed)
|
|
544
1332
|
return yield* transportClosed();
|
|
545
1333
|
if (transport)
|
|
546
1334
|
return transport;
|
|
547
|
-
const t = yield* singleFlight(opening, openTransport);
|
|
1335
|
+
const t = yield* limitConnect(singleFlight(opening, openTransport));
|
|
548
1336
|
if (closed)
|
|
549
1337
|
return yield* transportClosed();
|
|
550
1338
|
return t;
|
|
551
1339
|
});
|
|
552
|
-
|
|
1340
|
+
const closeTransport = Effect8.suspend(() => {
|
|
553
1341
|
if (closed)
|
|
554
|
-
return
|
|
1342
|
+
return Effect8.void;
|
|
555
1343
|
closed = true;
|
|
556
1344
|
const t = transport;
|
|
557
1345
|
if (!t)
|
|
558
|
-
return
|
|
559
|
-
return
|
|
1346
|
+
return SubscriptionRef4.set(connectionState, ConnectionState.Disconnected());
|
|
1347
|
+
return Effect8.andThen(Effect8.logDebug(`Closing ${serviceName}`), Effect8.andThen(Effect8.tryPromise({
|
|
1348
|
+
try: () => t.close(),
|
|
1349
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1350
|
+
}), SubscriptionRef4.set(connectionState, ConnectionState.Disconnected())));
|
|
560
1351
|
});
|
|
1352
|
+
yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
|
|
561
1353
|
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
562
|
-
const
|
|
1354
|
+
const reconnectTransport = (t) => limitConnect(singleFlight(reconnecting, Effect8.tryPromise({
|
|
1355
|
+
try: () => t.reconnect(),
|
|
1356
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1357
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current)))));
|
|
1358
|
+
const reconnectOnce = Effect8.suspend(() => {
|
|
563
1359
|
const t = transport;
|
|
564
1360
|
if (closed || !t)
|
|
565
1361
|
return transportClosed();
|
|
566
|
-
return
|
|
567
|
-
try: () => t.reconnect(),
|
|
568
|
-
catch: (error) => toModbusError(error)
|
|
569
|
-
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
1362
|
+
return reconnectTransport(t);
|
|
570
1363
|
});
|
|
571
1364
|
const report = (error) => {
|
|
572
|
-
if (
|
|
573
|
-
return
|
|
574
|
-
|
|
1365
|
+
if (closed || !resolvedReconnect.triggers(error))
|
|
1366
|
+
return Effect8.void;
|
|
1367
|
+
if (!supervised) {
|
|
1368
|
+
return SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Connected")(current) ? ConnectionState.Down({ cause: error }) : current);
|
|
1369
|
+
}
|
|
1370
|
+
return claimReconnect(reconnectOnce, connectionState, supervised, serviceScope, Effect8.logDebug(`${serviceName}: reconnecting after ${error.message}`));
|
|
575
1371
|
};
|
|
576
1372
|
const resilience = {
|
|
577
|
-
guard: supervised ? guardCircuit(connectionState) :
|
|
1373
|
+
guard: supervised ? guardCircuit(connectionState) : Effect8.void,
|
|
1374
|
+
onSuccess: supervised ? undefined : SubscriptionRef4.update(connectionState, (current) => ConnectionState.$is("Down")(current) ? ConnectionState.Connected() : current),
|
|
578
1375
|
report
|
|
579
1376
|
};
|
|
1377
|
+
const makeOperations = Effect8.fnUntraced(function* (unitId) {
|
|
1378
|
+
const t = yield* ensureOpen();
|
|
1379
|
+
let client = clientSet.get(unitId);
|
|
1380
|
+
if (!client) {
|
|
1381
|
+
client = yield* Effect8.try({
|
|
1382
|
+
try: () => t.createClient({ unitId }),
|
|
1383
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1384
|
+
});
|
|
1385
|
+
clientSet.set(unitId, client);
|
|
1386
|
+
}
|
|
1387
|
+
return createEffectModbusClient(client);
|
|
1388
|
+
});
|
|
1389
|
+
const makeClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
|
|
1390
|
+
const operations = yield* makeOperations(unitId);
|
|
1391
|
+
return withResilience(operations, {
|
|
1392
|
+
...resilience,
|
|
1393
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
const batching = createBatchingRegistry({
|
|
1397
|
+
withClient: makeClient,
|
|
1398
|
+
connectionState,
|
|
1399
|
+
scope: serviceScope
|
|
1400
|
+
});
|
|
1401
|
+
const withClient = Effect8.fnUntraced(function* (unitId, clientOptions) {
|
|
1402
|
+
const operations = yield* makeOperations(unitId);
|
|
1403
|
+
return withResilience(batching.guardRawWrites(unitId, operations), {
|
|
1404
|
+
...resilience,
|
|
1405
|
+
policy: clientOptions?.retry ?? transportRetry
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
580
1408
|
return {
|
|
581
1409
|
connectionState,
|
|
582
|
-
withClient
|
|
583
|
-
|
|
584
|
-
let client = clientSet.get(unitId);
|
|
585
|
-
if (!client) {
|
|
586
|
-
client = yield* Effect5.try({
|
|
587
|
-
try: () => t.createClient({ unitId }),
|
|
588
|
-
catch: (error) => toModbusError(error)
|
|
589
|
-
});
|
|
590
|
-
clientSet.set(unitId, client);
|
|
591
|
-
}
|
|
592
|
-
return withResilience(makeEffectModbusClient(client), {
|
|
593
|
-
...resilience,
|
|
594
|
-
policy: clientOptions?.retry ?? transportRetry
|
|
595
|
-
});
|
|
596
|
-
}),
|
|
597
|
-
setRequestTimeout: Effect5.fnUntraced(function* (timeoutMs) {
|
|
1410
|
+
withClient,
|
|
1411
|
+
setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
|
|
598
1412
|
const t = transport;
|
|
599
1413
|
if (!t || closed) {
|
|
600
1414
|
return yield* new ModbusNotConnectedError({
|
|
@@ -604,7 +1418,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
604
1418
|
}
|
|
605
1419
|
t.setRequestTimeout(timeoutMs);
|
|
606
1420
|
}),
|
|
607
|
-
clearRequestTimeout:
|
|
1421
|
+
clearRequestTimeout: Effect8.fnUntraced(function* () {
|
|
608
1422
|
const t = transport;
|
|
609
1423
|
if (!t || closed) {
|
|
610
1424
|
return yield* new ModbusNotConnectedError({
|
|
@@ -614,7 +1428,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
614
1428
|
}
|
|
615
1429
|
t.clearRequestTimeout();
|
|
616
1430
|
}),
|
|
617
|
-
reconnect:
|
|
1431
|
+
reconnect: Effect8.fnUntraced(function* () {
|
|
618
1432
|
if (closed)
|
|
619
1433
|
return yield* transportClosed();
|
|
620
1434
|
const t = transport;
|
|
@@ -622,29 +1436,22 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
622
1436
|
yield* ensureOpen();
|
|
623
1437
|
return;
|
|
624
1438
|
}
|
|
625
|
-
yield*
|
|
626
|
-
try: () => t.reconnect(),
|
|
627
|
-
catch: (error) => toModbusError(error)
|
|
628
|
-
}).pipe(Effect5.tap(() => closed ? closeOrphan(t) : Effect5.void)));
|
|
1439
|
+
yield* reconnectTransport(t);
|
|
629
1440
|
if (closed)
|
|
630
1441
|
return yield* transportClosed();
|
|
631
|
-
yield*
|
|
1442
|
+
yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
|
|
632
1443
|
}),
|
|
633
|
-
close:
|
|
1444
|
+
close: Effect8.fnUntraced(function* () {
|
|
634
1445
|
if (closed)
|
|
635
1446
|
return;
|
|
636
|
-
|
|
637
|
-
yield*
|
|
638
|
-
const t = transport;
|
|
639
|
-
if (t) {
|
|
640
|
-
yield* Effect5.tryPromise({
|
|
641
|
-
try: () => t.close(),
|
|
642
|
-
catch: (error) => toModbusError(error)
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
const scope = yield* Effect5.scope;
|
|
646
|
-
yield* Scope.close(scope, Exit.void);
|
|
1447
|
+
const scope = yield* Effect8.scope;
|
|
1448
|
+
yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
|
|
647
1449
|
}),
|
|
1450
|
+
withBatchingClient: batching.withBatchingClient,
|
|
1451
|
+
batchingClient: batching.batchingClient,
|
|
1452
|
+
get touchedUnits() {
|
|
1453
|
+
return new Set(clientSet.keys());
|
|
1454
|
+
},
|
|
648
1455
|
hasPendingRequests: () => {
|
|
649
1456
|
if (closed)
|
|
650
1457
|
return false;
|
|
@@ -659,10 +1466,12 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
659
1466
|
|
|
660
1467
|
// src/AsciiTransportService.ts
|
|
661
1468
|
class AsciiTransportService extends Context.Service()("AsciiTransportService") {
|
|
662
|
-
static makeScoped =
|
|
1469
|
+
static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
|
|
1470
|
+
return transportConstructor.open(options);
|
|
1471
|
+
}, "AsciiTransportService");
|
|
663
1472
|
static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
|
|
664
1473
|
static makeMockTransport = (devices) => {
|
|
665
|
-
const factory =
|
|
1474
|
+
const factory = createMockTransport(devices);
|
|
666
1475
|
return (options) => Layer.effect(AsciiTransportService, factory(options));
|
|
667
1476
|
};
|
|
668
1477
|
}
|
|
@@ -672,10 +1481,12 @@ import { Context as Context3, Layer as Layer3 } from "effect";
|
|
|
672
1481
|
// src/RtuTransportService.ts
|
|
673
1482
|
import { Context as Context2, Layer as Layer2 } from "effect";
|
|
674
1483
|
class RtuTransportService extends Context2.Service()("RtuTransportService") {
|
|
675
|
-
static makeScoped =
|
|
1484
|
+
static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
|
|
1485
|
+
return transportConstructor.open(options);
|
|
1486
|
+
}, "RtuTransportService");
|
|
676
1487
|
static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
|
|
677
1488
|
static makeMockTransport = (devices) => {
|
|
678
|
-
const factory =
|
|
1489
|
+
const factory = createMockTransport(devices);
|
|
679
1490
|
return (options) => Layer2.effect(RtuTransportService, factory(options));
|
|
680
1491
|
};
|
|
681
1492
|
}
|
|
@@ -689,17 +1500,19 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
|
|
|
689
1500
|
return Layer3.flatMap(RtuTransportService.make(options), (context) => Layer3.succeed(SerialTransportService, Context3.get(context, RtuTransportService)));
|
|
690
1501
|
}
|
|
691
1502
|
static makeMockTransport = (devices) => {
|
|
692
|
-
const factory =
|
|
1503
|
+
const factory = createMockTransport(devices);
|
|
693
1504
|
return (options) => Layer3.effect(SerialTransportService, factory(options));
|
|
694
1505
|
};
|
|
695
1506
|
}
|
|
696
1507
|
// src/TcpTransportService.ts
|
|
697
1508
|
import { Context as Context4, Layer as Layer4 } from "effect";
|
|
698
1509
|
class TcpTransportService extends Context4.Service()("TcpTransportService") {
|
|
699
|
-
static makeScoped =
|
|
1510
|
+
static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
|
|
1511
|
+
return transportConstructor.connect(options);
|
|
1512
|
+
}, "TcpTransportService");
|
|
700
1513
|
static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
|
|
701
1514
|
static makeMockTransport = (devices) => {
|
|
702
|
-
const factory =
|
|
1515
|
+
const factory = createMockTransport(devices);
|
|
703
1516
|
return (options) => Layer4.effect(TcpTransportService, factory(options));
|
|
704
1517
|
};
|
|
705
1518
|
}
|
|
@@ -709,24 +1522,24 @@ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.g
|
|
|
709
1522
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
710
1523
|
const server = yield* Effect9.tryPromise({
|
|
711
1524
|
try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
|
|
712
|
-
catch: (error) => toModbusError(error)
|
|
1525
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
713
1526
|
});
|
|
714
1527
|
yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
|
|
715
1528
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
716
1529
|
try: () => server.shutdown(),
|
|
717
|
-
catch: (error) => toModbusError(error)
|
|
1530
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
718
1531
|
})), Effect9.catch(() => Effect9.void)));
|
|
719
1532
|
}));
|
|
720
1533
|
var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
|
|
721
1534
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
722
1535
|
const server = yield* Effect9.tryPromise({
|
|
723
1536
|
try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
|
|
724
|
-
catch: (error) => toModbusError(error)
|
|
1537
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
725
1538
|
});
|
|
726
1539
|
yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
|
|
727
1540
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
728
1541
|
try: () => server.shutdown(),
|
|
729
|
-
catch: (error) => toModbusError(error)
|
|
1542
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
730
1543
|
})), Effect9.catch(() => Effect9.void)));
|
|
731
1544
|
}));
|
|
732
1545
|
// src/TcpModbusServerService.ts
|
|
@@ -735,12 +1548,12 @@ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(fu
|
|
|
735
1548
|
const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
|
|
736
1549
|
const server = yield* Effect10.tryPromise({
|
|
737
1550
|
try: () => AsyncTcpModbusServer.bind(options, handlers),
|
|
738
|
-
catch: (error) => toModbusError(error)
|
|
1551
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
739
1552
|
});
|
|
740
1553
|
yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
|
|
741
1554
|
yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
|
|
742
1555
|
try: () => server.shutdown(),
|
|
743
|
-
catch: (error) => toModbusError(error)
|
|
1556
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
744
1557
|
})), Effect10.catch(() => Effect10.void)));
|
|
745
1558
|
}));
|
|
746
1559
|
// src/TcpGatewayService.ts
|
|
@@ -749,41 +1562,47 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.
|
|
|
749
1562
|
const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
|
|
750
1563
|
const gateway = yield* Effect11.tryPromise({
|
|
751
1564
|
try: () => AsyncTcpGateway.bind(options, gatewayConfig),
|
|
752
|
-
catch: (error) => toModbusError(error)
|
|
1565
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
753
1566
|
});
|
|
754
1567
|
yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
|
|
755
1568
|
yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
|
|
756
1569
|
try: () => gateway.shutdown(),
|
|
757
|
-
catch: (error) => toModbusError(error)
|
|
1570
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
758
1571
|
})), Effect11.catch(() => Effect11.void)));
|
|
759
1572
|
}));
|
|
760
1573
|
// src/WasmWsTransportService.ts
|
|
761
1574
|
import { Context as Context5, Layer as Layer8 } from "effect";
|
|
762
1575
|
class WasmWsTransportService extends Context5.Service()("WasmWsTransportService") {
|
|
763
|
-
static makeScoped =
|
|
1576
|
+
static makeScoped = createTransportScoped("WasmWsTransport", (transportConstructor, options) => {
|
|
1577
|
+
return transportConstructor.connect(options);
|
|
1578
|
+
}, "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
764
1579
|
static make = (options) => Layer8.effect(WasmWsTransportService, WasmWsTransportService.makeScoped(options));
|
|
765
1580
|
static makeMockTransport = (devices) => {
|
|
766
|
-
const factory =
|
|
1581
|
+
const factory = createMockTransport(devices);
|
|
767
1582
|
return (options) => Layer8.effect(WasmWsTransportService, factory(options));
|
|
768
1583
|
};
|
|
769
1584
|
}
|
|
770
1585
|
// src/WasmRtuTransportService.ts
|
|
771
1586
|
import { Context as Context6, Layer as Layer9 } from "effect";
|
|
772
1587
|
class WasmRtuTransportService extends Context6.Service()("WasmRtuTransportService") {
|
|
773
|
-
static makeScoped =
|
|
1588
|
+
static makeScoped = createTransportScoped("WasmRtuTransport", (transportConstructor, { port, ...rest }) => {
|
|
1589
|
+
return transportConstructor.open(port, rest);
|
|
1590
|
+
}, "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
774
1591
|
static make = (options) => Layer9.effect(WasmRtuTransportService, WasmRtuTransportService.makeScoped(options));
|
|
775
1592
|
static makeMockTransport = (devices) => {
|
|
776
|
-
const factory =
|
|
1593
|
+
const factory = createMockTransport(devices);
|
|
777
1594
|
return (options) => Layer9.effect(WasmRtuTransportService, factory(options));
|
|
778
1595
|
};
|
|
779
1596
|
}
|
|
780
1597
|
// src/WasmAsciiTransportService.ts
|
|
781
1598
|
import { Context as Context7, Layer as Layer10 } from "effect";
|
|
782
1599
|
class WasmAsciiTransportService extends Context7.Service()("WasmAsciiTransportService") {
|
|
783
|
-
static makeScoped =
|
|
1600
|
+
static makeScoped = createTransportScoped("WasmAsciiTransport", (transportConstructor, { port, ...rest }) => {
|
|
1601
|
+
return transportConstructor.open(port, rest);
|
|
1602
|
+
}, "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
784
1603
|
static make = (options) => Layer10.effect(WasmAsciiTransportService, WasmAsciiTransportService.makeScoped(options));
|
|
785
1604
|
static makeMockTransport = (devices) => {
|
|
786
|
-
const factory =
|
|
1605
|
+
const factory = createMockTransport(devices);
|
|
787
1606
|
return (options) => Layer10.effect(WasmAsciiTransportService, factory(options));
|
|
788
1607
|
};
|
|
789
1608
|
}
|
|
@@ -797,74 +1616,76 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
|
|
|
797
1616
|
return Layer11.flatMap(WasmRtuTransportService.make(options), (context) => Layer11.succeed(WasmSerialTransportService, Context8.get(context, WasmRtuTransportService)));
|
|
798
1617
|
}
|
|
799
1618
|
static makeMockTransport = (devices) => {
|
|
800
|
-
const factory =
|
|
1619
|
+
const factory = createMockTransport(devices);
|
|
801
1620
|
return (options) => Layer11.effect(WasmSerialTransportService, factory(options));
|
|
802
1621
|
};
|
|
803
1622
|
}
|
|
804
1623
|
// src/WasmSerialPort.ts
|
|
805
|
-
import { Effect as
|
|
806
|
-
var requestSerialPort = () =>
|
|
1624
|
+
import { Effect as Effect12 } from "effect";
|
|
1625
|
+
var requestSerialPort = () => Effect12.tryPromise({
|
|
807
1626
|
try: async () => {
|
|
808
1627
|
const mod = await import("modbus-rs/web");
|
|
809
1628
|
return mod.requestSerialPort();
|
|
810
1629
|
},
|
|
811
|
-
catch: (error) => toModbusError(error)
|
|
1630
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
812
1631
|
});
|
|
813
1632
|
// src/WasmTcpServerService.ts
|
|
814
|
-
import { Effect as
|
|
815
|
-
var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(
|
|
816
|
-
const { WasmWsModbusServer } = yield*
|
|
817
|
-
const server = yield*
|
|
1633
|
+
import { Effect as Effect13, Layer as Layer12 } from "effect";
|
|
1634
|
+
var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(Effect13.gen(function* () {
|
|
1635
|
+
const { WasmWsModbusServer } = yield* Effect13.promise(() => import("modbus-rs/web"));
|
|
1636
|
+
const server = yield* Effect13.tryPromise({
|
|
818
1637
|
try: () => WasmWsModbusServer.bind(options, handlers),
|
|
819
|
-
catch: (error) => toModbusError(error)
|
|
1638
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
820
1639
|
});
|
|
821
|
-
yield*
|
|
822
|
-
yield*
|
|
1640
|
+
yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
|
|
1641
|
+
yield* Effect13.forkScoped(Effect13.tryPromise({
|
|
823
1642
|
try: () => server.serve(),
|
|
824
|
-
catch: (error) => toModbusError(error)
|
|
825
|
-
}).pipe(
|
|
826
|
-
yield*
|
|
1643
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1644
|
+
}).pipe(Effect13.catch((error) => Effect13.logError("WASM WS server loop ended", error))));
|
|
1645
|
+
yield* Effect13.addFinalizer(() => Effect13.logDebug("WASM WS server shutting down").pipe(Effect13.andThen(Effect13.tryPromise({
|
|
827
1646
|
try: () => server.shutdown(),
|
|
828
|
-
catch: (error) => toModbusError(error)
|
|
829
|
-
})),
|
|
1647
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1648
|
+
})), Effect13.catch(() => Effect13.void)));
|
|
830
1649
|
}));
|
|
831
1650
|
// src/WasmSerialModbusServerService.ts
|
|
832
|
-
import { Effect as
|
|
833
|
-
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
834
|
-
const { WasmSerialModbusServer } = yield*
|
|
835
|
-
const server = yield*
|
|
1651
|
+
import { Effect as Effect14, Layer as Layer13 } from "effect";
|
|
1652
|
+
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
|
|
1653
|
+
const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
|
|
1654
|
+
const server = yield* Effect14.tryPromise({
|
|
836
1655
|
try: () => WasmSerialModbusServer.bindRtu(options, handlers),
|
|
837
|
-
catch: (error) => toModbusError(error)
|
|
1656
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
838
1657
|
});
|
|
839
|
-
yield*
|
|
840
|
-
yield*
|
|
1658
|
+
yield* Effect14.logDebug("WASM serial RTU server bound");
|
|
1659
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
841
1660
|
try: () => server.serve(),
|
|
842
|
-
catch: (error) => toModbusError(error)
|
|
843
|
-
}).pipe(
|
|
844
|
-
yield*
|
|
1661
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1662
|
+
}).pipe(Effect14.catch((error) => Effect14.logError("WASM serial RTU server loop ended", error))));
|
|
1663
|
+
yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial RTU server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
|
|
845
1664
|
try: () => server.shutdown(),
|
|
846
|
-
catch: (error) => toModbusError(error)
|
|
847
|
-
})),
|
|
1665
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1666
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
848
1667
|
}));
|
|
849
|
-
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
850
|
-
const { WasmSerialModbusServer } = yield*
|
|
851
|
-
const server = yield*
|
|
1668
|
+
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(Effect14.gen(function* () {
|
|
1669
|
+
const { WasmSerialModbusServer } = yield* Effect14.promise(() => import("modbus-rs/web"));
|
|
1670
|
+
const server = yield* Effect14.tryPromise({
|
|
852
1671
|
try: () => WasmSerialModbusServer.bindAscii(options, handlers),
|
|
853
|
-
catch: (error) => toModbusError(error)
|
|
1672
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
854
1673
|
});
|
|
855
|
-
yield*
|
|
856
|
-
yield*
|
|
1674
|
+
yield* Effect14.logDebug("WASM serial ASCII server bound");
|
|
1675
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
857
1676
|
try: () => server.serve(),
|
|
858
|
-
catch: (error) => toModbusError(error)
|
|
859
|
-
}).pipe(
|
|
860
|
-
yield*
|
|
1677
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1678
|
+
}).pipe(Effect14.catch((error) => Effect14.logError("WASM serial ASCII server loop ended", error))));
|
|
1679
|
+
yield* Effect14.addFinalizer(() => Effect14.logDebug("WASM serial ASCII server shutting down").pipe(Effect14.andThen(Effect14.tryPromise({
|
|
861
1680
|
try: () => server.shutdown(),
|
|
862
|
-
catch: (error) => toModbusError(error)
|
|
863
|
-
})),
|
|
1681
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1682
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
864
1683
|
}));
|
|
865
1684
|
export {
|
|
866
1685
|
AsciiTransportService,
|
|
867
1686
|
ConnectionState,
|
|
1687
|
+
MODBUS_MAX_READ_REGISTERS,
|
|
1688
|
+
MODBUS_MAX_WRITE_REGISTERS,
|
|
868
1689
|
ModbusCircuitOpenError,
|
|
869
1690
|
ModbusConnectionClosedError,
|
|
870
1691
|
ModbusExceptionError,
|
|
@@ -881,7 +1702,16 @@ export {
|
|
|
881
1702
|
WasmRtuTransportService,
|
|
882
1703
|
WasmSerialTransportService,
|
|
883
1704
|
WasmWsTransportService,
|
|
884
|
-
|
|
1705
|
+
createBatchingRegistry,
|
|
1706
|
+
createReadDebouncer,
|
|
1707
|
+
createRegisterCache,
|
|
1708
|
+
createRetryPolicy,
|
|
1709
|
+
createWriteDebouncer,
|
|
1710
|
+
encodeRegisterValue,
|
|
1711
|
+
makeBatchingClient,
|
|
1712
|
+
mergeSpanAttributes,
|
|
1713
|
+
planReads,
|
|
1714
|
+
planWrites,
|
|
885
1715
|
requestSerialPort,
|
|
886
1716
|
retryModbus,
|
|
887
1717
|
retryableExceptionCodes,
|