@6qat/tcp-connection 0.2.7 → 0.2.9
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 +1 -1
- package/dist/{tcp-connection.d.ts → src/tcp-connection.d.ts} +4 -1
- package/package.json +5 -3
- package/src/examples.ts +62 -0
- package/src/index.ts +4 -0
- package/src/tcp-connection-OLD.ts +354 -0
- package/src/tcp-connection.ts +229 -0
- package/src/utils.ts +36 -0
- package/dist/lab-effect/src/log-level.d.ts +0 -1
- package/dist/m-cedro/src/tcp-stream.d.ts +0 -31
- package/dist/tcp-connection/src/index.d.ts +0 -1
- package/dist/tcp-connection/src/tcp-connection.d.ts +0 -38
- package/dist/tcp-connection/src/tcp-stream.d.ts +0 -38
- package/dist/tcp-stream.d.ts +0 -38
- package/dist/utils.d.ts +0 -3
- /package/dist/{examples.d.ts → src/examples.d.ts} +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{tcp-connection-OLD.d.ts → src/tcp-connection-OLD.d.ts} +0 -0
- /package/dist/{tcp-connection/src → src}/utils.d.ts +0 -0
- /package/dist/{tcp-connection/src → tests/unit}/tcp-connection.test.d.ts +0 -0
|
@@ -33,6 +33,9 @@ declare const ConnectionConfig_base: Context.TagClass<ConnectionConfig, "Connect
|
|
|
33
33
|
*/
|
|
34
34
|
export declare class ConnectionConfig extends ConnectionConfig_base {
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Creates the TcpStream layer using Layer.scoped for proper resource management.
|
|
38
|
+
*/
|
|
39
|
+
export declare const TcpStreamLive: () => Layer.Layer<TcpStream, TcpStreamError | import("effect/Cause").TimeoutException, ConnectionConfig>;
|
|
37
40
|
export declare const ConnectionConfigLive: (host: string, port: number, tickers: string[], magicToken: string, username: string, password: string) => Layer.Layer<ConnectionConfig, never, never>;
|
|
38
41
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@6qat/tcp-connection",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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": {
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"build:types": "bun tsc --emitDeclarationOnly --outDir dist",
|
|
23
24
|
"build": "bun run build:main && bun run build:types",
|
|
24
25
|
"prepublishOnly": "bun run build",
|
|
25
|
-
"test": "vitest run --passWithNoTests",
|
|
26
|
+
"test": "bunx --bun vitest run --passWithNoTests",
|
|
26
27
|
"format": "biome format --write ./src",
|
|
27
28
|
"lint": "biome lint ."
|
|
28
29
|
},
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@effect/language-service": "^0.63.2",
|
|
44
45
|
"@types/bun": "^1.3.5",
|
|
45
|
-
"typescript": "^5.9.3"
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^4.0.17"
|
|
46
48
|
},
|
|
47
49
|
"publishConfig": {
|
|
48
50
|
"access": "public"
|
package/src/examples.ts
ADDED
|
@@ -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,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,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Duration,
|
|
4
|
+
Effect,
|
|
5
|
+
Fiber,
|
|
6
|
+
Layer,
|
|
7
|
+
Queue,
|
|
8
|
+
Ref,
|
|
9
|
+
Stream,
|
|
10
|
+
Data,
|
|
11
|
+
} from 'effect';
|
|
12
|
+
|
|
13
|
+
// =========================================================================
|
|
14
|
+
// Errors
|
|
15
|
+
// =========================================================================
|
|
16
|
+
export class TcpStreamError extends Data.TaggedError('TcpStreamError')<{
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}> {}
|
|
19
|
+
|
|
20
|
+
// =========================================================================
|
|
21
|
+
// TCP Connection with Write support
|
|
22
|
+
// =========================================================================
|
|
23
|
+
interface TcpStreamShape {
|
|
24
|
+
readonly stream: Stream.Stream<Uint8Array, TcpStreamError>;
|
|
25
|
+
readonly send: (data: Uint8Array) => Effect.Effect<void, TcpStreamError>;
|
|
26
|
+
readonly sendText: (data: string) => Effect.Effect<void, TcpStreamError>;
|
|
27
|
+
readonly close: Effect.Effect<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class TcpStream extends Context.Tag('TcpStream')<
|
|
31
|
+
TcpStream,
|
|
32
|
+
TcpStreamShape
|
|
33
|
+
>() {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ConnectionConfigShape is an interface that defines the shape of the data
|
|
37
|
+
* needed to connect to the server.
|
|
38
|
+
*/
|
|
39
|
+
interface ConnectionConfigShape {
|
|
40
|
+
host: string;
|
|
41
|
+
port: number;
|
|
42
|
+
magicToken?: string;
|
|
43
|
+
username?: string;
|
|
44
|
+
password?: string;
|
|
45
|
+
tickers?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ConnectionConfig is a Context.Tag that provides the connection configuration.
|
|
50
|
+
*/
|
|
51
|
+
export class ConnectionConfig extends Context.Tag('ConnectionConfig')<
|
|
52
|
+
ConnectionConfig,
|
|
53
|
+
ConnectionConfigShape
|
|
54
|
+
>() {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates the TcpStream layer using Layer.scoped for proper resource management.
|
|
58
|
+
*/
|
|
59
|
+
export const TcpStreamLive = () =>
|
|
60
|
+
Layer.scoped(
|
|
61
|
+
TcpStream,
|
|
62
|
+
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const config = yield* ConnectionConfig;
|
|
65
|
+
|
|
66
|
+
const incomingQueue = yield* Queue.unbounded<Uint8Array>();
|
|
67
|
+
const outgoingQueue = yield* Queue.unbounded<Uint8Array>();
|
|
68
|
+
const writeErrorCount = yield* Ref.make(0);
|
|
69
|
+
const isClosing = yield* Ref.make(false);
|
|
70
|
+
const writerFiberRef = yield* Ref.make<Fiber.Fiber<
|
|
71
|
+
void,
|
|
72
|
+
TcpStreamError
|
|
73
|
+
> | null>(null);
|
|
74
|
+
|
|
75
|
+
// Connect to the server
|
|
76
|
+
const bunSocket = yield* Effect.tryPromise({
|
|
77
|
+
try: () =>
|
|
78
|
+
Bun.connect({
|
|
79
|
+
port: config.port,
|
|
80
|
+
hostname: config.host,
|
|
81
|
+
socket: {
|
|
82
|
+
data(_socket, data) {
|
|
83
|
+
// Use unsafeOffer for immediate queueing
|
|
84
|
+
Queue.unsafeOffer(incomingQueue, data);
|
|
85
|
+
},
|
|
86
|
+
error(_socket, error) {
|
|
87
|
+
Effect.runPromise(
|
|
88
|
+
Ref.get(isClosing).pipe(
|
|
89
|
+
Effect.flatMap((closing) =>
|
|
90
|
+
closing ? Effect.void : shutdownEffect,
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
close(_socket) {
|
|
96
|
+
Effect.runPromise(
|
|
97
|
+
Ref.get(isClosing).pipe(
|
|
98
|
+
Effect.flatMap((closing) =>
|
|
99
|
+
closing ? Effect.void : shutdownEffect,
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
catch: (error) =>
|
|
107
|
+
new TcpStreamError({
|
|
108
|
+
message: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
109
|
+
}),
|
|
110
|
+
}).pipe(
|
|
111
|
+
Effect.timeout(Duration.millis(3000)),
|
|
112
|
+
Effect.flatMap((maybeSocket) =>
|
|
113
|
+
maybeSocket
|
|
114
|
+
? Effect.succeed(maybeSocket)
|
|
115
|
+
: Effect.fail(
|
|
116
|
+
new TcpStreamError({ message: 'Connection timeout' }),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Define shutdown effect
|
|
122
|
+
const shutdownEffect = Effect.gen(function* () {
|
|
123
|
+
const alreadyClosing = yield* Ref.getAndSet(isClosing, true);
|
|
124
|
+
if (alreadyClosing) return;
|
|
125
|
+
|
|
126
|
+
yield* Effect.log('Closing TCP connection');
|
|
127
|
+
bunSocket.end();
|
|
128
|
+
|
|
129
|
+
// Allow socket events to propagate
|
|
130
|
+
yield* Effect.sleep(Duration.millis(10));
|
|
131
|
+
|
|
132
|
+
const fiber = yield* Ref.get(writerFiberRef);
|
|
133
|
+
if (fiber) {
|
|
134
|
+
yield* Fiber.interrupt(fiber);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
yield* Queue.shutdown(incomingQueue);
|
|
138
|
+
yield* Queue.shutdown(outgoingQueue);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Writer loop
|
|
142
|
+
const writerLoop = Effect.gen(function* () {
|
|
143
|
+
while (true) {
|
|
144
|
+
const isShutdown = yield* Queue.isShutdown(outgoingQueue);
|
|
145
|
+
if (isShutdown) break;
|
|
146
|
+
|
|
147
|
+
const data = yield* Queue.take(outgoingQueue);
|
|
148
|
+
|
|
149
|
+
yield* Effect.gen(function* () {
|
|
150
|
+
const bytesWritten = bunSocket.write(data);
|
|
151
|
+
if (bytesWritten !== data.length) {
|
|
152
|
+
return yield* Effect.fail(
|
|
153
|
+
new TcpStreamError({ message: 'Partial write' }),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
yield* Ref.set(writeErrorCount, 0);
|
|
157
|
+
}).pipe(
|
|
158
|
+
Effect.catchAll((error) =>
|
|
159
|
+
Effect.gen(function* () {
|
|
160
|
+
const currentErrors = yield* Ref.updateAndGet(
|
|
161
|
+
writeErrorCount,
|
|
162
|
+
(n) => n + 1,
|
|
163
|
+
);
|
|
164
|
+
if (currentErrors > 3) {
|
|
165
|
+
return yield* Effect.fail(
|
|
166
|
+
new TcpStreamError({
|
|
167
|
+
message: `Write failed after ${currentErrors} attempts: ${error}`,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
yield* Effect.sleep(Duration.millis(100));
|
|
172
|
+
yield* Queue.offer(outgoingQueue, data);
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const writerFiber = yield* Effect.fork(writerLoop);
|
|
180
|
+
yield* Ref.set(writerFiberRef, writerFiber);
|
|
181
|
+
|
|
182
|
+
// Register finalizer
|
|
183
|
+
yield* Effect.addFinalizer(() => shutdownEffect);
|
|
184
|
+
|
|
185
|
+
const safeOffer = (queue: Queue.Queue<Uint8Array>, data: Uint8Array) =>
|
|
186
|
+
Effect.gen(function* () {
|
|
187
|
+
const isShutdown = yield* Queue.isShutdown(queue);
|
|
188
|
+
if (isShutdown) {
|
|
189
|
+
return yield* Effect.fail(
|
|
190
|
+
new TcpStreamError({ message: 'Queue is shutdown' }),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
yield* Queue.offer(queue, data);
|
|
194
|
+
}).pipe(
|
|
195
|
+
Effect.mapError((e) =>
|
|
196
|
+
e instanceof TcpStreamError
|
|
197
|
+
? e
|
|
198
|
+
: new TcpStreamError({ message: String(e) }),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return TcpStream.of({
|
|
203
|
+
stream: Stream.fromQueue(incomingQueue).pipe(
|
|
204
|
+
Stream.mapError((e) => new TcpStreamError({ message: String(e) })),
|
|
205
|
+
),
|
|
206
|
+
send: (data: Uint8Array) => safeOffer(outgoingQueue, data),
|
|
207
|
+
sendText: (data: string) =>
|
|
208
|
+
safeOffer(outgoingQueue, new TextEncoder().encode(data)),
|
|
209
|
+
close: shutdownEffect,
|
|
210
|
+
});
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
export const ConnectionConfigLive = (
|
|
215
|
+
host: string,
|
|
216
|
+
port: number,
|
|
217
|
+
tickers: string[],
|
|
218
|
+
magicToken: string,
|
|
219
|
+
username: string,
|
|
220
|
+
password: string,
|
|
221
|
+
) =>
|
|
222
|
+
Layer.succeed(ConnectionConfig, {
|
|
223
|
+
host,
|
|
224
|
+
port,
|
|
225
|
+
magicToken,
|
|
226
|
+
username,
|
|
227
|
+
password,
|
|
228
|
+
tickers,
|
|
229
|
+
});
|