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