@cortexkit/subc-client 0.1.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/src/socket.ts ADDED
@@ -0,0 +1,239 @@
1
+ // A pull-based buffered wrapper over a node TCP socket. Node sockets are
2
+ // event-driven; the handshake and frame loop want "give me exactly N bytes, but
3
+ // don't exceed this absolute deadline". readExact provides that, draining an
4
+ // internal buffer and parking a single waiter until enough bytes arrive, the
5
+ // deadline passes, or the socket ends.
6
+
7
+ import net from "node:net";
8
+
9
+ export class SocketClosedError extends Error {}
10
+ export class SocketTimeoutError extends Error {}
11
+
12
+ export class SocketWriteNotQueuedError extends Error {
13
+ constructor(
14
+ message: string,
15
+ readonly cause?: Error,
16
+ ) {
17
+ super(message);
18
+ }
19
+ }
20
+
21
+ export class SocketWriteQueuedError extends Error {
22
+ constructor(
23
+ message: string,
24
+ readonly cause?: Error,
25
+ ) {
26
+ super(message);
27
+ }
28
+ }
29
+
30
+ export interface SocketWriteResult {
31
+ /** True once bytes were handed to Node's net.Socket.write. This is not a delivery guarantee. */
32
+ queued: boolean;
33
+ /** Resolves when Node reports the write complete; rejects with a classified write error. */
34
+ completed: Promise<void>;
35
+ }
36
+
37
+ interface Waiter {
38
+ need: number;
39
+ resolve: (bytes: Uint8Array) => void;
40
+ reject: (err: Error) => void;
41
+ timer: ReturnType<typeof setTimeout> | null;
42
+ }
43
+
44
+ export class SubcSocket {
45
+ private readonly sock: net.Socket;
46
+ private chunks: Buffer[] = [];
47
+ private buffered = 0;
48
+ private waiter: Waiter | null = null;
49
+ private closedErr: Error | null = null;
50
+
51
+ private constructor(sock: net.Socket) {
52
+ this.sock = sock;
53
+ sock.on("data", (chunk: Buffer) => {
54
+ this.chunks.push(chunk);
55
+ this.buffered += chunk.length;
56
+ this.tryServe();
57
+ });
58
+ const fail = (err: Error) => {
59
+ if (!this.closedErr) this.closedErr = err;
60
+ this.tryServe();
61
+ };
62
+ sock.on("error", (err) => fail(err instanceof Error ? err : new Error(String(err))));
63
+ sock.on("end", () => fail(new SocketClosedError("subc closed the connection")));
64
+ sock.on("close", () => fail(new SocketClosedError("subc connection closed")));
65
+ }
66
+
67
+ static connect(host: string, port: number, deadlineMs: number): Promise<SubcSocket> {
68
+ return new Promise((resolve, reject) => {
69
+ const sock = net.connect({ host, port });
70
+ sock.setNoDelay(true);
71
+ const timer = setTimeout(() => {
72
+ sock.destroy();
73
+ reject(new SocketTimeoutError(`timed out connecting to ${host}:${port}`));
74
+ }, Math.max(0, deadlineMs - Date.now()));
75
+ sock.once("connect", () => {
76
+ clearTimeout(timer);
77
+ resolve(new SubcSocket(sock));
78
+ });
79
+ sock.once("error", (err) => {
80
+ clearTimeout(timer);
81
+ reject(err);
82
+ });
83
+ });
84
+ }
85
+
86
+ /** Read exactly `n` bytes, rejecting if `deadlineMs` (epoch ms) passes first. */
87
+ readExact(n: number, deadlineMs: number): Promise<Uint8Array> {
88
+ if (this.waiter) {
89
+ return Promise.reject(new Error("concurrent readExact is not supported"));
90
+ }
91
+ if (n === 0) return Promise.resolve(new Uint8Array(0));
92
+ return new Promise<Uint8Array>((resolve, reject) => {
93
+ // A non-finite deadline means "wait indefinitely" (the background frame
94
+ // loop relies on this; per-request timeouts live on the request waiters).
95
+ let timer: ReturnType<typeof setTimeout> | null = null;
96
+ if (Number.isFinite(deadlineMs)) {
97
+ const remaining = deadlineMs - Date.now();
98
+ if (remaining <= 0) {
99
+ reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
100
+ return;
101
+ }
102
+ timer = setTimeout(() => {
103
+ this.waiter = null;
104
+ reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
105
+ }, remaining);
106
+ }
107
+ this.waiter = { need: n, resolve, reject, timer };
108
+ this.tryServe();
109
+ });
110
+ }
111
+
112
+ async write(bytes: Uint8Array, deadlineMs: number): Promise<void> {
113
+ try {
114
+ await this.writeTracked(bytes, deadlineMs).completed;
115
+ } catch (err) {
116
+ if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError) {
117
+ throw err.cause ?? err;
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+
123
+ writeTracked(bytes: Uint8Array, deadlineMs: number): SocketWriteResult {
124
+ if (this.closedErr) {
125
+ return {
126
+ queued: false,
127
+ completed: Promise.reject(
128
+ new SocketWriteNotQueuedError("subc socket was closed before bytes could be queued", this.closedErr),
129
+ ),
130
+ };
131
+ }
132
+
133
+ let queued = false;
134
+ let settled = false;
135
+ let timer: ReturnType<typeof setTimeout> | null = null;
136
+ const completed = new Promise<void>((resolve, reject) => {
137
+ const settle = (run: () => void): void => {
138
+ if (settled) return;
139
+ settled = true;
140
+ if (timer) clearTimeout(timer);
141
+ run();
142
+ };
143
+
144
+ const remaining = deadlineMs - Date.now();
145
+ if (remaining <= 0) {
146
+ settle(() =>
147
+ reject(
148
+ new SocketWriteNotQueuedError(
149
+ "timed out before bytes could be queued to subc",
150
+ new SocketTimeoutError("timed out writing to subc"),
151
+ ),
152
+ ),
153
+ );
154
+ return;
155
+ }
156
+
157
+ timer = setTimeout(() => {
158
+ const timeout = new SocketTimeoutError("timed out writing to subc");
159
+ settle(() =>
160
+ reject(
161
+ queued
162
+ ? new SocketWriteQueuedError("timed out after bytes were handed to the subc socket", timeout)
163
+ : new SocketWriteNotQueuedError("timed out before bytes could be queued to subc", timeout),
164
+ ),
165
+ );
166
+ }, remaining);
167
+
168
+ try {
169
+ this.sock.write(Buffer.from(bytes), (err) => {
170
+ settle(() => {
171
+ if (err) {
172
+ reject(
173
+ new SocketWriteQueuedError(
174
+ "subc socket reported a write error after bytes were handed to the socket",
175
+ err instanceof Error ? err : new Error(String(err)),
176
+ ),
177
+ );
178
+ } else {
179
+ resolve();
180
+ }
181
+ });
182
+ });
183
+ queued = true;
184
+ } catch (err) {
185
+ settle(() =>
186
+ reject(
187
+ new SocketWriteNotQueuedError(
188
+ "subc socket write threw before bytes could be queued",
189
+ err instanceof Error ? err : new Error(String(err)),
190
+ ),
191
+ ),
192
+ );
193
+ }
194
+ });
195
+
196
+ return { queued, completed };
197
+ }
198
+
199
+ close(): void {
200
+ this.sock.destroy();
201
+ }
202
+
203
+ private tryServe(): void {
204
+ const w = this.waiter;
205
+ if (!w) return;
206
+ if (this.buffered >= w.need) {
207
+ const out = this.take(w.need);
208
+ this.waiter = null;
209
+ if (w.timer) clearTimeout(w.timer);
210
+ w.resolve(out);
211
+ return;
212
+ }
213
+ if (this.closedErr) {
214
+ this.waiter = null;
215
+ if (w.timer) clearTimeout(w.timer);
216
+ w.reject(this.closedErr);
217
+ }
218
+ }
219
+
220
+ private take(n: number): Uint8Array {
221
+ const out = Buffer.allocUnsafe(n);
222
+ let off = 0;
223
+ while (off < n) {
224
+ const head = this.chunks[0]!;
225
+ const want = n - off;
226
+ if (head.length <= want) {
227
+ head.copy(out, off);
228
+ off += head.length;
229
+ this.chunks.shift();
230
+ } else {
231
+ head.copy(out, off, 0, want);
232
+ this.chunks[0] = head.subarray(want);
233
+ off += want;
234
+ }
235
+ }
236
+ this.buffered -= n;
237
+ return out;
238
+ }
239
+ }