@cortexkit/subc-client 0.21.0 → 0.21.2
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/client.d.ts +10 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -12
- package/dist/client.js.map +1 -1
- package/dist/provider.d.ts +6 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +4 -1
- package/dist/provider.js.map +1 -1
- package/dist/socket.d.ts +29 -2
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +105 -14
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +44 -12
- package/src/provider.ts +11 -1
- package/src/socket.ts +111 -15
package/src/provider.ts
CHANGED
|
@@ -312,6 +312,12 @@ export interface SubcProviderConnectOptions {
|
|
|
312
312
|
sleep?: (ms: number) => Promise<void>;
|
|
313
313
|
/** Milliseconds to wait before emitting restored after the provider re-registers following a reconnect. */
|
|
314
314
|
restoredDebounceMs?: number;
|
|
315
|
+
/**
|
|
316
|
+
* Deadline budget (ms) without progress for reading a frame body after its
|
|
317
|
+
* header arrives. Each incoming chunk pushes the deadline out again.
|
|
318
|
+
* Defaults to 30,000 ms.
|
|
319
|
+
*/
|
|
320
|
+
bodyReadTimeoutMs?: number;
|
|
315
321
|
/** Callback that receives ProviderConnectionState events one at a time and in order. */
|
|
316
322
|
onConnectionState?: (event: ProviderConnectionState) => void | Promise<void>;
|
|
317
323
|
/**
|
|
@@ -351,6 +357,7 @@ interface NormalizedSubcProviderConnectOptions {
|
|
|
351
357
|
reconnectBackoff: ReconnectBackoff;
|
|
352
358
|
sleep: (ms: number) => Promise<void>;
|
|
353
359
|
restoredDebounceMs: number;
|
|
360
|
+
bodyReadTimeoutMs?: number;
|
|
354
361
|
onConnectionState?: (event: ProviderConnectionState) => void | Promise<void>;
|
|
355
362
|
/** The launch nonce to echo in HELLO, already resolved against the environment. */
|
|
356
363
|
launchNonce?: string;
|
|
@@ -718,7 +725,9 @@ export class SubcProvider {
|
|
|
718
725
|
const control: ControlPushState = { undecodablePushLogged: false, drainingHooks: new Set() };
|
|
719
726
|
try {
|
|
720
727
|
for (;;) {
|
|
721
|
-
const frame = await sock.readFrame(Number.POSITIVE_INFINITY, {
|
|
728
|
+
const frame = await sock.readFrame(Number.POSITIVE_INFINITY, {
|
|
729
|
+
afterHeaderMs: this.opts.bodyReadTimeoutMs ?? BODY_READ_TIMEOUT_MS,
|
|
730
|
+
});
|
|
722
731
|
const keepGoing = await this.dispatch(frame, sock, generation, control);
|
|
723
732
|
if (!keepGoing) {
|
|
724
733
|
if (this.sock === sock && this.generation === generation) {
|
|
@@ -1493,6 +1502,7 @@ function normalizeProviderConnectOptions(opts: SubcProviderConnectOptions): Norm
|
|
|
1493
1502
|
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
1494
1503
|
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1495
1504
|
restoredDebounceMs: opts.restoredDebounceMs ?? DEFAULT_RESTORED_DEBOUNCE_MS,
|
|
1505
|
+
bodyReadTimeoutMs: opts.bodyReadTimeoutMs,
|
|
1496
1506
|
onConnectionState: opts.onConnectionState,
|
|
1497
1507
|
launchNonce: nonEmpty(opts.launchNonce ?? envLaunchNonce),
|
|
1498
1508
|
reconnectOnDrop: opts.reconnectOnDrop ?? !supervised,
|
package/src/socket.ts
CHANGED
|
@@ -71,8 +71,18 @@ interface Waiter {
|
|
|
71
71
|
resolve: (bytes: Uint8Array) => void;
|
|
72
72
|
reject: (err: Error) => void;
|
|
73
73
|
timer: ReturnType<typeof setTimeout> | null;
|
|
74
|
+
immediate: ReturnType<typeof setImmediate> | null;
|
|
75
|
+
budget: number;
|
|
76
|
+
/**
|
|
77
|
+
* True for an idle budget (time without progress), which arriving bytes
|
|
78
|
+
* extend. False for an absolute deadline, such as the handshake's, which is
|
|
79
|
+
* a fixed bound: a peer that trickles bytes must not be able to stretch it.
|
|
80
|
+
*/
|
|
81
|
+
extendOnProgress: boolean;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
export type ReadDeadline = number | { idleTimeoutMs: number };
|
|
85
|
+
|
|
76
86
|
export class SubcSocket {
|
|
77
87
|
private readonly sock: net.Socket;
|
|
78
88
|
private chunks: Buffer[] = [];
|
|
@@ -92,6 +102,17 @@ export class SubcSocket {
|
|
|
92
102
|
sock.on("data", (chunk: Buffer) => {
|
|
93
103
|
this.chunks.push(chunk);
|
|
94
104
|
this.buffered += chunk.length;
|
|
105
|
+
if (this.waiter) {
|
|
106
|
+
if (this.waiter.immediate) {
|
|
107
|
+
clearImmediate(this.waiter.immediate);
|
|
108
|
+
this.waiter.immediate = null;
|
|
109
|
+
}
|
|
110
|
+
if (this.waiter.extendOnProgress && this.buffered < this.waiter.need) {
|
|
111
|
+
// Progress was made on the pending read: push the deadline out
|
|
112
|
+
// by the budget again so a slow but moving stream does not time out.
|
|
113
|
+
this.armWaiterTimer(this.waiter);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
this.tryServe();
|
|
96
117
|
});
|
|
97
118
|
const fail = (err: Error) => {
|
|
@@ -163,34 +184,107 @@ export class SubcSocket {
|
|
|
163
184
|
throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
|
|
164
185
|
}
|
|
165
186
|
onHeader?.();
|
|
166
|
-
const
|
|
167
|
-
typeof bodyDeadline === "number" ? bodyDeadline :
|
|
168
|
-
const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len,
|
|
187
|
+
const bodyDeadlineArg =
|
|
188
|
+
typeof bodyDeadline === "number" ? bodyDeadline : { idleTimeoutMs: bodyDeadline.afterHeaderMs };
|
|
189
|
+
const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineArg);
|
|
169
190
|
return { header, body };
|
|
170
191
|
}
|
|
171
192
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Arm or reset the idle timeout timer for a pending waiter.
|
|
195
|
+
*
|
|
196
|
+
* In Node and Bun, expired timers run BEFORE the event loop polls for I/O.
|
|
197
|
+
* If the host process is blocked by CPU-bound work or a busy thread, the
|
|
198
|
+
* deadline timer may expire while bytes from the peer are already waiting in
|
|
199
|
+
* the OS kernel socket receive buffer. When the event loop resumes, the
|
|
200
|
+
* timer fires first, before the socket's 'data' event has had a chance to
|
|
201
|
+
* read those bytes from the kernel.
|
|
202
|
+
*
|
|
203
|
+
* A timer alone therefore cannot distinguish between "the peer never sent
|
|
204
|
+
* data" and "data arrived but has not yet been delivered across the event
|
|
205
|
+
* loop stall". We give pending socket data a chance to be delivered by
|
|
206
|
+
* deferring the timeout check past the I/O poll phase via setImmediate.
|
|
207
|
+
* (In Node, setImmediate runs after the poll phase; in Bun, a second
|
|
208
|
+
* setImmediate ensures the poll phase has run if the first ran before it).
|
|
209
|
+
* We only reject if the read is still unsatisfied and no bytes arrived
|
|
210
|
+
* meanwhile.
|
|
211
|
+
*/
|
|
212
|
+
private armWaiterTimer(w: Waiter): void {
|
|
213
|
+
if (!Number.isFinite(w.budget)) return;
|
|
214
|
+
if (w.timer) {
|
|
215
|
+
clearTimeout(w.timer);
|
|
216
|
+
w.timer = null;
|
|
217
|
+
}
|
|
218
|
+
w.timer = setTimeout(() => {
|
|
219
|
+
w.timer = null;
|
|
220
|
+
const bufferedAtTimeout = this.buffered;
|
|
221
|
+
const verifyTimeout = () => {
|
|
222
|
+
if (this.waiter !== w) return;
|
|
223
|
+
if (w.extendOnProgress && this.buffered > bufferedAtTimeout) {
|
|
224
|
+
// Progress was made: bytes arrived after the timer fired.
|
|
225
|
+
// Push the deadline out by the budget again.
|
|
226
|
+
this.armWaiterTimer(w);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.waiter = null;
|
|
230
|
+
w.reject(new SocketTimeoutError(`timed out waiting for ${w.need} bytes`));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
w.immediate = setImmediate(() => {
|
|
234
|
+
if (this.waiter !== w) return;
|
|
235
|
+
if (this.buffered > bufferedAtTimeout) {
|
|
236
|
+
verifyTimeout();
|
|
237
|
+
} else {
|
|
238
|
+
w.immediate = setImmediate(() => {
|
|
239
|
+
w.immediate = null;
|
|
240
|
+
verifyTimeout();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}, w.budget);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Read exactly `n` bytes, rejecting if `deadline` passes without progress.
|
|
249
|
+
* `deadline` is either an absolute epoch-ms deadline or `{ idleTimeoutMs }`
|
|
250
|
+
* specifying the maximum allowed time without progress (bytes arriving).
|
|
251
|
+
*/
|
|
252
|
+
readExact(n: number, deadline: ReadDeadline): Promise<Uint8Array> {
|
|
174
253
|
if (this.waiter) {
|
|
175
254
|
return Promise.reject(new Error("concurrent readExact is not supported"));
|
|
176
255
|
}
|
|
177
256
|
if (n === 0) return Promise.resolve(new Uint8Array(0));
|
|
178
257
|
return new Promise<Uint8Array>((resolve, reject) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (remaining <= 0) {
|
|
258
|
+
let budget: number;
|
|
259
|
+
const extendOnProgress = typeof deadline === "object";
|
|
260
|
+
if (typeof deadline === "object") {
|
|
261
|
+
budget = deadline.idleTimeoutMs;
|
|
262
|
+
if (budget <= 0) {
|
|
185
263
|
reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
|
|
186
264
|
return;
|
|
187
265
|
}
|
|
188
|
-
|
|
189
|
-
|
|
266
|
+
} else if (Number.isFinite(deadline)) {
|
|
267
|
+
const remaining = deadline - Date.now();
|
|
268
|
+
if (remaining <= 0) {
|
|
190
269
|
reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
|
|
191
|
-
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
budget = remaining;
|
|
273
|
+
} else {
|
|
274
|
+
budget = Number.POSITIVE_INFINITY;
|
|
192
275
|
}
|
|
193
|
-
|
|
276
|
+
|
|
277
|
+
const waiter: Waiter = {
|
|
278
|
+
need: n,
|
|
279
|
+
resolve,
|
|
280
|
+
reject,
|
|
281
|
+
timer: null,
|
|
282
|
+
immediate: null,
|
|
283
|
+
budget,
|
|
284
|
+
extendOnProgress,
|
|
285
|
+
};
|
|
286
|
+
this.waiter = waiter;
|
|
287
|
+
this.armWaiterTimer(waiter);
|
|
194
288
|
this.tryServe();
|
|
195
289
|
});
|
|
196
290
|
}
|
|
@@ -309,12 +403,14 @@ export class SubcSocket {
|
|
|
309
403
|
const out = this.take(w.need);
|
|
310
404
|
this.waiter = null;
|
|
311
405
|
if (w.timer) clearTimeout(w.timer);
|
|
406
|
+
if (w.immediate) clearImmediate(w.immediate);
|
|
312
407
|
w.resolve(out);
|
|
313
408
|
return;
|
|
314
409
|
}
|
|
315
410
|
if (this.closedErr) {
|
|
316
411
|
this.waiter = null;
|
|
317
412
|
if (w.timer) clearTimeout(w.timer);
|
|
413
|
+
if (w.immediate) clearImmediate(w.immediate);
|
|
318
414
|
w.reject(this.closedErr);
|
|
319
415
|
}
|
|
320
416
|
}
|