@blaxel/core 0.2.82-preview.146 → 0.2.82

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