@blaxel/core 0.2.82-preview.145 → 0.2.82-preview.147
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 +93 -13
- package/dist/cjs/common/h2pool.js +109 -14
- package/dist/cjs/common/lazyInit.js +1 -1
- package/dist/cjs/common/settings.js +11 -2
- package/dist/cjs/sandbox/action.js +12 -8
- package/dist/cjs/sandbox/interpreter.js +6 -3
- package/dist/cjs/sandbox/sandbox.js +22 -11
- package/dist/cjs/types/common/h2fetch.d.ts +6 -6
- package/dist/cjs/types/common/h2pool.d.ts +20 -0
- package/dist/cjs/types/common/settings.d.ts +2 -0
- package/dist/cjs/types/sandbox/action.d.ts +1 -0
- package/dist/cjs/types/sandbox/sandbox.d.ts +4 -1
- package/dist/cjs/types/sandbox/types.d.ts +1 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/h2fetch.js +1 -0
- package/dist/cjs-browser/common/lazyInit.js +1 -1
- package/dist/cjs-browser/common/settings.js +11 -2
- package/dist/cjs-browser/sandbox/action.js +12 -8
- package/dist/cjs-browser/sandbox/interpreter.js +6 -3
- package/dist/cjs-browser/sandbox/sandbox.js +22 -11
- package/dist/cjs-browser/types/common/h2fetch.d.ts +6 -6
- package/dist/cjs-browser/types/common/h2pool.d.ts +20 -0
- package/dist/cjs-browser/types/common/settings.d.ts +2 -0
- package/dist/cjs-browser/types/sandbox/action.d.ts +1 -0
- package/dist/cjs-browser/types/sandbox/sandbox.d.ts +4 -1
- package/dist/cjs-browser/types/sandbox/types.d.ts +1 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/h2fetch.js +92 -13
- package/dist/esm/common/h2pool.js +109 -14
- package/dist/esm/common/lazyInit.js +1 -1
- package/dist/esm/common/settings.js +11 -2
- package/dist/esm/sandbox/action.js +13 -9
- package/dist/esm/sandbox/interpreter.js +7 -4
- package/dist/esm/sandbox/sandbox.js +22 -11
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/h2fetch.js +1 -0
- package/dist/esm-browser/common/lazyInit.js +1 -1
- package/dist/esm-browser/common/settings.js +11 -2
- package/dist/esm-browser/sandbox/action.js +13 -9
- package/dist/esm-browser/sandbox/interpreter.js +7 -4
- package/dist/esm-browser/sandbox/sandbox.js +22 -11
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const MIN_H2_SESSION_MAX_LISTENERS = 64;
|
|
2
|
+
const sessionsWithListenerBudget = new WeakSet();
|
|
1
3
|
/**
|
|
2
4
|
* Creates a fetch()-compatible function that sends requests over an existing
|
|
3
5
|
* HTTP/2 session. Falls back to globalThis.fetch() only when the session is
|
|
@@ -17,25 +19,60 @@ export function createH2Fetch(session) {
|
|
|
17
19
|
/**
|
|
18
20
|
* Creates a fetch()-compatible function backed by the H2 session pool.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
* available
|
|
22
|
-
*
|
|
23
|
-
* so subsequent calls get H2).
|
|
22
|
+
* The pool validates idle sessions before reuse. If no usable H2 session is
|
|
23
|
+
* available, the request falls back to regular fetch before any H2 frames
|
|
24
|
+
* are sent.
|
|
24
25
|
*/
|
|
25
26
|
export function createPoolBackedH2Fetch(pool, domain) {
|
|
26
|
-
return (input) => {
|
|
27
|
-
const session = pool.
|
|
27
|
+
return async (input) => {
|
|
28
|
+
const session = await pool.get(domain);
|
|
28
29
|
if (session) {
|
|
29
|
-
|
|
30
|
+
let h2RequestCreated = false;
|
|
31
|
+
try {
|
|
32
|
+
return await _h2Request(session, input, {
|
|
33
|
+
onH2RequestCreated: () => {
|
|
34
|
+
h2RequestCreated = true;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (h2RequestCreated) {
|
|
40
|
+
pool.evictSession(domain, session);
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
return globalThis.fetch(input);
|
|
32
46
|
};
|
|
33
47
|
}
|
|
48
|
+
export async function h2RequestDirectFromPool(pool, domain, url, init) {
|
|
49
|
+
const session = await pool.get(domain);
|
|
50
|
+
if (session) {
|
|
51
|
+
let h2RequestCreated = false;
|
|
52
|
+
try {
|
|
53
|
+
return await h2RequestDirectInternal(session, url, init, {
|
|
54
|
+
onH2RequestCreated: () => {
|
|
55
|
+
h2RequestCreated = true;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (h2RequestCreated) {
|
|
61
|
+
pool.evictSession(domain, session);
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return globalThis.fetch(url, init);
|
|
67
|
+
}
|
|
34
68
|
/**
|
|
35
69
|
* Low-level H2 request that takes raw URL + init, skipping Request construction.
|
|
36
70
|
* Used by SandboxAction.h2Fetch() for direct calls from subsystems.
|
|
37
71
|
*/
|
|
38
72
|
export function h2RequestDirect(session, url, init) {
|
|
73
|
+
return h2RequestDirectInternal(session, url, init);
|
|
74
|
+
}
|
|
75
|
+
function h2RequestDirectInternal(session, url, init, options) {
|
|
39
76
|
if (session.closed || session.destroyed) {
|
|
40
77
|
return globalThis.fetch(url, init);
|
|
41
78
|
}
|
|
@@ -83,9 +120,9 @@ export function h2RequestDirect(session, url, init) {
|
|
|
83
120
|
h2Headers["content-length"] = body.byteLength;
|
|
84
121
|
}
|
|
85
122
|
}
|
|
86
|
-
return _h2Send(session, h2Headers, body, init?.signal ?? null, url, init);
|
|
123
|
+
return _h2Send(session, h2Headers, body, init?.signal ?? null, url, init, options);
|
|
87
124
|
}
|
|
88
|
-
async function _h2Request(session, input) {
|
|
125
|
+
async function _h2Request(session, input, options) {
|
|
89
126
|
const url = new URL(input.url);
|
|
90
127
|
const method = input.method || "GET";
|
|
91
128
|
const h2Headers = {
|
|
@@ -110,15 +147,15 @@ async function _h2Request(session, input) {
|
|
|
110
147
|
headers: input.headers,
|
|
111
148
|
body,
|
|
112
149
|
signal: input.signal,
|
|
113
|
-
});
|
|
150
|
+
}, options);
|
|
114
151
|
}
|
|
115
|
-
function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
152
|
+
function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit, options) {
|
|
116
153
|
return new Promise((resolve, reject) => {
|
|
117
154
|
let settled = false;
|
|
118
155
|
let responded = false;
|
|
119
156
|
let streamController = null;
|
|
120
157
|
let streamClosed = false;
|
|
121
|
-
let req;
|
|
158
|
+
let req = null;
|
|
122
159
|
try {
|
|
123
160
|
req = session.request(h2Headers);
|
|
124
161
|
}
|
|
@@ -128,12 +165,40 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
128
165
|
globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
|
|
129
166
|
return;
|
|
130
167
|
}
|
|
168
|
+
options?.onH2RequestCreated?.();
|
|
169
|
+
ensureH2SessionListenerBudget(session);
|
|
170
|
+
const cleanupBeforeResponseListeners = () => {
|
|
171
|
+
session.off("close", onSessionClose);
|
|
172
|
+
session.off("goaway", onSessionGoaway);
|
|
173
|
+
session.off("error", onSessionError);
|
|
174
|
+
};
|
|
175
|
+
const rejectBeforeResponse = (err) => {
|
|
176
|
+
if (settled)
|
|
177
|
+
return;
|
|
178
|
+
settled = true;
|
|
179
|
+
cleanupBeforeResponseListeners();
|
|
180
|
+
req?.close();
|
|
181
|
+
reject(err);
|
|
182
|
+
};
|
|
183
|
+
const onSessionClose = () => {
|
|
184
|
+
rejectBeforeResponse(new Error("HTTP/2 session closed before response"));
|
|
185
|
+
};
|
|
186
|
+
const onSessionGoaway = () => {
|
|
187
|
+
rejectBeforeResponse(new Error("HTTP/2 session sent GOAWAY before response"));
|
|
188
|
+
};
|
|
189
|
+
const onSessionError = (err) => {
|
|
190
|
+
rejectBeforeResponse(err);
|
|
191
|
+
};
|
|
192
|
+
session.once("close", onSessionClose);
|
|
193
|
+
session.once("goaway", onSessionGoaway);
|
|
194
|
+
session.once("error", onSessionError);
|
|
131
195
|
const abort = () => {
|
|
132
|
-
req
|
|
196
|
+
req?.close();
|
|
133
197
|
const abortError = new DOMException("The operation was aborted.", "AbortError");
|
|
134
198
|
if (!responded) {
|
|
135
199
|
if (!settled) {
|
|
136
200
|
settled = true;
|
|
201
|
+
cleanupBeforeResponseListeners();
|
|
137
202
|
reject(abortError);
|
|
138
203
|
}
|
|
139
204
|
return;
|
|
@@ -146,6 +211,7 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
146
211
|
if (signal) {
|
|
147
212
|
if (signal.aborted) {
|
|
148
213
|
req.close();
|
|
214
|
+
cleanupBeforeResponseListeners();
|
|
149
215
|
settled = true;
|
|
150
216
|
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
151
217
|
return;
|
|
@@ -157,6 +223,7 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
157
223
|
return;
|
|
158
224
|
settled = true;
|
|
159
225
|
responded = true;
|
|
226
|
+
cleanupBeforeResponseListeners();
|
|
160
227
|
const status = headers[":status"] ?? 200;
|
|
161
228
|
const resHeaders = new Headers();
|
|
162
229
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -176,12 +243,14 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
176
243
|
req.on("end", () => {
|
|
177
244
|
if (!streamClosed) {
|
|
178
245
|
streamClosed = true;
|
|
246
|
+
signal?.removeEventListener("abort", abort);
|
|
179
247
|
controller.close();
|
|
180
248
|
}
|
|
181
249
|
});
|
|
182
250
|
req.on("error", (err) => {
|
|
183
251
|
if (!streamClosed) {
|
|
184
252
|
streamClosed = true;
|
|
253
|
+
signal?.removeEventListener("abort", abort);
|
|
185
254
|
controller.error(err);
|
|
186
255
|
}
|
|
187
256
|
});
|
|
@@ -193,6 +262,7 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
193
262
|
if (settled)
|
|
194
263
|
return;
|
|
195
264
|
settled = true;
|
|
265
|
+
cleanupBeforeResponseListeners();
|
|
196
266
|
reject(err);
|
|
197
267
|
});
|
|
198
268
|
if (body) {
|
|
@@ -203,3 +273,12 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
|
|
|
203
273
|
}
|
|
204
274
|
});
|
|
205
275
|
}
|
|
276
|
+
function ensureH2SessionListenerBudget(session) {
|
|
277
|
+
if (sessionsWithListenerBudget.has(session))
|
|
278
|
+
return;
|
|
279
|
+
sessionsWithListenerBudget.add(session);
|
|
280
|
+
const currentMax = session.getMaxListeners();
|
|
281
|
+
if (currentMax > 0 && currentMax < MIN_H2_SESSION_MAX_LISTENERS) {
|
|
282
|
+
session.setMaxListeners(MIN_H2_SESSION_MAX_LISTENERS);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const DEFAULT_MAX_IDLE_MS = 5_000;
|
|
2
|
+
const DEFAULT_PING_TIMEOUT_MS = 500;
|
|
1
3
|
/**
|
|
2
4
|
* Singleton H2 session pool keyed by edge domain.
|
|
3
5
|
*
|
|
@@ -10,6 +12,14 @@ export class H2Pool {
|
|
|
10
12
|
sessions = new Map();
|
|
11
13
|
inflight = new Map();
|
|
12
14
|
_establish = null;
|
|
15
|
+
maxIdleMs;
|
|
16
|
+
pingTimeoutMs;
|
|
17
|
+
now;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.maxIdleMs = options.maxIdleMs ?? DEFAULT_MAX_IDLE_MS;
|
|
20
|
+
this.pingTimeoutMs = options.pingTimeoutMs ?? DEFAULT_PING_TIMEOUT_MS;
|
|
21
|
+
this.now = options.now ?? Date.now;
|
|
22
|
+
}
|
|
13
23
|
/**
|
|
14
24
|
* Lazily resolve the establish function so the http2 / tls / dns modules
|
|
15
25
|
* are only imported in Node.js environments.
|
|
@@ -32,7 +42,7 @@ export class H2Pool {
|
|
|
32
42
|
const evict = () => {
|
|
33
43
|
// Only evict if this specific session is still the cached one.
|
|
34
44
|
// A newer session may have taken its place after reconnect.
|
|
35
|
-
if (this.sessions.get(domain) === session) {
|
|
45
|
+
if (this.sessions.get(domain)?.session === session) {
|
|
36
46
|
this.sessions.delete(domain);
|
|
37
47
|
}
|
|
38
48
|
};
|
|
@@ -40,19 +50,92 @@ export class H2Pool {
|
|
|
40
50
|
session.on("error", evict);
|
|
41
51
|
session.on("close", evict);
|
|
42
52
|
}
|
|
53
|
+
isClosed(session) {
|
|
54
|
+
return session.closed || session.destroyed;
|
|
55
|
+
}
|
|
56
|
+
isIdle(entry) {
|
|
57
|
+
return this.now() - entry.lastUsedAt > this.maxIdleMs;
|
|
58
|
+
}
|
|
59
|
+
cache(domain, session) {
|
|
60
|
+
this.sessions.set(domain, {
|
|
61
|
+
session,
|
|
62
|
+
lastUsedAt: this.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
markUsed(domain, session) {
|
|
66
|
+
const entry = this.sessions.get(domain);
|
|
67
|
+
if (entry?.session === session) {
|
|
68
|
+
entry.lastUsedAt = this.now();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
evict(domain, session) {
|
|
72
|
+
const entry = this.sessions.get(domain);
|
|
73
|
+
if (!entry)
|
|
74
|
+
return;
|
|
75
|
+
if (session && entry.session !== session)
|
|
76
|
+
return;
|
|
77
|
+
this.sessions.delete(domain);
|
|
78
|
+
if (!entry.session.closed && !entry.session.destroyed) {
|
|
79
|
+
entry.session.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ping(session) {
|
|
83
|
+
if (this.isClosed(session))
|
|
84
|
+
return Promise.resolve(false);
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
let settled = false;
|
|
87
|
+
const finish = (ok) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(ok);
|
|
93
|
+
};
|
|
94
|
+
const timer = setTimeout(() => finish(false), this.pingTimeoutMs);
|
|
95
|
+
try {
|
|
96
|
+
const sent = session.ping((err) => {
|
|
97
|
+
finish(!err && !this.isClosed(session));
|
|
98
|
+
});
|
|
99
|
+
if (!sent)
|
|
100
|
+
finish(false);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
finish(false);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async validateEntry(domain, entry) {
|
|
108
|
+
if (!entry)
|
|
109
|
+
return null;
|
|
110
|
+
const { session } = entry;
|
|
111
|
+
if (this.isClosed(session)) {
|
|
112
|
+
this.evict(domain, session);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (!this.isIdle(entry)) {
|
|
116
|
+
this.markUsed(domain, session);
|
|
117
|
+
return session;
|
|
118
|
+
}
|
|
119
|
+
if (await this.ping(session)) {
|
|
120
|
+
this.markUsed(domain, session);
|
|
121
|
+
return session;
|
|
122
|
+
}
|
|
123
|
+
this.evict(domain, session);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
43
126
|
/**
|
|
44
127
|
* Fire-and-forget background warming. Safe to call multiple times for
|
|
45
128
|
* the same domain — only one connection attempt per domain at a time.
|
|
46
129
|
*/
|
|
47
130
|
warm(domain) {
|
|
48
|
-
const existing = this.
|
|
49
|
-
if (existing
|
|
131
|
+
const existing = this.tryGet(domain);
|
|
132
|
+
if (existing)
|
|
50
133
|
return;
|
|
51
134
|
if (this.inflight.has(domain))
|
|
52
135
|
return;
|
|
53
136
|
const p = this.establish(domain)
|
|
54
137
|
.then((session) => {
|
|
55
|
-
this.
|
|
138
|
+
this.cache(domain, session);
|
|
56
139
|
return session;
|
|
57
140
|
})
|
|
58
141
|
.catch(() => null)
|
|
@@ -67,25 +150,37 @@ export class H2Pool {
|
|
|
67
150
|
*/
|
|
68
151
|
tryGet(domain) {
|
|
69
152
|
const cached = this.sessions.get(domain);
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
72
|
-
this.
|
|
73
|
-
|
|
153
|
+
if (!cached)
|
|
154
|
+
return null;
|
|
155
|
+
if (this.isClosed(cached.session) || this.isIdle(cached)) {
|
|
156
|
+
this.evict(domain, cached.session);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
this.markUsed(domain, cached.session);
|
|
160
|
+
return cached.session;
|
|
161
|
+
}
|
|
162
|
+
isUsable(session) {
|
|
163
|
+
return !this.isClosed(session);
|
|
164
|
+
}
|
|
165
|
+
evictSession(domain, session) {
|
|
166
|
+
this.evict(domain, session);
|
|
74
167
|
}
|
|
75
168
|
/**
|
|
76
169
|
* Get a live H2 session for `domain`. Returns immediately from cache,
|
|
77
170
|
* joins an in-flight warming, or starts a new one.
|
|
78
171
|
*/
|
|
79
172
|
async get(domain) {
|
|
80
|
-
const fast = this.
|
|
173
|
+
const fast = await this.validateEntry(domain, this.sessions.get(domain));
|
|
81
174
|
if (fast)
|
|
82
175
|
return fast;
|
|
83
176
|
// Join in-flight warming if one exists
|
|
84
177
|
const pending = this.inflight.get(domain);
|
|
85
178
|
if (pending) {
|
|
86
179
|
const session = await pending;
|
|
87
|
-
if (session &&
|
|
180
|
+
if (session && this.isUsable(session)) {
|
|
181
|
+
this.markUsed(domain, session);
|
|
88
182
|
return session;
|
|
183
|
+
}
|
|
89
184
|
}
|
|
90
185
|
// Start fresh, deduplicating concurrent callers via inflight
|
|
91
186
|
// Re-check: another caller may have started a fresh one while we awaited
|
|
@@ -97,7 +192,7 @@ export class H2Pool {
|
|
|
97
192
|
return freshCached;
|
|
98
193
|
const p = this.establish(domain)
|
|
99
194
|
.then((session) => {
|
|
100
|
-
this.
|
|
195
|
+
this.cache(domain, session);
|
|
101
196
|
return session;
|
|
102
197
|
})
|
|
103
198
|
.catch(() => null)
|
|
@@ -109,9 +204,9 @@ export class H2Pool {
|
|
|
109
204
|
}
|
|
110
205
|
/** Close all sessions (for cleanup). */
|
|
111
206
|
closeAll() {
|
|
112
|
-
for (const [,
|
|
113
|
-
if (!session.closed && !session.destroyed)
|
|
114
|
-
session.close();
|
|
207
|
+
for (const [, entry] of this.sessions) {
|
|
208
|
+
if (!entry.session.closed && !entry.session.destroyed)
|
|
209
|
+
entry.session.close();
|
|
115
210
|
}
|
|
116
211
|
this.sessions.clear();
|
|
117
212
|
this.inflight.clear();
|
|
@@ -41,7 +41,7 @@ export function ensureAutoloaded() {
|
|
|
41
41
|
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
42
42
|
/* eslint-disable */
|
|
43
43
|
const isBrowser = typeof globalThis !== "undefined" && globalThis?.window !== undefined;
|
|
44
|
-
if (isNode && !isBrowser) {
|
|
44
|
+
if (isNode && !isBrowser && !settings.disableH2) {
|
|
45
45
|
try {
|
|
46
46
|
// Pre-warm edge H2 for the configured region so the first
|
|
47
47
|
// SandboxInstance.create() gets an instant session via the pool.
|
|
@@ -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.82-preview.
|
|
9
|
-
const BUILD_COMMIT = "
|
|
8
|
+
const BUILD_VERSION = "0.2.82-preview.147";
|
|
9
|
+
const BUILD_COMMIT = "4d2b3531bdb9d8a065f6d83822343840ae0b8d65";
|
|
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
|
|
@@ -212,6 +212,15 @@ class Settings {
|
|
|
212
212
|
get region() {
|
|
213
213
|
return env.BL_REGION || undefined;
|
|
214
214
|
}
|
|
215
|
+
get disableH2() {
|
|
216
|
+
if (typeof this.config.disableH2 === "boolean") {
|
|
217
|
+
return this.config.disableH2;
|
|
218
|
+
}
|
|
219
|
+
const value = env.BL_DISABLE_H2;
|
|
220
|
+
if (!value)
|
|
221
|
+
return false;
|
|
222
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
223
|
+
}
|
|
215
224
|
async authenticate() {
|
|
216
225
|
await this.credentials.authenticate();
|
|
217
226
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createClient } from "@hey-api/client-fetch";
|
|
2
2
|
import { interceptors } from "../client/interceptors.js";
|
|
3
3
|
import { responseInterceptors } from "../client/responseInterceptor.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createPoolBackedH2Fetch, h2RequestDirectFromPool } from "../common/h2fetch.js";
|
|
5
|
+
import { h2Pool } from "../common/h2pool.js";
|
|
5
6
|
import { getForcedUrl, getGlobalUniqueHash } from "../common/internal.js";
|
|
6
7
|
import { settings } from "../common/settings.js";
|
|
7
8
|
import { client as defaultClient } from "./client/client.gen.js";
|
|
@@ -32,6 +33,7 @@ export class ResponseError extends Error {
|
|
|
32
33
|
export class SandboxAction {
|
|
33
34
|
sandbox;
|
|
34
35
|
_h2Client = null;
|
|
36
|
+
_h2ClientDomain = null;
|
|
35
37
|
constructor(sandbox) {
|
|
36
38
|
this.sandbox = sandbox;
|
|
37
39
|
}
|
|
@@ -58,12 +60,13 @@ export class SandboxAction {
|
|
|
58
60
|
headers: this.sandbox.headers,
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
if (!this._h2Client) {
|
|
63
|
+
const h2Domain = this.sandbox.h2Domain;
|
|
64
|
+
if (h2Domain) {
|
|
65
|
+
if (!this._h2Client || this._h2ClientDomain !== h2Domain) {
|
|
64
66
|
this._h2Client = createClient({
|
|
65
|
-
fetch:
|
|
67
|
+
fetch: createPoolBackedH2Fetch(h2Pool, h2Domain),
|
|
66
68
|
});
|
|
69
|
+
this._h2ClientDomain = h2Domain;
|
|
67
70
|
for (const interceptor of interceptors) {
|
|
68
71
|
// @ts-expect-error - Interceptor is not typed
|
|
69
72
|
this._h2Client.interceptors.request.use(interceptor);
|
|
@@ -74,8 +77,9 @@ export class SandboxAction {
|
|
|
74
77
|
}
|
|
75
78
|
return this._h2Client;
|
|
76
79
|
}
|
|
77
|
-
// Invalidate cached H2 client when
|
|
80
|
+
// Invalidate cached H2 client when the sandbox no longer has an H2 domain.
|
|
78
81
|
this._h2Client = null;
|
|
82
|
+
this._h2ClientDomain = null;
|
|
79
83
|
return defaultClient;
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
@@ -83,9 +87,9 @@ export class SandboxAction {
|
|
|
83
87
|
* globalThis.fetch. Uses a direct H2 path that avoids Request allocation.
|
|
84
88
|
*/
|
|
85
89
|
h2Fetch(input, init) {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
90
|
+
const h2Domain = this.sandbox.h2Domain;
|
|
91
|
+
if (h2Domain) {
|
|
92
|
+
return h2RequestDirectFromPool(h2Pool, h2Domain, input.toString(), init);
|
|
89
93
|
}
|
|
90
94
|
return globalThis.fetch(input, init);
|
|
91
95
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h2RequestDirectFromPool } from "../common/h2fetch.js";
|
|
2
|
+
import { h2Pool } from "../common/h2pool.js";
|
|
2
3
|
import { logger } from "../common/logger.js";
|
|
3
4
|
import { settings } from "../common/settings.js";
|
|
4
5
|
import { SandboxInstance } from "./sandbox.js";
|
|
@@ -25,6 +26,7 @@ export class CodeInterpreter extends SandboxInstance {
|
|
|
25
26
|
status: base.status,
|
|
26
27
|
events: base.events,
|
|
27
28
|
h2Session: base.h2Session,
|
|
29
|
+
h2Domain: base.h2Domain,
|
|
28
30
|
};
|
|
29
31
|
return new CodeInterpreter(config);
|
|
30
32
|
}
|
|
@@ -69,6 +71,7 @@ export class CodeInterpreter extends SandboxInstance {
|
|
|
69
71
|
status: baseInstance.status,
|
|
70
72
|
events: baseInstance.events,
|
|
71
73
|
h2Session: baseInstance.h2Session,
|
|
74
|
+
h2Domain: baseInstance.h2Domain,
|
|
72
75
|
};
|
|
73
76
|
// Preserve forceUrl, headers, and params from input if provided
|
|
74
77
|
if (sandbox && typeof sandbox === "object" && !Array.isArray(sandbox)) {
|
|
@@ -91,9 +94,9 @@ export class CodeInterpreter extends SandboxInstance {
|
|
|
91
94
|
return this.process.url;
|
|
92
95
|
}
|
|
93
96
|
_fetch(input, init) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
+
const h2Domain = this._sandboxConfig.h2Domain;
|
|
98
|
+
if (h2Domain) {
|
|
99
|
+
return h2RequestDirectFromPool(h2Pool, h2Domain, input.toString(), init);
|
|
97
100
|
}
|
|
98
101
|
return globalThis.fetch(input, init);
|
|
99
102
|
}
|
|
@@ -21,7 +21,6 @@ export class SandboxInstance {
|
|
|
21
21
|
codegen;
|
|
22
22
|
system;
|
|
23
23
|
drives;
|
|
24
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
25
24
|
h2Session;
|
|
26
25
|
constructor(sandbox) {
|
|
27
26
|
this.sandbox = sandbox;
|
|
@@ -50,27 +49,35 @@ export class SandboxInstance {
|
|
|
50
49
|
get lastUsedAt() {
|
|
51
50
|
return this.sandbox.lastUsedAt;
|
|
52
51
|
}
|
|
52
|
+
get h2Domain() {
|
|
53
|
+
return this.sandbox.h2Domain ?? null;
|
|
54
|
+
}
|
|
53
55
|
/**
|
|
54
56
|
* Warm and attach an H2 session based on the sandbox's region.
|
|
55
57
|
* Shared by create(), get(), list(), and update helpers.
|
|
56
58
|
*/
|
|
57
59
|
static async attachH2Session(instance) {
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
+
const edgeDomain = SandboxInstance.edgeDomainForRegion(instance.spec?.region);
|
|
61
|
+
if (!edgeDomain || settings.disableH2)
|
|
60
62
|
return instance;
|
|
61
|
-
const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
|
|
62
|
-
const edgeDomain = `any.${region}.${edgeSuffix}`;
|
|
63
63
|
try {
|
|
64
64
|
const { h2Pool } = await import("../common/h2pool.js");
|
|
65
65
|
const h2Session = await h2Pool.get(edgeDomain);
|
|
66
66
|
instance.h2Session = h2Session;
|
|
67
67
|
instance.sandbox.h2Session = h2Session;
|
|
68
|
+
instance.sandbox.h2Domain = edgeDomain;
|
|
68
69
|
}
|
|
69
70
|
catch {
|
|
70
71
|
// H2 warming is best-effort; fall back to regular fetch
|
|
71
72
|
}
|
|
72
73
|
return instance;
|
|
73
74
|
}
|
|
75
|
+
static edgeDomainForRegion(region) {
|
|
76
|
+
if (!region)
|
|
77
|
+
return null;
|
|
78
|
+
const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
|
|
79
|
+
return `any.${region}.${edgeSuffix}`;
|
|
80
|
+
}
|
|
74
81
|
get expiresIn() {
|
|
75
82
|
return this.sandbox.expiresIn;
|
|
76
83
|
}
|
|
@@ -165,10 +172,9 @@ export class SandboxInstance {
|
|
|
165
172
|
}
|
|
166
173
|
sandbox.spec.runtime.image = sandbox.spec.runtime.image || defaultImage;
|
|
167
174
|
sandbox.spec.runtime.memory = sandbox.spec.runtime.memory || defaultMemory;
|
|
168
|
-
const
|
|
169
|
-
const edgeDomain = sandbox.spec?.region ? `any.${sandbox.spec.region}.${edgeSuffix}` : null;
|
|
175
|
+
const edgeDomain = SandboxInstance.edgeDomainForRegion(sandbox.spec?.region);
|
|
170
176
|
// Kick off warming so h2Pool.get() can join it during the API call
|
|
171
|
-
if (edgeDomain) {
|
|
177
|
+
if (edgeDomain && !settings.disableH2) {
|
|
172
178
|
import("../common/h2pool.js").then(({ h2Pool }) => h2Pool.warm(edgeDomain)).catch(() => { });
|
|
173
179
|
}
|
|
174
180
|
const [{ data }, h2Session] = await Promise.all([
|
|
@@ -176,10 +182,10 @@ export class SandboxInstance {
|
|
|
176
182
|
body: sandbox,
|
|
177
183
|
throwOnError: true,
|
|
178
184
|
}),
|
|
179
|
-
edgeDomain ? import("../common/h2pool.js").then(({ h2Pool }) => h2Pool.get(edgeDomain)).catch(() => null) : Promise.resolve(null),
|
|
185
|
+
edgeDomain && !settings.disableH2 ? import("../common/h2pool.js").then(({ h2Pool }) => h2Pool.get(edgeDomain)).catch(() => null) : Promise.resolve(null),
|
|
180
186
|
]);
|
|
181
187
|
// Inject the H2 session into the config so subsystems can use it
|
|
182
|
-
const config = { ...data, h2Session };
|
|
188
|
+
const config = { ...data, h2Session, h2Domain: settings.disableH2 ? null : edgeDomain };
|
|
183
189
|
const instance = new SandboxInstance(config);
|
|
184
190
|
instance.h2Session = h2Session;
|
|
185
191
|
// Note: H2 session already attached via Promise.all above, no need for attachH2Session()
|
|
@@ -188,7 +194,10 @@ export class SandboxInstance {
|
|
|
188
194
|
try {
|
|
189
195
|
await instance.fs.ls('/');
|
|
190
196
|
}
|
|
191
|
-
catch {
|
|
197
|
+
catch (err) {
|
|
198
|
+
await SandboxInstance.delete(instance.metadata.name).catch(() => { });
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
192
201
|
}
|
|
193
202
|
return instance;
|
|
194
203
|
}
|
|
@@ -219,6 +228,8 @@ export class SandboxInstance {
|
|
|
219
228
|
async delete() {
|
|
220
229
|
// Don't close the H2 session — it's shared via h2Pool
|
|
221
230
|
this.h2Session = null;
|
|
231
|
+
this.sandbox.h2Session = null;
|
|
232
|
+
this.sandbox.h2Domain = null;
|
|
222
233
|
return await SandboxInstance.delete(this.metadata.name);
|
|
223
234
|
}
|
|
224
235
|
static async updateMetadata(sandboxName, metadata) {
|