@6qat/tcp-connection 0.2.7 → 0.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@6qat/tcp-connection",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "TCP connection library with Effect.js integration",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "type": "module",
9
9
  "files": [
10
10
  "dist",
11
+ "src",
11
12
  "README.md"
12
13
  ],
13
14
  "exports": {
@@ -0,0 +1,62 @@
1
+ import { Effect, Fiber, Layer } from 'effect';
2
+ import {
3
+ TcpStream,
4
+ TcpStreamLive,
5
+ ConnectionConfigLive,
6
+ } from './tcp-connection.js';
7
+ import { ping } from './utils.js';
8
+
9
+ /**
10
+ * Example usage of ping function with real TCP stream
11
+ */
12
+ export const pingExample = Effect.gen(function* () {
13
+ // Create TCP stream layer
14
+ const tcpStreamLayer = TcpStreamLive().pipe(
15
+ Layer.provide(ConnectionConfigLive('localhost', 8080, [], '', '', '')),
16
+ );
17
+
18
+ // Run the ping example with TCP stream
19
+ yield* Effect.gen(function* () {
20
+ const tcpStream = yield* TcpStream;
21
+
22
+ console.log('Connected to TCP server, starting ping loop...');
23
+
24
+ // Start ping loop using the real TCP send function (handle errors)
25
+ const pingFiber = yield* ping((data) =>
26
+ tcpStream
27
+ .send(data)
28
+ .pipe(
29
+ Effect.catchAll((error) =>
30
+ Effect.logError(`Send failed: ${error.message}`),
31
+ ),
32
+ ),
33
+ );
34
+
35
+ console.log('Ping loop started in background');
36
+
37
+ // Keep the main effect alive for 10 seconds to see pings
38
+ yield* Effect.sleep('10 seconds');
39
+
40
+ // Clean up: stop the ping loop
41
+ yield* Fiber.interrupt(pingFiber);
42
+
43
+ console.log('Ping loop stopped, closing connection...');
44
+
45
+ // Close the TCP connection
46
+ yield* tcpStream.close;
47
+
48
+ console.log('Connection closed');
49
+ }).pipe(
50
+ Effect.provide(tcpStreamLayer),
51
+ Effect.catchAll((error) =>
52
+ Effect.logError(
53
+ `Connection error: ${error instanceof Error ? error.message : String(error)}`,
54
+ ),
55
+ ),
56
+ );
57
+ });
58
+
59
+ /**
60
+ * To run this example:
61
+ * Effect.runPromise(pingExample)
62
+ */
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Export everything from tcp-connection
2
+ export * from './tcp-connection.js';
3
+ // Export utilities
4
+ export * from './utils.js';
@@ -0,0 +1,354 @@
1
+ import type { Socket } from 'node:net';
2
+ // src/tcp-stream.ts
3
+ import {
4
+ Context,
5
+ Data,
6
+ Effect,
7
+ Layer,
8
+ Queue,
9
+ Ref,
10
+ Runtime,
11
+ Schedule,
12
+ Stream,
13
+ pipe,
14
+ } from 'effect';
15
+
16
+ // Base error class with a more flexible tag system
17
+ export class TcpConnectionError extends Data.TaggedError('TcpConnectionError') {
18
+ constructor(readonly errorType?: string) {
19
+ super();
20
+ }
21
+ }
22
+
23
+ export class TcpConnectionTimeoutError extends TcpConnectionError {
24
+ constructor(readonly message: string) {
25
+ super('ConnectionTimeout');
26
+ }
27
+ }
28
+
29
+ export class TcpConnectionWriteError extends TcpConnectionError {
30
+ constructor(readonly message: string) {
31
+ super('WriteError');
32
+ }
33
+ }
34
+
35
+ export class TcpConnectionCloseError extends TcpConnectionError {
36
+ constructor(readonly message: string) {
37
+ super('CloseError');
38
+ }
39
+ }
40
+
41
+ // Context Tag for Dependency Injection
42
+ class TcpConnection extends Context.Tag('TcpConnection')<
43
+ TcpConnection,
44
+ TcpConnectionShape
45
+ >() {}
46
+
47
+ interface TcpConnectionShape {
48
+ readonly incoming: Stream.Stream<Uint8Array, TcpConnectionError>;
49
+ readonly send: (data: Uint8Array) => Effect.Effect<void, TcpConnectionError>;
50
+ readonly sendWithRetry: (
51
+ data: Uint8Array,
52
+ ) => Effect.Effect<void, TcpConnectionError>;
53
+ }
54
+ // Configuration (host/port)
55
+ class TcpConfig extends Context.Tag('TcpConfig')<
56
+ TcpConfig,
57
+ {
58
+ host: string;
59
+ port: number;
60
+ bufferSize?: number;
61
+ runtimeEffect?: Effect.Effect<Runtime.Runtime<never>, never, never>;
62
+ }
63
+ >() {}
64
+
65
+ // State
66
+ interface ConnectionState {
67
+ readonly socket: Socket; // Or TLS socket
68
+ readonly isOpen: boolean;
69
+ }
70
+
71
+ const TcpConnectionLive = Layer.scoped(
72
+ TcpConnection,
73
+ Effect.gen(function* () {
74
+ const {
75
+ host,
76
+ port,
77
+ bufferSize = 2048,
78
+ runtimeEffect = Effect.succeed(Runtime.defaultRuntime),
79
+ } = yield* TcpConfig;
80
+
81
+ const queue = yield* Queue.bounded<Uint8Array>(bufferSize);
82
+ const stateRef = yield* Ref.make<ConnectionState | null>(null);
83
+ const restartQueue = yield* Queue.unbounded<void>(); // Signal restarts
84
+
85
+ const runtime = yield* runtimeEffect;
86
+ const runPromise = <A, E>(effect: Effect.Effect<A, E, never>) =>
87
+ Runtime.runPromise(runtime)(effect);
88
+
89
+ const net = yield* Effect.promise(() => import('node:net'));
90
+
91
+ /**
92
+ * The close event triggers runPromise(handleClose), which sets stateRef to null and signals a restart. If a new connection is established before the previous one is fully cleaned up, you could have overlapping sockets or fibers.
93
+ */
94
+ // TODO: Improvement: Ensure that any socket cleanup is fully completed before a new socket is created. Consider using a mutex or a dedicated "connection management" fiber to serialize open/close/restart operations.
95
+ // When the socket closes, notify the restart queue
96
+ const handleClose = Effect.gen(function* () {
97
+ const currentState = yield* Ref.get(stateRef);
98
+ const socket = currentState?.socket;
99
+ if (socket && !socket.closed) {
100
+ // TODO: Check if its necessary to check if the socket is closed before end()
101
+ socket.removeAllListeners().end();
102
+ if (!socket.destroyed) {
103
+ socket.destroy();
104
+ }
105
+ }
106
+ yield* Ref.set(stateRef, null);
107
+ yield* Queue.offer(restartQueue, void 0); // Signal restart
108
+ yield* Effect.logDebug('Connection closed abruptly!!!');
109
+ });
110
+
111
+ const handleShutdown = Effect.gen(function* () {
112
+ yield* handleClose;
113
+ yield* Effect.logDebug('Finalizer: shutting down queues...');
114
+ yield* Queue.shutdown(queue);
115
+ yield* Queue.shutdown(restartQueue);
116
+ yield* Effect.logDebug('Finalizer: shutting down socket...');
117
+ const currentState = yield* Ref.get(stateRef);
118
+ const socket = currentState?.socket;
119
+ if (socket && !socket.closed) {
120
+ socket.removeAllListeners().end();
121
+ if (!socket.destroyed) {
122
+ socket.destroy();
123
+ }
124
+ }
125
+ yield* Effect.logDebug('Finalizer: done.');
126
+ });
127
+
128
+ const createSocket: () => Effect.Effect<
129
+ ConnectionState,
130
+ TcpConnectionError,
131
+ never
132
+ > = () => {
133
+ const effect: Effect.Effect<ConnectionState, TcpConnectionError, never> =
134
+ Effect.async((resume) => {
135
+ const socket = net.createConnection({ host, port });
136
+ socket.on('connect', () => {
137
+ // Handle incoming data
138
+ socket.on('data', (chunk: Buffer) => {
139
+ runPromise(Queue.offer(queue, new Uint8Array(chunk)));
140
+ });
141
+ socket.on('error', (err) => {
142
+ runPromise(Effect.fail(new TcpConnectionError(err.message)));
143
+ });
144
+ socket.on('close', () => {
145
+ runPromise(handleClose);
146
+ });
147
+ resume(Effect.succeed({ socket, isOpen: true }));
148
+ });
149
+ socket.on('error', (err) =>
150
+ resume(Effect.fail(new TcpConnectionError(err.message))),
151
+ );
152
+ });
153
+ return effect.pipe(
154
+ Effect.retry(
155
+ Schedule.exponential('1 second').pipe(
156
+ Schedule.compose(Schedule.recurUpTo(5)),
157
+ ),
158
+ ),
159
+ Effect.tap(() => Effect.logDebug('Connection established')),
160
+
161
+ Effect.tapErrorCause((cause) =>
162
+ Effect.logError('Connection failed', cause),
163
+ ),
164
+ );
165
+ };
166
+
167
+ // Create TCP socket
168
+ const initialState = yield* createSocket();
169
+
170
+ yield* Ref.set(stateRef, initialState);
171
+
172
+ // Restart logic
173
+ /**
174
+ * If multiple errors or closes happen in quick succession, you may queue multiple restarts.
175
+ */
176
+ // TODO: Debounce the restart calls
177
+ const restart: Effect.Effect<void, TcpConnectionError, never> = Effect.gen(
178
+ function* () {
179
+ yield* Effect.logDebug('Restarting connection');
180
+ const currentState = yield* Ref.get(stateRef);
181
+ if (currentState?.isOpen) return; // Already connected
182
+ yield* createSocket().pipe(
183
+ Effect.flatMap((state) => Ref.set(stateRef, state)),
184
+ Effect.retry(Schedule.fibonacci('5 seconds')),
185
+ );
186
+ },
187
+ );
188
+
189
+ // Add a mutex for state management
190
+ const stateMutex = yield* Effect.makeSemaphore(1);
191
+ // Wrap state updates
192
+ const withState = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
193
+ stateMutex.withPermits(1)(effect).pipe(Effect.withLogSpan('elapsed'));
194
+
195
+ const safeRestart = withState(restart);
196
+
197
+ const debouncedRestart = pipe(
198
+ safeRestart,
199
+ Effect.delay('3 seconds'),
200
+ // Effect.raceFirst(Effect.never), // Cancels previous restart if new one comes in
201
+ Effect.uninterruptible,
202
+ );
203
+
204
+ yield* pipe(
205
+ Stream.fromQueue(restartQueue, { shutdown: true }),
206
+ Stream.tap(() => Effect.logDebug('Queue restarted')),
207
+ Stream.tap(() => debouncedRestart),
208
+ Stream.runDrain,
209
+ Effect.onInterrupt(() =>
210
+ Effect.logDebug('Queue restart fiber interrupted'),
211
+ ),
212
+ Effect.forkDaemon,
213
+ );
214
+
215
+ const send = (data: Uint8Array) =>
216
+ Effect.gen(function* () {
217
+ const state = yield* Ref.get(stateRef);
218
+ if (!state?.isOpen) {
219
+ return yield* Effect.fail(new TcpConnectionCloseError('not_open'));
220
+ }
221
+ return yield* Effect.tryPromise({
222
+ try: () =>
223
+ new Promise((resolve, reject) => {
224
+ state.socket.write(data, (err) =>
225
+ err
226
+ ? reject(new TcpConnectionWriteError(err.message))
227
+ : resolve(void 0),
228
+ );
229
+ }),
230
+ catch: (error) => new TcpConnectionWriteError(error as string),
231
+ });
232
+ });
233
+
234
+ // Retry logic for send
235
+ const sendWithRetry = (data: Uint8Array) =>
236
+ send(data).pipe(
237
+ Effect.retry(
238
+ Schedule.exponential('100 millis', 2).pipe(
239
+ Schedule.compose(Schedule.recurUpTo(5)),
240
+ ),
241
+ ),
242
+ Effect.catchAll((error) => new TcpConnectionWriteError(error.message)),
243
+ );
244
+
245
+ // Cleanup on exit
246
+ yield* Effect.addFinalizer(() =>
247
+ Effect.gen(function* () {
248
+ yield* handleShutdown;
249
+ }),
250
+ );
251
+
252
+ return {
253
+ incoming: Stream.fromQueue(queue, { shutdown: true }),
254
+ send,
255
+ sendWithRetry,
256
+ };
257
+ }),
258
+ );
259
+
260
+ /*
261
+ // Usage examples
262
+
263
+ // Metrics
264
+ const bytesReceived = Metric.counter('tcp.bytes_received');
265
+ const bytesSent = Metric.counter('tcp.bytes_sent');
266
+
267
+ const program = Effect.gen(function* () {
268
+ // Create a Ref to track the value
269
+ const bytesReceivedRef = yield* Ref.make(0);
270
+ const bytesSentRef = yield* Ref.make(0);
271
+
272
+ const printMetrics = Effect.gen(function* () {
273
+ const received = yield* Ref.get(bytesReceivedRef);
274
+ const sent = yield* Ref.get(bytesSentRef);
275
+ yield* Effect.logInfo(`Bytes received: ${received}`);
276
+ yield* Effect.logInfo(`Bytes sent: ${sent}`);
277
+ });
278
+
279
+ const client = yield* TcpConnection;
280
+
281
+ // yield* client.send('GET / HTTP/1.1\r\nHost: www.terra.com.br\r\n\r\n');
282
+ const data = new TextEncoder().encode(
283
+ 'GET / HTTP/1.1\r\nHost: www.terra.com.br\r\n\r\n',
284
+ );
285
+
286
+ const send = (data: Uint8Array) =>
287
+ pipe(
288
+ Ref.update(bytesSentRef, (n) => n + data.length),
289
+ Effect.zipRight(Metric.incrementBy(bytesSent, data.length)),
290
+ Effect.flatMap(() => client.send(data)),
291
+ );
292
+
293
+ const _sendWithRetry = (data: Uint8Array) =>
294
+ pipe(
295
+ Ref.update(bytesSentRef, (n) => n + data.length),
296
+ Effect.zipRight(Metric.incrementBy(bytesSent, data.length)),
297
+ Effect.flatMap(() => client.sendWithRetry(data)),
298
+ );
299
+
300
+ // Initial send
301
+ yield* send(data).pipe(Effect.orElse(() => Effect.logError('deu ruim')));
302
+
303
+ // Incoming data handling
304
+ yield* pipe(
305
+ client.incoming,
306
+ Stream.tap((data) => Metric.incrementBy(bytesReceived, data.length)),
307
+ Stream.tap((data) => Ref.update(bytesReceivedRef, (n) => n + data.length)),
308
+ // Stream.tap((data) => Effect.logDebug(new TextDecoder().decode(data))),
309
+ Stream.tap((data) => Effect.logDebug(`Received: ${Buffer.from(data)}`)),
310
+ Stream.tap((chunks) => Effect.logDebug(`Received ${chunks.length} chunks`)),
311
+ Stream.catchAll((error) => {
312
+ if (error instanceof TcpConnectionCloseError) {
313
+ return Effect.log('Connection closed. Restarting...');
314
+ }
315
+ return Effect.fail(error);
316
+ }),
317
+ Stream.runDrain,
318
+ // Effect.forever,
319
+ Effect.fork, // Run in background
320
+ );
321
+
322
+ // Cleanup on exit
323
+ yield* Effect.addFinalizer(() =>
324
+ Effect.gen(function* () {
325
+ yield* Effect.logDebug('Program Finalizer');
326
+ yield* Effect.logDebug('Before printing stats');
327
+ yield* printMetrics;
328
+ }),
329
+ );
330
+
331
+ yield* Effect.never;
332
+ }).pipe(Effect.catchAll((error) => Effect.logError(error)));
333
+
334
+ const LoggerLive = Logger.minimumLogLevel(LogLevel.Debug);
335
+ const TcpConfigLive = Layer.succeed(TcpConfig, {
336
+ host: 'www.terra.com.br',
337
+ port: 80,
338
+ bufferSize: 1024,
339
+ runtimeEffect: Effect.scoped(Layer.toRuntime(LoggerLive)),
340
+ });
341
+ */
342
+
343
+ // const runnable = Effect.gen(function* () {
344
+ // yield* Effect.scoped(
345
+ // Effect.provide(
346
+ // Effect.provide(Effect.provide(program, TcpConnectionLive), TcpConfigLive),
347
+ // LoggerLive,
348
+ // ),
349
+ // );
350
+ // });
351
+
352
+ // NodeRuntime.runMain(runnable);
353
+
354
+ export { TcpConfig, TcpConnection, TcpConnectionLive };
@@ -0,0 +1,216 @@
1
+ import {
2
+ Context,
3
+ Duration,
4
+ Effect,
5
+ Fiber,
6
+ Layer,
7
+ Queue,
8
+ Ref,
9
+ Stream,
10
+ pipe,
11
+ Data,
12
+ } from 'effect';
13
+
14
+ // =========================================================================
15
+ // Errors
16
+ // =========================================================================
17
+ export class TcpStreamError extends Data.TaggedError('TcpStreamError')<{
18
+ readonly message: string;
19
+ }> {}
20
+
21
+ // =========================================================================
22
+ // TCP Connection with Write support
23
+ // =========================================================================
24
+ interface TcpStreamShape {
25
+ readonly stream: Stream.Stream<Uint8Array, TcpStreamError>;
26
+ readonly send: (data: Uint8Array) => Effect.Effect<void, TcpStreamError>;
27
+ readonly sendText: (data: string) => Effect.Effect<void, TcpStreamError>;
28
+ readonly close: Effect.Effect<void>;
29
+ }
30
+
31
+ export class TcpStream extends Context.Tag('TcpStream')<
32
+ TcpStream,
33
+ TcpStreamShape
34
+ >() {}
35
+
36
+ /**
37
+ * ConnectionConfigShape is an interface that defines the shape of the data
38
+ * needed to connect to the server.
39
+ */
40
+ interface ConnectionConfigShape {
41
+ host: string;
42
+ port: number;
43
+ magicToken?: string;
44
+ username?: string;
45
+ password?: string;
46
+ tickers?: string[];
47
+ }
48
+
49
+ /**
50
+ * ConnectionConfig is a Context.Tag that provides the connection configuration.
51
+ */
52
+ export class ConnectionConfig extends Context.Tag('ConnectionConfig')<
53
+ ConnectionConfig,
54
+ ConnectionConfigShape
55
+ >() {}
56
+
57
+ export const TcpStreamLive = () =>
58
+ /**
59
+ * Layer.effect() binds an Effect to a Context.Tag and returns
60
+ * a Layer.
61
+ */
62
+ Layer.effect(
63
+ TcpStream,
64
+
65
+ Effect.gen(function* () {
66
+ const config = yield* ConnectionConfig;
67
+
68
+ // Create queues for incoming and outgoing data
69
+ const incomingQueue = yield* Queue.unbounded<Uint8Array>();
70
+ const outgoingQueue = yield* Queue.unbounded<Uint8Array>();
71
+
72
+ // Track error count
73
+ const writeErrorCount = yield* Ref.make(0);
74
+
75
+ // Use refs for coordinated cleanup
76
+ const isClosing = yield* Ref.make(false);
77
+
78
+ // Safely shut down once
79
+ const performShutdown = Effect.gen(function* () {
80
+ const alreadyClosing = yield* Ref.getAndSet(isClosing, true);
81
+ if (alreadyClosing) return;
82
+ yield* Effect.log('Closing TCP connection');
83
+ bunSocket.end();
84
+
85
+ // Allow socket events to propagate
86
+ yield* Effect.sleep(Duration.millis(10));
87
+
88
+ // Interrupt writer fiber before shutting down queues
89
+ // TODO: isn't the writing fiber already interrupted when sutting down the Queue?
90
+ yield* Fiber.interrupt(writerFiber);
91
+
92
+ yield* Effect.all([
93
+ Queue.shutdown(incomingQueue),
94
+ Queue.shutdown(outgoingQueue),
95
+ ]);
96
+ });
97
+
98
+ // Create deferred for connection cleanup
99
+ const bunSocket = yield* Effect.tryPromise(() =>
100
+ Bun.connect({
101
+ port: config.port,
102
+ hostname: config.host,
103
+ socket: {
104
+ data(_socket, data) {
105
+ Queue.unsafeOffer(incomingQueue, data);
106
+ },
107
+ error(_socket, _error) {
108
+ //Queue.unsafeOffer(incomingQueue, error)
109
+ Effect.runPromise(performShutdown);
110
+ },
111
+ close(_socket) {
112
+ Effect.runPromise(performShutdown);
113
+ },
114
+ },
115
+ }),
116
+ ).pipe(
117
+ Effect.timeout(Duration.millis(3000)),
118
+ Effect.flatMap((maybeSocket) =>
119
+ maybeSocket
120
+ ? Effect.succeed(maybeSocket)
121
+ : Effect.fail(
122
+ new TcpStreamError({ message: 'Connection timeout' }),
123
+ ),
124
+ ),
125
+ );
126
+
127
+ // Fiber for writing outgoing data
128
+ const writerFiber = yield* pipe(
129
+ Effect.iterate(undefined, {
130
+ while: () => true,
131
+ body: () =>
132
+ pipe(
133
+ Queue.take(outgoingQueue),
134
+ Effect.flatMap((data) =>
135
+ Effect.try({
136
+ try: () => {
137
+ const bytesWritten = bunSocket.write(data);
138
+ if (bytesWritten !== data.length) {
139
+ throw new Error('Partial write');
140
+ }
141
+ // Reset error count on success
142
+ Effect.runSync(Ref.set(writeErrorCount, 0));
143
+ },
144
+ catch: (error) => {
145
+ const currentErrors = Effect.runSync(
146
+ Ref.updateAndGet(writeErrorCount, (n) => n + 1),
147
+ );
148
+ if (currentErrors > 3) {
149
+ // Too many errors, close the socket
150
+ return Effect.fail(
151
+ new TcpStreamError({
152
+ message: `Write failed after ${currentErrors} attempts: ${error}`,
153
+ }),
154
+ );
155
+ }
156
+ // Retry with the same data after a delay
157
+ return Effect.sleep(Duration.millis(100)).pipe(
158
+ Effect.flatMap(() => Queue.offer(outgoingQueue, data)),
159
+ );
160
+ },
161
+ }),
162
+ ),
163
+ ),
164
+ }),
165
+ Effect.fork,
166
+ );
167
+
168
+ // Cleanup procedure
169
+ const close = Effect.gen(function* () {
170
+ yield* Effect.void;
171
+ yield* performShutdown;
172
+ });
173
+
174
+ // returns TCPStream implementation
175
+ return TcpStream.of({
176
+ stream: Stream.fromQueue(incomingQueue).pipe(
177
+ Stream.mapError((e) => new TcpStreamError({ message: String(e) })),
178
+ Stream.ensuring(close),
179
+ ),
180
+ // TODO: yield* Queue.isShutdown(queue) and only offer if false.
181
+ send: (data: Uint8Array) =>
182
+ Queue.offer(outgoingQueue, data).pipe(
183
+ Effect.mapError((e) => new TcpStreamError({ message: String(e) })),
184
+ ),
185
+
186
+ sendText: (data: string) =>
187
+ // TODO: yield* Queue.isShutdown(queue) and only offer if false.
188
+ Queue.offer(outgoingQueue, new TextEncoder().encode(data)).pipe(
189
+ Effect.mapError((e) => new TcpStreamError({ message: String(e) })),
190
+ ),
191
+ close,
192
+ });
193
+ }),
194
+ );
195
+
196
+ export const ConnectionConfigLive = (
197
+ host: string,
198
+ port: number,
199
+ tickers: string[],
200
+ magicToken: string,
201
+ username: string,
202
+ password: string,
203
+ ) =>
204
+ Layer.scoped(
205
+ ConnectionConfig,
206
+ Effect.succeed({
207
+ host,
208
+ port,
209
+ magicToken,
210
+ username,
211
+ password,
212
+ tickers,
213
+ }),
214
+ );
215
+
216
+ // Redundant export removed
package/src/utils.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Effect, pipe, Schedule } from 'effect';
2
+
3
+ // Send "ping" every second
4
+ export const ping = (
5
+ send: (data: Uint8Array) => Effect.Effect<void, never, never>,
6
+ ) =>
7
+ pipe(
8
+ Effect.repeat(
9
+ pipe(
10
+ Effect.flatMap(
11
+ Effect.sync(() => Buffer.from('ping')),
12
+ (data) =>
13
+ send(data).pipe(
14
+ Effect.catchAll((error) => Effect.logError(error)),
15
+ // Effect.zipRight(printMetrics), // <-- Add this line
16
+ ),
17
+ ),
18
+ ),
19
+ Schedule.spaced('2 seconds'),
20
+ ),
21
+ Effect.fork,
22
+ );
23
+
24
+ // Retry logic for send
25
+ export const sendWithRetry = (
26
+ send: (data: Uint8Array) => Effect.Effect<void, never, never>,
27
+ data: Uint8Array,
28
+ ) =>
29
+ send(data).pipe(
30
+ Effect.retry(
31
+ Schedule.exponential('100 millis', 2).pipe(
32
+ Schedule.compose(Schedule.recurUpTo(5)),
33
+ ),
34
+ ),
35
+ Effect.catchAll((error) => Effect.logError(error)),
36
+ );