@blaxel/core 0.2.86 → 0.2.87-preview.168
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/h2fetch.js +64 -16
- package/dist/cjs/common/settings.js +2 -2
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/h2fetch.js +64 -16
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/settings.js +2 -2
- package/package.json +1 -1
|
@@ -11,14 +11,21 @@ const sessionsWithListenerBudget = new WeakSet();
|
|
|
11
11
|
* When `settings.maxConcurrentH2Requests` is `0`/unset the gate is a no-op
|
|
12
12
|
* (current default behavior: unlimited concurrency).
|
|
13
13
|
*/
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
14
|
+
// Last-resort leak guard for a held slot. The slot is now released on the real
|
|
15
|
+
// stream terminal (end / error / abort / cancel / pre-response reject), which
|
|
16
|
+
// always fires, so this timer is a pure backstop for the one residual case: a
|
|
17
|
+
// request that never opens a stream AND never settles (no response, no error,
|
|
18
|
+
// no abort) — it must not pin a slot forever and starve the per-domain queue.
|
|
19
|
+
//
|
|
20
|
+
// When it fires it ONLY frees the queue slot. It NEVER aborts or cancels the
|
|
21
|
+
// underlying request or its response stream: `release` does not touch `req` or
|
|
22
|
+
// the body stream, so a long-lived streaming response keeps flowing. Raised
|
|
23
|
+
// well beyond any legitimate stream lifetime so it cannot interfere with a
|
|
24
|
+
// healthy long-open stream (process.streamLogs / execWithStreaming / port
|
|
25
|
+
// proxy). RESIDUAL: a stream that outlives this backstop would have its slot
|
|
26
|
+
// freed early (the queue could then admit one extra stream); acceptable as a
|
|
27
|
+
// pure backstop, since the slot is otherwise tied to the true stream terminal.
|
|
28
|
+
const H2_SLOT_TIMEOUT_MS = 1_800_000; // 30 minutes
|
|
22
29
|
const h2GatesByDomain = new Map();
|
|
23
30
|
function getH2Gate(domain) {
|
|
24
31
|
let gate = h2GatesByDomain.get(domain);
|
|
@@ -29,10 +36,14 @@ function getH2Gate(domain) {
|
|
|
29
36
|
return gate;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
|
-
* Acquire an
|
|
33
|
-
* that is idempotent and FIFO-fair: releasing wakes the longest-waiting
|
|
34
|
-
*
|
|
35
|
-
*
|
|
39
|
+
* Acquire an OPEN-STREAM slot for `domain`. Resolves with a release function
|
|
40
|
+
* that is idempotent and FIFO-fair: releasing wakes the longest-waiting queued
|
|
41
|
+
* caller for the same domain. The slot is held for the lifetime of an OPEN H2
|
|
42
|
+
* stream — the send path releases it on the stream terminal — so the gate
|
|
43
|
+
* bounds true concurrent streams on the one shared session, not merely requests
|
|
44
|
+
* awaiting headers (ENG-2678). A backstop timer releases the slot if a request
|
|
45
|
+
* neither opens a stream nor ever settles, preventing per-domain starvation;
|
|
46
|
+
* it never aborts the underlying request (see `H2_SLOT_TIMEOUT_MS`).
|
|
36
47
|
*/
|
|
37
48
|
async function acquireH2Slot(domain) {
|
|
38
49
|
const max = settings.maxConcurrentH2Requests; // 0/undefined = unlimited
|
|
@@ -94,6 +105,12 @@ export function createH2Fetch(session) {
|
|
|
94
105
|
*/
|
|
95
106
|
export function createPoolBackedH2Fetch(pool, domain) {
|
|
96
107
|
return async (input) => {
|
|
108
|
+
// Acquire the slot here, but hand its release to the send path: it is held
|
|
109
|
+
// for the OPEN-STREAM lifetime and freed on the stream terminal. Paths that
|
|
110
|
+
// never open a stream on the shared session (fetch fallbacks go over a
|
|
111
|
+
// different connection) release it immediately so they do not count against
|
|
112
|
+
// the open-stream budget. `rel` is idempotent, so the explicit releases
|
|
113
|
+
// below are safe even though the send path may also release.
|
|
97
114
|
const rel = await acquireH2Slot(domain);
|
|
98
115
|
try {
|
|
99
116
|
const session = await pool.get(domain);
|
|
@@ -104,6 +121,7 @@ export function createPoolBackedH2Fetch(pool, domain) {
|
|
|
104
121
|
onH2RequestCreated: () => {
|
|
105
122
|
h2RequestCreated = true;
|
|
106
123
|
},
|
|
124
|
+
releaseSlot: rel,
|
|
107
125
|
});
|
|
108
126
|
}
|
|
109
127
|
catch (err) {
|
|
@@ -113,14 +131,23 @@ export function createPoolBackedH2Fetch(pool, domain) {
|
|
|
113
131
|
throw err;
|
|
114
132
|
}
|
|
115
133
|
}
|
|
134
|
+
// No usable session: this fetch does not open a stream on the shared
|
|
135
|
+
// session, so free the slot before falling back.
|
|
136
|
+
rel();
|
|
116
137
|
return await globalThis.fetch(input);
|
|
117
138
|
}
|
|
118
|
-
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Pre-send throw (e.g. pool.get() or Request body read): release the slot
|
|
141
|
+
// so a failed request never pins it. Idempotent if already released.
|
|
119
142
|
rel();
|
|
143
|
+
throw err;
|
|
120
144
|
}
|
|
121
145
|
};
|
|
122
146
|
}
|
|
123
147
|
export async function h2RequestDirectFromPool(pool, domain, url, init) {
|
|
148
|
+
// See createPoolBackedH2Fetch: the slot is held for the OPEN-STREAM lifetime
|
|
149
|
+
// and released by the send path on the stream terminal; non-stream fallbacks
|
|
150
|
+
// release it immediately. `rel` is idempotent.
|
|
124
151
|
const rel = await acquireH2Slot(domain);
|
|
125
152
|
try {
|
|
126
153
|
const session = await pool.get(domain);
|
|
@@ -131,6 +158,7 @@ export async function h2RequestDirectFromPool(pool, domain, url, init) {
|
|
|
131
158
|
onH2RequestCreated: () => {
|
|
132
159
|
h2RequestCreated = true;
|
|
133
160
|
},
|
|
161
|
+
releaseSlot: rel,
|
|
134
162
|
});
|
|
135
163
|
}
|
|
136
164
|
catch (err) {
|
|
@@ -140,10 +168,13 @@ export async function h2RequestDirectFromPool(pool, domain, url, init) {
|
|
|
140
168
|
throw err;
|
|
141
169
|
}
|
|
142
170
|
}
|
|
171
|
+
// No usable session: fetch over a different connection, no stream opened.
|
|
172
|
+
rel();
|
|
143
173
|
return await globalThis.fetch(url, init);
|
|
144
174
|
}
|
|
145
|
-
|
|
175
|
+
catch (err) {
|
|
146
176
|
rel();
|
|
177
|
+
throw err;
|
|
147
178
|
}
|
|
148
179
|
}
|
|
149
180
|
/**
|
|
@@ -155,6 +186,9 @@ export function h2RequestDirect(session, url, init) {
|
|
|
155
186
|
}
|
|
156
187
|
function h2RequestDirectInternal(session, url, init, options) {
|
|
157
188
|
if (session.closed || session.destroyed) {
|
|
189
|
+
// Pre-flight fallback (session unusable): no stream opens on the shared
|
|
190
|
+
// session, so free any held slot before going over globalThis.fetch.
|
|
191
|
+
options?.releaseSlot?.();
|
|
158
192
|
return globalThis.fetch(url, init);
|
|
159
193
|
}
|
|
160
194
|
const parsed = new URL(url);
|
|
@@ -194,7 +228,9 @@ function h2RequestDirectInternal(session, url, init, options) {
|
|
|
194
228
|
else {
|
|
195
229
|
// FormData, ReadableStream, Blob, etc. can't be serialized to Buffer
|
|
196
230
|
// for manual H2 framing — fall back to regular fetch (pre-flight,
|
|
197
|
-
// nothing has been sent on the wire yet).
|
|
231
|
+
// nothing has been sent on the wire yet). No stream opens on the shared
|
|
232
|
+
// session, so free any held slot before falling back.
|
|
233
|
+
options?.releaseSlot?.();
|
|
198
234
|
return globalThis.fetch(url, init);
|
|
199
235
|
}
|
|
200
236
|
if (!h2Headers["content-length"]) {
|
|
@@ -238,13 +274,19 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit, op
|
|
|
238
274
|
let streamClosed = false;
|
|
239
275
|
let req = null;
|
|
240
276
|
let releaseSessionRef = () => { };
|
|
277
|
+
// The per-domain open-stream slot (idempotent; no-op for the non-pool
|
|
278
|
+
// transports). Held for the OPEN-STREAM lifetime and released alongside the
|
|
279
|
+
// session ref from cleanupActiveRequest() on every terminal path.
|
|
280
|
+
const releaseSlot = options?.releaseSlot ?? (() => { });
|
|
241
281
|
let abort = null;
|
|
242
282
|
try {
|
|
243
283
|
req = session.request(h2Headers);
|
|
244
284
|
}
|
|
245
285
|
catch {
|
|
246
286
|
// Pre-flight fallback: session.request() threw synchronously, so no
|
|
247
|
-
// H2 frames were sent.
|
|
287
|
+
// H2 frames were sent. No stream opened on the shared session, so free
|
|
288
|
+
// the slot before retrying over globalThis.fetch.
|
|
289
|
+
releaseSlot();
|
|
248
290
|
globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
|
|
249
291
|
return;
|
|
250
292
|
}
|
|
@@ -259,7 +301,13 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit, op
|
|
|
259
301
|
const cleanupActiveRequest = () => {
|
|
260
302
|
if (abort)
|
|
261
303
|
signal?.removeEventListener("abort", abort);
|
|
304
|
+
// Slot and session-ref share the OPEN-STREAM lifetime but stay
|
|
305
|
+
// independent and each idempotent (PM-2160). Every terminal path funnels
|
|
306
|
+
// through here exactly once: pre-response reject (close/goaway/error),
|
|
307
|
+
// abort-before-response, abort-during-stream, stream end, stream error,
|
|
308
|
+
// and ReadableStream cancel — so the slot is freed once on each.
|
|
262
309
|
releaseSessionRef();
|
|
310
|
+
releaseSlot();
|
|
263
311
|
};
|
|
264
312
|
const rejectBeforeResponse = (err) => {
|
|
265
313
|
if (settled)
|
|
@@ -5,8 +5,8 @@ import { authentication } from "../authentication/index.js";
|
|
|
5
5
|
import { env } from "../common/env.js";
|
|
6
6
|
import { fs, os, path } from "../common/node.js";
|
|
7
7
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
8
|
-
const BUILD_VERSION = "0.2.
|
|
9
|
-
const BUILD_COMMIT = "
|
|
8
|
+
const BUILD_VERSION = "0.2.87-preview.168";
|
|
9
|
+
const BUILD_COMMIT = "bd5fddf1656456ae916681fa617df50fb584f997";
|
|
10
10
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
11
11
|
const BLAXEL_API_VERSION = "2026-04-16";
|
|
12
12
|
// Cache for config.yaml tracking value
|