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