@6qat/tcp-connection 0.2.8 → 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.
@@ -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
- export declare const TcpStreamLive: () => Layer.Layer<TcpStream, TcpStreamError | import("effect/Cause").UnknownException | import("effect/Cause").TimeoutException, ConnectionConfig>;
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.8",
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",
@@ -23,7 +23,7 @@
23
23
  "build:types": "bun tsc --emitDeclarationOnly --outDir dist",
24
24
  "build": "bun run build:main && bun run build:types",
25
25
  "prepublishOnly": "bun run build",
26
- "test": "vitest run --passWithNoTests",
26
+ "test": "bunx --bun vitest run --passWithNoTests",
27
27
  "format": "biome format --write ./src",
28
28
  "lint": "biome lint ."
29
29
  },
@@ -43,7 +43,8 @@
43
43
  "devDependencies": {
44
44
  "@effect/language-service": "^0.63.2",
45
45
  "@types/bun": "^1.3.5",
46
- "typescript": "^5.9.3"
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.0.17"
47
48
  },
48
49
  "publishConfig": {
49
50
  "access": "public"
@@ -7,7 +7,6 @@ import {
7
7
  Queue,
8
8
  Ref,
9
9
  Stream,
10
- pipe,
11
10
  Data,
12
11
  } from 'effect';
13
12
 
@@ -54,141 +53,160 @@ export class ConnectionConfig extends Context.Tag('ConnectionConfig')<
54
53
  ConnectionConfigShape
55
54
  >() {}
56
55
 
56
+ /**
57
+ * Creates the TcpStream layer using Layer.scoped for proper resource management.
58
+ */
57
59
  export const TcpStreamLive = () =>
58
- /**
59
- * Layer.effect() binds an Effect to a Context.Tag and returns
60
- * a Layer.
61
- */
62
- Layer.effect(
60
+ Layer.scoped(
63
61
  TcpStream,
64
62
 
65
63
  Effect.gen(function* () {
66
64
  const config = yield* ConnectionConfig;
67
65
 
68
- // Create queues for incoming and outgoing data
69
66
  const incomingQueue = yield* Queue.unbounded<Uint8Array>();
70
67
  const outgoingQueue = yield* Queue.unbounded<Uint8Array>();
71
-
72
- // Track error count
73
68
  const writeErrorCount = yield* Ref.make(0);
74
-
75
- // Use refs for coordinated cleanup
76
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
+ );
77
120
 
78
- // Safely shut down once
79
- const performShutdown = Effect.gen(function* () {
121
+ // Define shutdown effect
122
+ const shutdownEffect = Effect.gen(function* () {
80
123
  const alreadyClosing = yield* Ref.getAndSet(isClosing, true);
81
124
  if (alreadyClosing) return;
125
+
82
126
  yield* Effect.log('Closing TCP connection');
83
127
  bunSocket.end();
84
128
 
85
129
  // Allow socket events to propagate
86
130
  yield* Effect.sleep(Duration.millis(10));
87
131
 
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);
132
+ const fiber = yield* Ref.get(writerFiberRef);
133
+ if (fiber) {
134
+ yield* Fiber.interrupt(fiber);
135
+ }
91
136
 
92
- yield* Effect.all([
93
- Queue.shutdown(incomingQueue),
94
- Queue.shutdown(outgoingQueue),
95
- ]);
137
+ yield* Queue.shutdown(incomingQueue);
138
+ yield* Queue.shutdown(outgoingQueue);
96
139
  });
97
140
 
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
- ),
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
+ }),
163
174
  ),
164
- }),
165
- Effect.fork,
166
- );
167
-
168
- // Cleanup procedure
169
- const close = Effect.gen(function* () {
170
- yield* Effect.void;
171
- yield* performShutdown;
175
+ );
176
+ }
172
177
  });
173
178
 
174
- // returns TCPStream implementation
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
+
175
202
  return TcpStream.of({
176
203
  stream: Stream.fromQueue(incomingQueue).pipe(
177
204
  Stream.mapError((e) => new TcpStreamError({ message: String(e) })),
178
- Stream.ensuring(close),
179
205
  ),
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
-
206
+ send: (data: Uint8Array) => safeOffer(outgoingQueue, data),
186
207
  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,
208
+ safeOffer(outgoingQueue, new TextEncoder().encode(data)),
209
+ close: shutdownEffect,
192
210
  });
193
211
  }),
194
212
  );
@@ -201,16 +219,11 @@ export const ConnectionConfigLive = (
201
219
  username: string,
202
220
  password: string,
203
221
  ) =>
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
222
+ Layer.succeed(ConnectionConfig, {
223
+ host,
224
+ port,
225
+ magicToken,
226
+ username,
227
+ password,
228
+ tickers,
229
+ });
@@ -1 +0,0 @@
1
- export {};
@@ -1,31 +0,0 @@
1
- import { Context, Effect, Layer, Stream } from 'effect';
2
- interface TcpStreamShape {
3
- readonly stream: Stream.Stream<Uint8Array, Error>;
4
- readonly send: (data: Uint8Array) => Effect.Effect<void>;
5
- readonly sendText: (data: string) => Effect.Effect<void>;
6
- readonly close: Effect.Effect<void>;
7
- }
8
- declare const TcpStream_base: Context.TagClass<TcpStream, "TcpStream", TcpStreamShape>;
9
- declare class TcpStream extends TcpStream_base {
10
- }
11
- /**
12
- * ConnectionConfigShape is an interface that defines the shape of the data
13
- * needed to connect to the Cedro server.
14
- */
15
- interface ConnectionConfigShape {
16
- host: string;
17
- port: number;
18
- magicToken?: string;
19
- username?: string;
20
- password?: string;
21
- tickers?: string[];
22
- }
23
- declare const ConnectionConfig_base: Context.TagClass<ConnectionConfig, "ConnectionConfig", ConnectionConfigShape>;
24
- /**
25
- * ConnectionConfig is a Context.Tag that provides the connection configuration.
26
- */
27
- declare class ConnectionConfig extends ConnectionConfig_base {
28
- }
29
- declare const TcpStreamLive: () => Layer.Layer<TcpStream, Error, ConnectionConfig>;
30
- declare const ConnectionConfigLive: (host: string, port: number, tickers: string[], magicToken: string, username: string, password: string) => Layer.Layer<ConnectionConfig, never, never>;
31
- export { ConnectionConfig, ConnectionConfigLive, TcpStream, TcpStreamLive };
@@ -1 +0,0 @@
1
- export * from './tcp-stream';
@@ -1,38 +0,0 @@
1
- import { Context, Effect, Layer, Runtime, Stream } from 'effect';
2
- declare const TcpConnectionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
3
- readonly _tag: "TcpConnectionError";
4
- } & Readonly<A>;
5
- export declare class TcpConnectionError extends TcpConnectionError_base {
6
- readonly errorType?: string | undefined;
7
- constructor(errorType?: string | undefined);
8
- }
9
- export declare class TcpConnectionTimeoutError extends TcpConnectionError {
10
- readonly message: string;
11
- constructor(message: string);
12
- }
13
- export declare class TcpConnectionWriteError extends TcpConnectionError {
14
- readonly message: string;
15
- constructor(message: string);
16
- }
17
- export declare class TcpConnectionCloseError extends TcpConnectionError {
18
- readonly message: string;
19
- constructor(message: string);
20
- }
21
- declare const TcpConnection_base: Context.TagClass<TcpConnection, "TcpConnection", TcpConnectionShape>;
22
- declare class TcpConnection extends TcpConnection_base {
23
- }
24
- interface TcpConnectionShape {
25
- readonly incoming: Stream.Stream<Uint8Array, TcpConnectionError>;
26
- readonly send: (data: Uint8Array) => Effect.Effect<void, TcpConnectionError>;
27
- readonly sendWithRetry: (data: Uint8Array) => Effect.Effect<void, TcpConnectionError>;
28
- }
29
- declare const TcpConfig_base: Context.TagClass<TcpConfig, "TcpConfig", {
30
- host: string;
31
- port: number;
32
- bufferSize?: number;
33
- runtimeEffect?: Effect.Effect<Runtime.Runtime<never>, never, never>;
34
- }>;
35
- declare class TcpConfig extends TcpConfig_base {
36
- }
37
- declare const TcpConnectionLive: Layer.Layer<TcpConnection, TcpConnectionError, TcpConfig>;
38
- export { TcpConfig, TcpConnection, TcpConnectionLive };
@@ -1,38 +0,0 @@
1
- import { Context, Effect, Layer, Stream } from 'effect';
2
- declare const TcpStreamError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
3
- readonly _tag: "TcpStreamError";
4
- } & Readonly<A>;
5
- export declare class TcpStreamError extends TcpStreamError_base<{
6
- readonly message: string;
7
- }> {
8
- }
9
- interface TcpStreamShape {
10
- readonly stream: Stream.Stream<Uint8Array, TcpStreamError>;
11
- readonly send: (data: Uint8Array) => Effect.Effect<void, TcpStreamError>;
12
- readonly sendText: (data: string) => Effect.Effect<void, TcpStreamError>;
13
- readonly close: Effect.Effect<void>;
14
- }
15
- declare const TcpStream_base: Context.TagClass<TcpStream, "TcpStream", TcpStreamShape>;
16
- declare class TcpStream extends TcpStream_base {
17
- }
18
- /**
19
- * ConnectionConfigShape is an interface that defines the shape of the data
20
- * needed to connect to the server.
21
- */
22
- interface ConnectionConfigShape {
23
- host: string;
24
- port: number;
25
- magicToken?: string;
26
- username?: string;
27
- password?: string;
28
- tickers?: string[];
29
- }
30
- declare const ConnectionConfig_base: Context.TagClass<ConnectionConfig, "ConnectionConfig", ConnectionConfigShape>;
31
- /**
32
- * ConnectionConfig is a Context.Tag that provides the connection configuration.
33
- */
34
- declare class ConnectionConfig extends ConnectionConfig_base {
35
- }
36
- declare const TcpStreamLive: () => Layer.Layer<TcpStream, import("effect/Cause").UnknownException | import("effect/Cause").TimeoutException | TcpStreamError, ConnectionConfig>;
37
- declare const ConnectionConfigLive: (host: string, port: number, tickers: string[], magicToken: string, username: string, password: string) => Layer.Layer<ConnectionConfig, never, never>;
38
- export { ConnectionConfig, ConnectionConfigLive, TcpStream, TcpStreamLive };
@@ -1,38 +0,0 @@
1
- import { Context, Effect, Layer, Stream } from 'effect';
2
- declare const TcpStreamError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
3
- readonly _tag: "TcpStreamError";
4
- } & Readonly<A>;
5
- export declare class TcpStreamError extends TcpStreamError_base<{
6
- readonly message: string;
7
- }> {
8
- }
9
- interface TcpStreamShape {
10
- readonly stream: Stream.Stream<Uint8Array, TcpStreamError>;
11
- readonly send: (data: Uint8Array) => Effect.Effect<void, TcpStreamError>;
12
- readonly sendText: (data: string) => Effect.Effect<void, TcpStreamError>;
13
- readonly close: Effect.Effect<void>;
14
- }
15
- declare const TcpStream_base: Context.TagClass<TcpStream, "TcpStream", TcpStreamShape>;
16
- export declare class TcpStream extends TcpStream_base {
17
- }
18
- /**
19
- * ConnectionConfigShape is an interface that defines the shape of the data
20
- * needed to connect to the server.
21
- */
22
- interface ConnectionConfigShape {
23
- host: string;
24
- port: number;
25
- magicToken?: string;
26
- username?: string;
27
- password?: string;
28
- tickers?: string[];
29
- }
30
- declare const ConnectionConfig_base: Context.TagClass<ConnectionConfig, "ConnectionConfig", ConnectionConfigShape>;
31
- /**
32
- * ConnectionConfig is a Context.Tag that provides the connection configuration.
33
- */
34
- export declare class ConnectionConfig extends ConnectionConfig_base {
35
- }
36
- export declare const TcpStreamLive: () => Layer.Layer<TcpStream, TcpStreamError | import("effect/Cause").UnknownException | import("effect/Cause").TimeoutException, ConnectionConfig>;
37
- export declare const ConnectionConfigLive: (host: string, port: number, tickers: string[], magicToken: string, username: string, password: string) => Layer.Layer<ConnectionConfig, never, never>;
38
- export {};
package/dist/utils.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import { Effect } from 'effect';
2
- export declare const ping: (send: (data: Uint8Array) => Effect.Effect<void, never, never>) => Effect.Effect<import("effect/Fiber").RuntimeFiber<number, never>, never, never>;
3
- export declare const sendWithRetry: (send: (data: Uint8Array) => Effect.Effect<void, never, never>, data: Uint8Array) => Effect.Effect<void, never, never>;
File without changes
File without changes
File without changes