@flux-control/effect-modbus-rs 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -20
- package/dist/index.d.ts +62 -1
- package/dist/index.js +1034 -213
- package/dist/src/AsciiTransportService.d.ts +14 -10
- package/dist/src/RtuTransportService.d.ts +14 -10
- package/dist/src/TcpTransportService.d.ts +14 -10
- package/dist/src/WasmAsciiTransportService.d.ts +15 -11
- package/dist/src/WasmRtuTransportService.d.ts +15 -11
- package/dist/src/WasmWsTransportService.d.ts +14 -10
- package/dist/src/batching-client.d.ts +197 -0
- package/dist/src/connection.d.ts +6 -3
- package/dist/src/mocks.d.ts +14 -8
- package/dist/src/modbus-client.d.ts +3 -1
- package/dist/src/read-debouncer.d.ts +100 -0
- package/dist/src/register-cache.d.ts +90 -0
- package/dist/src/register-plan.d.ts +212 -0
- package/dist/src/retry.d.ts +4 -4
- package/dist/src/shared-transport.d.ts +60 -4
- package/dist/src/span-attributes.d.ts +34 -0
- package/dist/src/write-debouncer.d.ts +134 -0
- package/dist/test/retry.test.d.ts +1 -0
- package/dist/test/shared-transport.test.d.ts +1 -0
- package/dist/test/span-attributes.test.d.ts +1 -0
- package/dist/test/upstream-options.test.d.ts +1 -0
- package/dist/test/wasm-mocks.test.d.ts +1 -0
- package/dist/test/write-debouncer.test.d.ts +1 -0
- package/package.json +5 -4
- /package/dist/{src/mock-options.test.d.ts → test/batching-client.test.d.ts} +0 -0
- /package/dist/{src/mocks.test.d.ts → test/mock-options.test.d.ts} +0 -0
- /package/dist/{src/modbus-client.wasm.test.d.ts → test/mocks.test.d.ts} +0 -0
- /package/dist/{src/resilience.test.d.ts → test/modbus-client.wasm.test.d.ts} +0 -0
- /package/dist/{src/retry.test.d.ts → test/read-debouncer.test.d.ts} +0 -0
- /package/dist/{src/shared-transport.test.d.ts → test/register-cache.test.d.ts} +0 -0
- /package/dist/{src/upstream-options.test.d.ts → test/register-plan.test.d.ts} +0 -0
- /package/dist/{src/wasm-mocks.test.d.ts → test/resilience.test.d.ts} +0 -0
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,89 +1155,168 @@ 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, 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 openOptions = { ...options };
|
|
1296
|
+
Reflect.deleteProperty(openOptions, "retry");
|
|
1297
|
+
Reflect.deleteProperty(openOptions, "reconnect");
|
|
1298
|
+
const TC = yield* Effect8.promise(() => loadTransportConstructor(transportKey, config?.moduleSpecifier ?? "modbus-rs"));
|
|
523
1299
|
let transport = null;
|
|
524
1300
|
const opening = yield* makeInFlight();
|
|
525
1301
|
const reconnecting = yield* makeInFlight();
|
|
526
|
-
const connectionState = yield*
|
|
527
|
-
const
|
|
528
|
-
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;
|
|
529
1306
|
const clientSet = new Map;
|
|
530
1307
|
let closed = false;
|
|
531
1308
|
const transportClosed = () => new ModbusNotConnectedError({
|
|
532
1309
|
cause: new Error("Transport has been closed"),
|
|
533
1310
|
message: "Transport has been closed"
|
|
534
1311
|
});
|
|
535
|
-
const closeOrphan = (t) =>
|
|
536
|
-
const openTransport =
|
|
1312
|
+
const closeOrphan = (t) => Effect8.forkDetach(Effect8.ignore(Effect8.tryPromise(() => t.close())));
|
|
1313
|
+
const openTransport = Effect8.tryPromise({
|
|
537
1314
|
try: () => openMethod(TC, openOptions),
|
|
538
|
-
catch: (
|
|
539
|
-
}).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(() => {
|
|
540
1317
|
transport = t;
|
|
541
|
-
}),
|
|
542
|
-
const ensureOpen =
|
|
1318
|
+
}), SubscriptionRef4.set(connectionState, ConnectionState.Connected()))));
|
|
1319
|
+
const ensureOpen = Effect8.fnUntraced(function* () {
|
|
543
1320
|
if (closed)
|
|
544
1321
|
return yield* transportClosed();
|
|
545
1322
|
if (transport)
|
|
@@ -549,52 +1326,77 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
549
1326
|
return yield* transportClosed();
|
|
550
1327
|
return t;
|
|
551
1328
|
});
|
|
552
|
-
|
|
1329
|
+
const closeTransport = Effect8.suspend(() => {
|
|
553
1330
|
if (closed)
|
|
554
|
-
return
|
|
1331
|
+
return Effect8.void;
|
|
555
1332
|
closed = true;
|
|
556
1333
|
const t = transport;
|
|
557
1334
|
if (!t)
|
|
558
|
-
return
|
|
559
|
-
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())));
|
|
560
1340
|
});
|
|
1341
|
+
yield* Effect8.addFinalizer(() => Effect8.orDie(closeTransport));
|
|
561
1342
|
const notConnectedMsg = "Transport is not connected. Call withClient() first.";
|
|
562
|
-
const reconnectOnce =
|
|
1343
|
+
const reconnectOnce = Effect8.suspend(() => {
|
|
563
1344
|
const t = transport;
|
|
564
1345
|
if (closed || !t)
|
|
565
1346
|
return transportClosed();
|
|
566
|
-
return singleFlight(reconnecting,
|
|
1347
|
+
return singleFlight(reconnecting, Effect8.tryPromise({
|
|
567
1348
|
try: () => t.reconnect(),
|
|
568
|
-
catch: (
|
|
569
|
-
}).pipe(
|
|
1349
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1350
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
570
1351
|
});
|
|
571
1352
|
const report = (error) => {
|
|
572
|
-
if (
|
|
573
|
-
return
|
|
574
|
-
|
|
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}`));
|
|
575
1359
|
};
|
|
576
1360
|
const resilience = {
|
|
577
|
-
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),
|
|
578
1363
|
report
|
|
579
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
|
+
});
|
|
580
1396
|
return {
|
|
581
1397
|
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) {
|
|
1398
|
+
withClient,
|
|
1399
|
+
setRequestTimeout: Effect8.fnUntraced(function* (timeoutMs) {
|
|
598
1400
|
const t = transport;
|
|
599
1401
|
if (!t || closed) {
|
|
600
1402
|
return yield* new ModbusNotConnectedError({
|
|
@@ -604,7 +1406,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
604
1406
|
}
|
|
605
1407
|
t.setRequestTimeout(timeoutMs);
|
|
606
1408
|
}),
|
|
607
|
-
clearRequestTimeout:
|
|
1409
|
+
clearRequestTimeout: Effect8.fnUntraced(function* () {
|
|
608
1410
|
const t = transport;
|
|
609
1411
|
if (!t || closed) {
|
|
610
1412
|
return yield* new ModbusNotConnectedError({
|
|
@@ -614,7 +1416,7 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
614
1416
|
}
|
|
615
1417
|
t.clearRequestTimeout();
|
|
616
1418
|
}),
|
|
617
|
-
reconnect:
|
|
1419
|
+
reconnect: Effect8.fnUntraced(function* () {
|
|
618
1420
|
if (closed)
|
|
619
1421
|
return yield* transportClosed();
|
|
620
1422
|
const t = transport;
|
|
@@ -622,29 +1424,25 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
622
1424
|
yield* ensureOpen();
|
|
623
1425
|
return;
|
|
624
1426
|
}
|
|
625
|
-
yield* singleFlight(reconnecting,
|
|
1427
|
+
yield* singleFlight(reconnecting, Effect8.tryPromise({
|
|
626
1428
|
try: () => t.reconnect(),
|
|
627
|
-
catch: (
|
|
628
|
-
}).pipe(
|
|
1429
|
+
catch: (cause) => toModbusError(cause instanceof Error ? cause : new Error(String(cause)))
|
|
1430
|
+
}).pipe(Effect8.tap(() => closed ? closeOrphan(t) : Effect8.void)));
|
|
629
1431
|
if (closed)
|
|
630
1432
|
return yield* transportClosed();
|
|
631
|
-
yield*
|
|
1433
|
+
yield* SubscriptionRef4.set(connectionState, ConnectionState.Connected());
|
|
632
1434
|
}),
|
|
633
|
-
close:
|
|
1435
|
+
close: Effect8.fnUntraced(function* () {
|
|
634
1436
|
if (closed)
|
|
635
1437
|
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);
|
|
1438
|
+
const scope = yield* Effect8.scope;
|
|
1439
|
+
yield* Scope3.close(scope, Exit4.void).pipe(Effect8.onExit(() => closeTransport));
|
|
647
1440
|
}),
|
|
1441
|
+
withBatchingClient: batching.withBatchingClient,
|
|
1442
|
+
batchingClient: batching.batchingClient,
|
|
1443
|
+
get touchedUnits() {
|
|
1444
|
+
return new Set(clientSet.keys());
|
|
1445
|
+
},
|
|
648
1446
|
hasPendingRequests: () => {
|
|
649
1447
|
if (closed)
|
|
650
1448
|
return false;
|
|
@@ -659,10 +1457,12 @@ function makeTransportScoped(transportKey, openMethod, serviceName, config) {
|
|
|
659
1457
|
|
|
660
1458
|
// src/AsciiTransportService.ts
|
|
661
1459
|
class AsciiTransportService extends Context.Service()("AsciiTransportService") {
|
|
662
|
-
static makeScoped =
|
|
1460
|
+
static makeScoped = createTransportScoped("AsyncAsciiTransport", (transportConstructor, options) => {
|
|
1461
|
+
return transportConstructor.open(options);
|
|
1462
|
+
}, "AsciiTransportService");
|
|
663
1463
|
static make = (options) => Layer.effect(AsciiTransportService, AsciiTransportService.makeScoped(options));
|
|
664
1464
|
static makeMockTransport = (devices) => {
|
|
665
|
-
const factory =
|
|
1465
|
+
const factory = createMockTransport(devices);
|
|
666
1466
|
return (options) => Layer.effect(AsciiTransportService, factory(options));
|
|
667
1467
|
};
|
|
668
1468
|
}
|
|
@@ -672,10 +1472,12 @@ import { Context as Context3, Layer as Layer3 } from "effect";
|
|
|
672
1472
|
// src/RtuTransportService.ts
|
|
673
1473
|
import { Context as Context2, Layer as Layer2 } from "effect";
|
|
674
1474
|
class RtuTransportService extends Context2.Service()("RtuTransportService") {
|
|
675
|
-
static makeScoped =
|
|
1475
|
+
static makeScoped = createTransportScoped("AsyncRtuTransport", (transportConstructor, options) => {
|
|
1476
|
+
return transportConstructor.open(options);
|
|
1477
|
+
}, "RtuTransportService");
|
|
676
1478
|
static make = (options) => Layer2.effect(RtuTransportService, RtuTransportService.makeScoped(options));
|
|
677
1479
|
static makeMockTransport = (devices) => {
|
|
678
|
-
const factory =
|
|
1480
|
+
const factory = createMockTransport(devices);
|
|
679
1481
|
return (options) => Layer2.effect(RtuTransportService, factory(options));
|
|
680
1482
|
};
|
|
681
1483
|
}
|
|
@@ -689,17 +1491,19 @@ class SerialTransportService extends Context3.Service()("SerialTransportService"
|
|
|
689
1491
|
return Layer3.flatMap(RtuTransportService.make(options), (context) => Layer3.succeed(SerialTransportService, Context3.get(context, RtuTransportService)));
|
|
690
1492
|
}
|
|
691
1493
|
static makeMockTransport = (devices) => {
|
|
692
|
-
const factory =
|
|
1494
|
+
const factory = createMockTransport(devices);
|
|
693
1495
|
return (options) => Layer3.effect(SerialTransportService, factory(options));
|
|
694
1496
|
};
|
|
695
1497
|
}
|
|
696
1498
|
// src/TcpTransportService.ts
|
|
697
1499
|
import { Context as Context4, Layer as Layer4 } from "effect";
|
|
698
1500
|
class TcpTransportService extends Context4.Service()("TcpTransportService") {
|
|
699
|
-
static makeScoped =
|
|
1501
|
+
static makeScoped = createTransportScoped("AsyncTcpTransport", (transportConstructor, options) => {
|
|
1502
|
+
return transportConstructor.connect(options);
|
|
1503
|
+
}, "TcpTransportService");
|
|
700
1504
|
static make = (options) => Layer4.effect(TcpTransportService, TcpTransportService.makeScoped(options));
|
|
701
1505
|
static makeMockTransport = (devices) => {
|
|
702
|
-
const factory =
|
|
1506
|
+
const factory = createMockTransport(devices);
|
|
703
1507
|
return (options) => Layer4.effect(TcpTransportService, factory(options));
|
|
704
1508
|
};
|
|
705
1509
|
}
|
|
@@ -709,24 +1513,24 @@ var serialRtuServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.g
|
|
|
709
1513
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
710
1514
|
const server = yield* Effect9.tryPromise({
|
|
711
1515
|
try: () => AsyncSerialModbusServer.bindRtu(options, handlers),
|
|
712
|
-
catch: (error) => toModbusError(error)
|
|
1516
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
713
1517
|
});
|
|
714
1518
|
yield* Effect9.logDebug(`Serial RTU server bound to ${options.portPath}`);
|
|
715
1519
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial RTU server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
716
1520
|
try: () => server.shutdown(),
|
|
717
|
-
catch: (error) => toModbusError(error)
|
|
1521
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
718
1522
|
})), Effect9.catch(() => Effect9.void)));
|
|
719
1523
|
}));
|
|
720
1524
|
var serialAsciiServerLayer = (options, handlers) => Layer5.effectDiscard(Effect9.gen(function* () {
|
|
721
1525
|
const { AsyncSerialModbusServer } = yield* Effect9.promise(() => import("modbus-rs"));
|
|
722
1526
|
const server = yield* Effect9.tryPromise({
|
|
723
1527
|
try: () => AsyncSerialModbusServer.bindAscii(options, handlers),
|
|
724
|
-
catch: (error) => toModbusError(error)
|
|
1528
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
725
1529
|
});
|
|
726
1530
|
yield* Effect9.logDebug(`Serial ASCII server bound to ${options.portPath}`);
|
|
727
1531
|
yield* Effect9.addFinalizer(() => Effect9.logDebug("Serial ASCII server shutting down").pipe(Effect9.andThen(Effect9.tryPromise({
|
|
728
1532
|
try: () => server.shutdown(),
|
|
729
|
-
catch: (error) => toModbusError(error)
|
|
1533
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
730
1534
|
})), Effect9.catch(() => Effect9.void)));
|
|
731
1535
|
}));
|
|
732
1536
|
// src/TcpModbusServerService.ts
|
|
@@ -735,12 +1539,12 @@ var tcpServerLayer = (options, handlers) => Layer6.effectDiscard(Effect10.gen(fu
|
|
|
735
1539
|
const { AsyncTcpModbusServer } = yield* Effect10.promise(() => import("modbus-rs"));
|
|
736
1540
|
const server = yield* Effect10.tryPromise({
|
|
737
1541
|
try: () => AsyncTcpModbusServer.bind(options, handlers),
|
|
738
|
-
catch: (error) => toModbusError(error)
|
|
1542
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
739
1543
|
});
|
|
740
1544
|
yield* Effect10.logDebug(`TCP server bound to ${options.host}:${options.port}`);
|
|
741
1545
|
yield* Effect10.addFinalizer(() => Effect10.logDebug("TCP server shutting down").pipe(Effect10.andThen(Effect10.tryPromise({
|
|
742
1546
|
try: () => server.shutdown(),
|
|
743
|
-
catch: (error) => toModbusError(error)
|
|
1547
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
744
1548
|
})), Effect10.catch(() => Effect10.void)));
|
|
745
1549
|
}));
|
|
746
1550
|
// src/TcpGatewayService.ts
|
|
@@ -749,41 +1553,47 @@ var tcpGatewayLayer = (options, gatewayConfig) => Layer7.effectDiscard(Effect11.
|
|
|
749
1553
|
const { AsyncTcpGateway } = yield* Effect11.promise(() => import("modbus-rs"));
|
|
750
1554
|
const gateway = yield* Effect11.tryPromise({
|
|
751
1555
|
try: () => AsyncTcpGateway.bind(options, gatewayConfig),
|
|
752
|
-
catch: (error) => toModbusError(error)
|
|
1556
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
753
1557
|
});
|
|
754
1558
|
yield* Effect11.logDebug(`TCP gateway bound to ${options.host}:${options.port}`);
|
|
755
1559
|
yield* Effect11.addFinalizer(() => Effect11.logDebug("TCP gateway shutting down").pipe(Effect11.andThen(Effect11.tryPromise({
|
|
756
1560
|
try: () => gateway.shutdown(),
|
|
757
|
-
catch: (error) => toModbusError(error)
|
|
1561
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
758
1562
|
})), Effect11.catch(() => Effect11.void)));
|
|
759
1563
|
}));
|
|
760
1564
|
// src/WasmWsTransportService.ts
|
|
761
1565
|
import { Context as Context5, Layer as Layer8 } from "effect";
|
|
762
1566
|
class WasmWsTransportService extends Context5.Service()("WasmWsTransportService") {
|
|
763
|
-
static makeScoped =
|
|
1567
|
+
static makeScoped = createTransportScoped("WasmWsTransport", (transportConstructor, options) => {
|
|
1568
|
+
return transportConstructor.connect(options);
|
|
1569
|
+
}, "WasmWsTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
764
1570
|
static make = (options) => Layer8.effect(WasmWsTransportService, WasmWsTransportService.makeScoped(options));
|
|
765
1571
|
static makeMockTransport = (devices) => {
|
|
766
|
-
const factory =
|
|
1572
|
+
const factory = createMockTransport(devices);
|
|
767
1573
|
return (options) => Layer8.effect(WasmWsTransportService, factory(options));
|
|
768
1574
|
};
|
|
769
1575
|
}
|
|
770
1576
|
// src/WasmRtuTransportService.ts
|
|
771
1577
|
import { Context as Context6, Layer as Layer9 } from "effect";
|
|
772
1578
|
class WasmRtuTransportService extends Context6.Service()("WasmRtuTransportService") {
|
|
773
|
-
static makeScoped =
|
|
1579
|
+
static makeScoped = createTransportScoped("WasmRtuTransport", (transportConstructor, { port, ...rest }) => {
|
|
1580
|
+
return transportConstructor.open(port, rest);
|
|
1581
|
+
}, "WasmRtuTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
774
1582
|
static make = (options) => Layer9.effect(WasmRtuTransportService, WasmRtuTransportService.makeScoped(options));
|
|
775
1583
|
static makeMockTransport = (devices) => {
|
|
776
|
-
const factory =
|
|
1584
|
+
const factory = createMockTransport(devices);
|
|
777
1585
|
return (options) => Layer9.effect(WasmRtuTransportService, factory(options));
|
|
778
1586
|
};
|
|
779
1587
|
}
|
|
780
1588
|
// src/WasmAsciiTransportService.ts
|
|
781
1589
|
import { Context as Context7, Layer as Layer10 } from "effect";
|
|
782
1590
|
class WasmAsciiTransportService extends Context7.Service()("WasmAsciiTransportService") {
|
|
783
|
-
static makeScoped =
|
|
1591
|
+
static makeScoped = createTransportScoped("WasmAsciiTransport", (transportConstructor, { port, ...rest }) => {
|
|
1592
|
+
return transportConstructor.open(port, rest);
|
|
1593
|
+
}, "WasmAsciiTransportService", { moduleSpecifier: "modbus-rs/web" });
|
|
784
1594
|
static make = (options) => Layer10.effect(WasmAsciiTransportService, WasmAsciiTransportService.makeScoped(options));
|
|
785
1595
|
static makeMockTransport = (devices) => {
|
|
786
|
-
const factory =
|
|
1596
|
+
const factory = createMockTransport(devices);
|
|
787
1597
|
return (options) => Layer10.effect(WasmAsciiTransportService, factory(options));
|
|
788
1598
|
};
|
|
789
1599
|
}
|
|
@@ -797,74 +1607,76 @@ class WasmSerialTransportService extends Context8.Service()("WasmSerialTransport
|
|
|
797
1607
|
return Layer11.flatMap(WasmRtuTransportService.make(options), (context) => Layer11.succeed(WasmSerialTransportService, Context8.get(context, WasmRtuTransportService)));
|
|
798
1608
|
}
|
|
799
1609
|
static makeMockTransport = (devices) => {
|
|
800
|
-
const factory =
|
|
1610
|
+
const factory = createMockTransport(devices);
|
|
801
1611
|
return (options) => Layer11.effect(WasmSerialTransportService, factory(options));
|
|
802
1612
|
};
|
|
803
1613
|
}
|
|
804
1614
|
// src/WasmSerialPort.ts
|
|
805
|
-
import { Effect as
|
|
806
|
-
var requestSerialPort = () =>
|
|
1615
|
+
import { Effect as Effect12 } from "effect";
|
|
1616
|
+
var requestSerialPort = () => Effect12.tryPromise({
|
|
807
1617
|
try: async () => {
|
|
808
1618
|
const mod = await import("modbus-rs/web");
|
|
809
1619
|
return mod.requestSerialPort();
|
|
810
1620
|
},
|
|
811
|
-
catch: (error) => toModbusError(error)
|
|
1621
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
812
1622
|
});
|
|
813
1623
|
// src/WasmTcpServerService.ts
|
|
814
|
-
import { Effect as
|
|
815
|
-
var wasmWsServerLayer = (options, handlers) => Layer12.effectDiscard(
|
|
816
|
-
const { WasmWsModbusServer } = yield*
|
|
817
|
-
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({
|
|
818
1628
|
try: () => WasmWsModbusServer.bind(options, handlers),
|
|
819
|
-
catch: (error) => toModbusError(error)
|
|
1629
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
820
1630
|
});
|
|
821
|
-
yield*
|
|
822
|
-
yield*
|
|
1631
|
+
yield* Effect13.logDebug(`WASM WS server bound to ${options.wsUrl}`);
|
|
1632
|
+
yield* Effect13.forkScoped(Effect13.tryPromise({
|
|
823
1633
|
try: () => server.serve(),
|
|
824
|
-
catch: (error) => toModbusError(error)
|
|
825
|
-
}).pipe(
|
|
826
|
-
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({
|
|
827
1637
|
try: () => server.shutdown(),
|
|
828
|
-
catch: (error) => toModbusError(error)
|
|
829
|
-
})),
|
|
1638
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1639
|
+
})), Effect13.catch(() => Effect13.void)));
|
|
830
1640
|
}));
|
|
831
1641
|
// src/WasmSerialModbusServerService.ts
|
|
832
|
-
import { Effect as
|
|
833
|
-
var wasmSerialRtuServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
834
|
-
const { WasmSerialModbusServer } = yield*
|
|
835
|
-
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({
|
|
836
1646
|
try: () => WasmSerialModbusServer.bindRtu(options, handlers),
|
|
837
|
-
catch: (error) => toModbusError(error)
|
|
1647
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
838
1648
|
});
|
|
839
|
-
yield*
|
|
840
|
-
yield*
|
|
1649
|
+
yield* Effect14.logDebug("WASM serial RTU server bound");
|
|
1650
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
841
1651
|
try: () => server.serve(),
|
|
842
|
-
catch: (error) => toModbusError(error)
|
|
843
|
-
}).pipe(
|
|
844
|
-
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({
|
|
845
1655
|
try: () => server.shutdown(),
|
|
846
|
-
catch: (error) => toModbusError(error)
|
|
847
|
-
})),
|
|
1656
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1657
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
848
1658
|
}));
|
|
849
|
-
var wasmSerialAsciiServerLayer = (options, handlers) => Layer13.effectDiscard(
|
|
850
|
-
const { WasmSerialModbusServer } = yield*
|
|
851
|
-
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({
|
|
852
1662
|
try: () => WasmSerialModbusServer.bindAscii(options, handlers),
|
|
853
|
-
catch: (error) => toModbusError(error)
|
|
1663
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
854
1664
|
});
|
|
855
|
-
yield*
|
|
856
|
-
yield*
|
|
1665
|
+
yield* Effect14.logDebug("WASM serial ASCII server bound");
|
|
1666
|
+
yield* Effect14.forkScoped(Effect14.tryPromise({
|
|
857
1667
|
try: () => server.serve(),
|
|
858
|
-
catch: (error) => toModbusError(error)
|
|
859
|
-
}).pipe(
|
|
860
|
-
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({
|
|
861
1671
|
try: () => server.shutdown(),
|
|
862
|
-
catch: (error) => toModbusError(error)
|
|
863
|
-
})),
|
|
1672
|
+
catch: (error) => toModbusError(error instanceof Error ? error : new Error(String(error)))
|
|
1673
|
+
})), Effect14.catch(() => Effect14.void)));
|
|
864
1674
|
}));
|
|
865
1675
|
export {
|
|
866
1676
|
AsciiTransportService,
|
|
867
1677
|
ConnectionState,
|
|
1678
|
+
MODBUS_MAX_READ_REGISTERS,
|
|
1679
|
+
MODBUS_MAX_WRITE_REGISTERS,
|
|
868
1680
|
ModbusCircuitOpenError,
|
|
869
1681
|
ModbusConnectionClosedError,
|
|
870
1682
|
ModbusExceptionError,
|
|
@@ -881,7 +1693,16 @@ export {
|
|
|
881
1693
|
WasmRtuTransportService,
|
|
882
1694
|
WasmSerialTransportService,
|
|
883
1695
|
WasmWsTransportService,
|
|
884
|
-
|
|
1696
|
+
createBatchingRegistry,
|
|
1697
|
+
createReadDebouncer,
|
|
1698
|
+
createRegisterCache,
|
|
1699
|
+
createRetryPolicy,
|
|
1700
|
+
createWriteDebouncer,
|
|
1701
|
+
encodeRegisterValue,
|
|
1702
|
+
makeBatchingClient,
|
|
1703
|
+
mergeSpanAttributes,
|
|
1704
|
+
planReads,
|
|
1705
|
+
planWrites,
|
|
885
1706
|
requestSerialPort,
|
|
886
1707
|
retryModbus,
|
|
887
1708
|
retryableExceptionCodes,
|