@6qat/tcp-connection 0.2.6 → 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.
@@ -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
+ );