@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
@@ -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
- * Non-blocking: checks the pool cache synchronously. If a warm session is
21
- * available it's used immediately; otherwise the request goes through
22
- * regular fetch with zero delay (the pool keeps warming in the background
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.tryGet(domain);
27
+ return async (input) => {
28
+ const session = await pool.get(domain);
28
29
  if (session) {
29
- return _h2Request(session, input);
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.close();
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.sessions.get(domain);
49
- if (existing && !existing.closed && !existing.destroyed)
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.sessions.set(domain, session);
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 (cached && !cached.closed && !cached.destroyed)
71
- return cached;
72
- this.sessions.delete(domain);
73
- return null;
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.tryGet(domain);
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 && !session.closed && !session.destroyed)
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.sessions.set(domain, session);
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 [, session] of this.sessions) {
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.146";
9
- const BUILD_COMMIT = "bd1f2f080c52d701b0dcbcccd73d139be5740ef2";
8
+ const BUILD_VERSION = "0.2.82";
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 { createH2Fetch, h2RequestDirect } from "../common/h2fetch.js";
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 session = this.sandbox.h2Session;
62
- if (session && !session.closed && !session.destroyed) {
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: createH2Fetch(session),
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 session is no longer usable
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 session = this.sandbox.h2Session;
87
- if (session && !session.closed && !session.destroyed) {
88
- return h2RequestDirect(session, input.toString(), init);
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 { h2RequestDirect } from "../common/h2fetch.js";
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 session = this._sandboxConfig.h2Session;
95
- if (session && !session.closed && !session.destroyed) {
96
- return h2RequestDirect(session, input.toString(), init);
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 region = instance.spec?.region;
59
- if (!region)
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 edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
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) {