@blaxel/core 0.2.71-dev.101 → 0.2.71-dev.105

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 (65) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/common/autoload.js +64 -0
  3. package/dist/cjs/common/h2fetch.js +207 -0
  4. package/dist/cjs/common/h2pool.js +137 -0
  5. package/dist/cjs/common/h2warm.js +54 -0
  6. package/dist/cjs/common/settings.js +2 -2
  7. package/dist/cjs/sandbox/action.js +33 -0
  8. package/dist/cjs/sandbox/drive/drive.js +3 -3
  9. package/dist/cjs/sandbox/filesystem/filesystem.js +1 -2
  10. package/dist/cjs/sandbox/interpreter.js +12 -2
  11. package/dist/cjs/sandbox/process/process.js +2 -2
  12. package/dist/cjs/sandbox/sandbox.js +84 -10
  13. package/dist/cjs/types/common/autoload.d.ts +5 -0
  14. package/dist/cjs/types/common/h2fetch.d.ts +22 -0
  15. package/dist/cjs/types/common/h2pool.d.ts +38 -0
  16. package/dist/cjs/types/common/h2warm.d.ts +2 -0
  17. package/dist/cjs/types/sandbox/action.d.ts +8 -1
  18. package/dist/cjs/types/sandbox/interpreter.d.ts +1 -0
  19. package/dist/cjs/types/sandbox/sandbox.d.ts +6 -0
  20. package/dist/cjs/types/sandbox/types.d.ts +2 -0
  21. package/dist/cjs-browser/.tsbuildinfo +1 -1
  22. package/dist/cjs-browser/common/autoload.js +64 -0
  23. package/dist/cjs-browser/common/h2fetch.js +4 -0
  24. package/dist/cjs-browser/common/h2pool.js +4 -0
  25. package/dist/cjs-browser/common/h2warm.js +2 -0
  26. package/dist/cjs-browser/common/settings.js +2 -2
  27. package/dist/cjs-browser/sandbox/action.js +33 -0
  28. package/dist/cjs-browser/sandbox/drive/drive.js +3 -3
  29. package/dist/cjs-browser/sandbox/filesystem/filesystem.js +1 -2
  30. package/dist/cjs-browser/sandbox/interpreter.js +12 -2
  31. package/dist/cjs-browser/sandbox/process/process.js +2 -2
  32. package/dist/cjs-browser/sandbox/sandbox.js +84 -10
  33. package/dist/cjs-browser/types/common/autoload.d.ts +5 -0
  34. package/dist/cjs-browser/types/common/h2fetch.d.ts +22 -0
  35. package/dist/cjs-browser/types/common/h2pool.d.ts +38 -0
  36. package/dist/cjs-browser/types/common/h2warm.d.ts +2 -0
  37. package/dist/cjs-browser/types/sandbox/action.d.ts +8 -1
  38. package/dist/cjs-browser/types/sandbox/interpreter.d.ts +1 -0
  39. package/dist/cjs-browser/types/sandbox/sandbox.d.ts +6 -0
  40. package/dist/cjs-browser/types/sandbox/types.d.ts +2 -0
  41. package/dist/esm/.tsbuildinfo +1 -1
  42. package/dist/esm/common/autoload.js +30 -0
  43. package/dist/esm/common/h2fetch.js +202 -0
  44. package/dist/esm/common/h2pool.js +101 -0
  45. package/dist/esm/common/h2warm.js +48 -0
  46. package/dist/esm/common/settings.js +2 -2
  47. package/dist/esm/sandbox/action.js +33 -0
  48. package/dist/esm/sandbox/drive/drive.js +3 -3
  49. package/dist/esm/sandbox/filesystem/filesystem.js +1 -2
  50. package/dist/esm/sandbox/interpreter.js +12 -2
  51. package/dist/esm/sandbox/process/process.js +2 -2
  52. package/dist/esm/sandbox/sandbox.js +51 -10
  53. package/dist/esm-browser/.tsbuildinfo +1 -1
  54. package/dist/esm-browser/common/autoload.js +30 -0
  55. package/dist/esm-browser/common/h2fetch.js +4 -0
  56. package/dist/esm-browser/common/h2pool.js +4 -0
  57. package/dist/esm-browser/common/h2warm.js +2 -0
  58. package/dist/esm-browser/common/settings.js +2 -2
  59. package/dist/esm-browser/sandbox/action.js +33 -0
  60. package/dist/esm-browser/sandbox/drive/drive.js +3 -3
  61. package/dist/esm-browser/sandbox/filesystem/filesystem.js +1 -2
  62. package/dist/esm-browser/sandbox/interpreter.js +12 -2
  63. package/dist/esm-browser/sandbox/process/process.js +2 -2
  64. package/dist/esm-browser/sandbox/sandbox.js +51 -10
  65. package/package.json +1 -1
@@ -21,6 +21,28 @@ for (const interceptor of responseInterceptors) {
21
21
  }
22
22
  // Initialize Sentry for SDK error tracking immediately when module loads
23
23
  initSentry();
24
+ // Background H2 connection warming (Node.js only)
25
+ const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
26
+ /* eslint-disable */
27
+ const isBrowser = typeof globalThis !== "undefined" && globalThis?.window !== undefined;
28
+ if (isNode && !isBrowser) {
29
+ try {
30
+ // Pre-warm edge H2 for the configured region so the first
31
+ // SandboxInstance.create() gets an instant session via the pool.
32
+ // The control-plane client (api.blaxel.ai) stays on regular fetch
33
+ // which already benefits from undici's built-in connection pooling.
34
+ const region = settings.region;
35
+ if (region) {
36
+ import("./h2pool.js").then(({ h2Pool }) => {
37
+ const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
38
+ h2Pool.warm(`any.${region}.${edgeSuffix}`);
39
+ }).catch(() => { });
40
+ }
41
+ }
42
+ catch {
43
+ // Silently ignore warming failures
44
+ }
45
+ }
24
46
  // Allow to set custom configuration for browser environment
25
47
  export function initialize(config) {
26
48
  settings.setConfig(config);
@@ -34,3 +56,11 @@ export function initialize(config) {
34
56
  export function authenticate() {
35
57
  return settings.authenticate();
36
58
  }
59
+ /**
60
+ * Close all pooled H2 connections. Call this for explicit cleanup
61
+ * (e.g. in test teardown or before process exit).
62
+ */
63
+ export async function closeConnections() {
64
+ const { h2Pool } = await import("./h2pool.js");
65
+ h2Pool.closeAll();
66
+ }
@@ -0,0 +1,202 @@
1
+ const H2_REQUEST_TIMEOUT_MS = 10_000;
2
+ /**
3
+ * Creates a fetch()-compatible function that sends requests over an existing
4
+ * HTTP/2 session. Falls back to global fetch() if the session is closed,
5
+ * destroyed, or if the H2 request times out.
6
+ */
7
+ export function createH2Fetch(session) {
8
+ return (input) => {
9
+ if (session.closed || session.destroyed) {
10
+ return globalThis.fetch(input);
11
+ }
12
+ return _h2Request(session, input);
13
+ };
14
+ }
15
+ /**
16
+ * Creates a fetch()-compatible function backed by the H2 session pool.
17
+ *
18
+ * Non-blocking: checks the pool cache synchronously. If a warm session is
19
+ * available it's used immediately; otherwise the request goes through
20
+ * regular fetch with zero delay (the pool keeps warming in the background
21
+ * so subsequent calls get H2).
22
+ */
23
+ export function createPoolBackedH2Fetch(pool, domain) {
24
+ return (input) => {
25
+ const session = pool.tryGet(domain);
26
+ if (session) {
27
+ return _h2Request(session, input);
28
+ }
29
+ return globalThis.fetch(input);
30
+ };
31
+ }
32
+ /**
33
+ * Low-level H2 request that takes raw URL + init, skipping Request construction.
34
+ * Used by SandboxAction.h2Fetch() for direct calls from subsystems.
35
+ */
36
+ export function h2RequestDirect(session, url, init) {
37
+ if (session.closed || session.destroyed) {
38
+ return globalThis.fetch(url, init);
39
+ }
40
+ const parsed = new URL(url);
41
+ const method = init?.method || "GET";
42
+ const h2Headers = {
43
+ ":method": method,
44
+ ":path": parsed.pathname + parsed.search,
45
+ ":authority": parsed.host,
46
+ };
47
+ if (init?.headers) {
48
+ const entries = init.headers instanceof Headers
49
+ ? init.headers.entries()
50
+ : Array.isArray(init.headers)
51
+ ? init.headers.values()
52
+ : Object.entries(init.headers).values();
53
+ for (const [key, value] of entries) {
54
+ const k = key.toLowerCase();
55
+ if (k === "host")
56
+ continue;
57
+ h2Headers[k] = value;
58
+ }
59
+ }
60
+ let body;
61
+ if (init?.body) {
62
+ if (typeof init.body === "string") {
63
+ body = Buffer.from(init.body);
64
+ }
65
+ else if (Buffer.isBuffer(init.body)) {
66
+ body = init.body;
67
+ }
68
+ else if (init.body instanceof ArrayBuffer) {
69
+ body = Buffer.from(init.body);
70
+ }
71
+ else if (init.body instanceof Uint8Array) {
72
+ body = Buffer.from(init.body.buffer, init.body.byteOffset, init.body.byteLength);
73
+ }
74
+ else {
75
+ // FormData, ReadableStream, Blob, etc. can't be serialized to Buffer
76
+ // for manual H2 framing — fall back to regular fetch.
77
+ return globalThis.fetch(url, init);
78
+ }
79
+ if (!h2Headers["content-length"]) {
80
+ h2Headers["content-length"] = body.byteLength;
81
+ }
82
+ }
83
+ return _h2Send(session, h2Headers, body, init?.signal ?? null, url, init);
84
+ }
85
+ async function _h2Request(session, input) {
86
+ const url = new URL(input.url);
87
+ const method = input.method || "GET";
88
+ const h2Headers = {
89
+ ":method": method,
90
+ ":path": url.pathname + url.search,
91
+ ":authority": url.host,
92
+ };
93
+ for (const [key, value] of input.headers.entries()) {
94
+ if (key === "host")
95
+ continue;
96
+ h2Headers[key] = value;
97
+ }
98
+ let body;
99
+ if (input.body) {
100
+ body = Buffer.from(await input.arrayBuffer());
101
+ if (!h2Headers["content-length"]) {
102
+ h2Headers["content-length"] = body.byteLength;
103
+ }
104
+ }
105
+ return _h2Send(session, h2Headers, body, input.signal, input.url, {
106
+ method,
107
+ headers: input.headers,
108
+ body,
109
+ });
110
+ }
111
+ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
112
+ return new Promise((resolve, reject) => {
113
+ let req;
114
+ try {
115
+ req = session.request(h2Headers);
116
+ }
117
+ catch {
118
+ return globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
119
+ }
120
+ const timer = setTimeout(() => {
121
+ if (settled)
122
+ return;
123
+ settled = true;
124
+ req.close();
125
+ globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
126
+ }, H2_REQUEST_TIMEOUT_MS);
127
+ if (signal) {
128
+ if (signal.aborted) {
129
+ clearTimeout(timer);
130
+ req.close();
131
+ reject(new DOMException("The operation was aborted.", "AbortError"));
132
+ return;
133
+ }
134
+ signal.addEventListener("abort", () => {
135
+ clearTimeout(timer);
136
+ req.close();
137
+ if (!settled) {
138
+ settled = true;
139
+ reject(new DOMException("The operation was aborted.", "AbortError"));
140
+ }
141
+ }, { once: true });
142
+ }
143
+ let settled = false;
144
+ req.on("response", (headers) => {
145
+ clearTimeout(timer);
146
+ if (settled)
147
+ return;
148
+ settled = true;
149
+ const status = headers[":status"] ?? 200;
150
+ const resHeaders = new Headers();
151
+ for (const [k, v] of Object.entries(headers)) {
152
+ if (k.startsWith(":"))
153
+ continue;
154
+ if (v === undefined)
155
+ continue;
156
+ resHeaders.set(k, Array.isArray(v) ? v.join(", ") : String(v));
157
+ }
158
+ let streamClosed = false;
159
+ const readable = new ReadableStream({
160
+ start(controller) {
161
+ req.on("data", (chunk) => {
162
+ if (!streamClosed)
163
+ controller.enqueue(new Uint8Array(chunk));
164
+ });
165
+ req.on("end", () => {
166
+ if (!streamClosed) {
167
+ streamClosed = true;
168
+ controller.close();
169
+ }
170
+ });
171
+ req.on("error", (err) => {
172
+ if (!streamClosed) {
173
+ streamClosed = true;
174
+ controller.error(err);
175
+ }
176
+ });
177
+ signal?.addEventListener("abort", () => {
178
+ req.close();
179
+ if (!streamClosed) {
180
+ streamClosed = true;
181
+ controller.error(new DOMException("The operation was aborted.", "AbortError"));
182
+ }
183
+ }, { once: true });
184
+ },
185
+ });
186
+ resolve(new Response(readable, { status, headers: resHeaders }));
187
+ });
188
+ req.on("error", () => {
189
+ clearTimeout(timer);
190
+ if (settled)
191
+ return;
192
+ settled = true;
193
+ globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
194
+ });
195
+ if (body) {
196
+ req.end(body);
197
+ }
198
+ else {
199
+ req.end();
200
+ }
201
+ });
202
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Singleton H2 session pool keyed by edge domain.
3
+ *
4
+ * - `warm(domain)` starts a background connection (fire-and-forget).
5
+ * - `get(domain)` returns a live session immediately if cached, or awaits
6
+ * an in-flight warming, or establishes a fresh one.
7
+ * - Closed / destroyed sessions are automatically evicted.
8
+ */
9
+ class H2Pool {
10
+ sessions = new Map();
11
+ inflight = new Map();
12
+ _establish = null;
13
+ /**
14
+ * Lazily resolve the establish function so the http2 / tls / dns modules
15
+ * are only imported in Node.js environments.
16
+ */
17
+ async establish(domain) {
18
+ if (!this._establish) {
19
+ const { establishH2 } = await import("./h2warm.js");
20
+ this._establish = establishH2;
21
+ }
22
+ return this._establish(domain);
23
+ }
24
+ /**
25
+ * Fire-and-forget background warming. Safe to call multiple times for
26
+ * the same domain — only one connection attempt per domain at a time.
27
+ */
28
+ warm(domain) {
29
+ const existing = this.sessions.get(domain);
30
+ if (existing && !existing.closed && !existing.destroyed)
31
+ return;
32
+ if (this.inflight.has(domain))
33
+ return;
34
+ const p = this.establish(domain)
35
+ .then((session) => {
36
+ this.sessions.set(domain, session);
37
+ return session;
38
+ })
39
+ .catch(() => null)
40
+ .finally(() => {
41
+ this.inflight.delete(domain);
42
+ });
43
+ this.inflight.set(domain, p);
44
+ }
45
+ /**
46
+ * Synchronous cache check. Returns a live cached session or `null`.
47
+ * Never blocks, never establishes — use for non-blocking fast paths.
48
+ */
49
+ tryGet(domain) {
50
+ const cached = this.sessions.get(domain);
51
+ if (cached && !cached.closed && !cached.destroyed)
52
+ return cached;
53
+ this.sessions.delete(domain);
54
+ return null;
55
+ }
56
+ /**
57
+ * Get a live H2 session for `domain`. Returns immediately from cache,
58
+ * joins an in-flight warming, or starts a new one.
59
+ */
60
+ async get(domain) {
61
+ const fast = this.tryGet(domain);
62
+ if (fast)
63
+ return fast;
64
+ // Join in-flight warming if one exists
65
+ const pending = this.inflight.get(domain);
66
+ if (pending) {
67
+ const session = await pending;
68
+ if (session && !session.closed && !session.destroyed)
69
+ return session;
70
+ }
71
+ // Start fresh, deduplicating concurrent callers via inflight
72
+ // Re-check: another caller may have started a fresh one while we awaited
73
+ const existingInflight = this.inflight.get(domain);
74
+ if (existingInflight)
75
+ return existingInflight;
76
+ const freshCached = this.tryGet(domain);
77
+ if (freshCached)
78
+ return freshCached;
79
+ const p = this.establish(domain)
80
+ .then((session) => {
81
+ this.sessions.set(domain, session);
82
+ return session;
83
+ })
84
+ .catch(() => null)
85
+ .finally(() => {
86
+ this.inflight.delete(domain);
87
+ });
88
+ this.inflight.set(domain, p);
89
+ return p;
90
+ }
91
+ /** Close all sessions (for cleanup). */
92
+ closeAll() {
93
+ for (const [, session] of this.sessions) {
94
+ if (!session.closed && !session.destroyed)
95
+ session.close();
96
+ }
97
+ this.sessions.clear();
98
+ this.inflight.clear();
99
+ }
100
+ }
101
+ export const h2Pool = new H2Pool();
@@ -0,0 +1,48 @@
1
+ import dns from "dns/promises";
2
+ import http2 from "http2";
3
+ import tls from "tls";
4
+ export async function establishH2(sniHostname) {
5
+ let timedOut = false;
6
+ let timer;
7
+ const timeout = new Promise((_, reject) => {
8
+ timer = setTimeout(() => {
9
+ timedOut = true;
10
+ reject(new Error("H2 warm-up timed out"));
11
+ }, 5000);
12
+ });
13
+ const attempt = _establishH2(sniHostname).then((session) => {
14
+ // If the timeout already fired, destroy the orphaned session immediately
15
+ if (timedOut) {
16
+ session.destroy();
17
+ }
18
+ return session;
19
+ });
20
+ return Promise.race([attempt, timeout]).finally(() => clearTimeout(timer));
21
+ }
22
+ async function _establishH2(sniHostname) {
23
+ const { address } = await dns.lookup(sniHostname);
24
+ const tlsSocket = tls.connect({
25
+ host: address,
26
+ port: 443,
27
+ servername: sniHostname,
28
+ ALPNProtocols: ["h2"],
29
+ });
30
+ const session = http2.connect(`https://${sniHostname}:443`, {
31
+ createConnection: () => tlsSocket,
32
+ });
33
+ await new Promise((resolve, reject) => {
34
+ session.on("connect", resolve);
35
+ session.on("error", (err) => {
36
+ // Ensure the TLS socket is cleaned up on connection error
37
+ if (!tlsSocket.destroyed)
38
+ tlsSocket.destroy();
39
+ reject(err);
40
+ });
41
+ });
42
+ // Complete the SETTINGS exchange so the first real request has zero
43
+ // protocol overhead. This RTT is hidden by the parallel createSandbox() call.
44
+ await new Promise((resolve) => session.ping(() => resolve()));
45
+ // Unref so the session doesn't prevent process exit
46
+ session.unref();
47
+ return session;
48
+ }
@@ -3,8 +3,8 @@ import { authentication } from "../authentication/index.js";
3
3
  import { env } from "../common/env.js";
4
4
  import { fs, os, path } from "../common/node.js";
5
5
  // Build info - these placeholders are replaced at build time by build:replace-imports
6
- const BUILD_VERSION = "0.2.71-dev.101";
7
- const BUILD_COMMIT = "dccf33c2f51602cd1cd06116b55d326f4645465a";
6
+ const BUILD_VERSION = "0.2.71-dev.105";
7
+ const BUILD_COMMIT = "cd4e249990c84664410df2d7158d13d6342300ed";
8
8
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
9
9
  // Cache for config.yaml tracking value
10
10
  let configTrackingValue = null;
@@ -1,4 +1,7 @@
1
1
  import { createClient } from "@hey-api/client-fetch";
2
+ import { interceptors } from "../client/interceptors.js";
3
+ import { responseInterceptors } from "../client/responseInterceptor.js";
4
+ import { createH2Fetch, h2RequestDirect } from "../common/h2fetch.js";
2
5
  import { getForcedUrl, getGlobalUniqueHash } from "../common/internal.js";
3
6
  import { settings } from "../common/settings.js";
4
7
  import { client as defaultClient } from "./client/client.gen.js";
@@ -28,6 +31,7 @@ export class ResponseError extends Error {
28
31
  }
29
32
  export class SandboxAction {
30
33
  sandbox;
34
+ _h2Client = null;
31
35
  constructor(sandbox) {
32
36
  this.sandbox = sandbox;
33
37
  }
@@ -54,8 +58,37 @@ export class SandboxAction {
54
58
  headers: this.sandbox.headers,
55
59
  });
56
60
  }
61
+ const session = this.sandbox.h2Session;
62
+ if (session && !session.closed && !session.destroyed) {
63
+ if (!this._h2Client) {
64
+ this._h2Client = createClient({
65
+ fetch: createH2Fetch(session),
66
+ });
67
+ for (const interceptor of interceptors) {
68
+ // @ts-expect-error - Interceptor is not typed
69
+ this._h2Client.interceptors.request.use(interceptor);
70
+ }
71
+ for (const interceptor of responseInterceptors) {
72
+ this._h2Client.interceptors.response.use(interceptor);
73
+ }
74
+ }
75
+ return this._h2Client;
76
+ }
77
+ // Invalidate cached H2 client when session is no longer usable
78
+ this._h2Client = null;
57
79
  return defaultClient;
58
80
  }
81
+ /**
82
+ * Routes through the H2 session when available, falling back to
83
+ * globalThis.fetch. Uses a direct H2 path that avoids Request allocation.
84
+ */
85
+ h2Fetch(input, init) {
86
+ const session = this.sandbox.h2Session;
87
+ if (session && !session.closed && !session.destroyed) {
88
+ return h2RequestDirect(session, input.toString(), init);
89
+ }
90
+ return globalThis.fetch(input, init);
91
+ }
59
92
  get forcedUrl() {
60
93
  if (this.sandbox.forceUrl)
61
94
  return this.sandbox.forceUrl;
@@ -14,7 +14,7 @@ export class SandboxDrive extends SandboxAction {
14
14
  mountPath: request.mountPath,
15
15
  drivePath: request.drivePath || "/",
16
16
  };
17
- const response = await fetch(`${this.url}/drives/mount`, {
17
+ const response = await this.h2Fetch(`${this.url}/drives/mount`, {
18
18
  method: 'POST',
19
19
  headers: {
20
20
  ...headers,
@@ -37,7 +37,7 @@ export class SandboxDrive extends SandboxAction {
37
37
  const normalizedPath = mountPath.startsWith('/') ? mountPath : `/${mountPath}`;
38
38
  // Remove leading slash for URL (DELETE /drives/mnt/test not /drives//mnt/test)
39
39
  const urlPath = normalizedPath.substring(1);
40
- const response = await fetch(`${this.url}/drives/mount/${urlPath}`, {
40
+ const response = await this.h2Fetch(`${this.url}/drives/mount/${urlPath}`, {
41
41
  method: 'DELETE',
42
42
  headers,
43
43
  });
@@ -52,7 +52,7 @@ export class SandboxDrive extends SandboxAction {
52
52
  */
53
53
  async list() {
54
54
  const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings.headers;
55
- const response = await fetch(`${this.url}/drives/mount`, {
55
+ const response = await this.h2Fetch(`${this.url}/drives/mount`, {
56
56
  method: 'GET',
57
57
  headers,
58
58
  });
@@ -98,8 +98,7 @@ export class SandboxFileSystem extends SandboxAction {
98
98
  if (this.forcedUrl) {
99
99
  url = `${this.forcedUrl.toString()}/filesystem/${path}`;
100
100
  }
101
- // Make the request using fetch instead of axios for better FormData handling
102
- const response = await fetch(url, {
101
+ const response = await this.h2Fetch(url, {
103
102
  method: 'PUT',
104
103
  headers: {
105
104
  ...settings.headers,
@@ -1,3 +1,4 @@
1
+ import { h2RequestDirect } from "../common/h2fetch.js";
1
2
  import { logger } from "../common/logger.js";
2
3
  import { settings } from "../common/settings.js";
3
4
  import { SandboxInstance } from "./sandbox.js";
@@ -23,6 +24,7 @@ export class CodeInterpreter extends SandboxInstance {
23
24
  spec: base.spec,
24
25
  status: base.status,
25
26
  events: base.events,
27
+ h2Session: base.h2Session,
26
28
  };
27
29
  return new CodeInterpreter(config);
28
30
  }
@@ -80,6 +82,7 @@ export class CodeInterpreter extends SandboxInstance {
80
82
  spec: baseInstance.spec,
81
83
  status: baseInstance.status,
82
84
  events: baseInstance.events,
85
+ h2Session: baseInstance.h2Session,
83
86
  };
84
87
  // Preserve forceUrl and headers from input if it was a dict-like object
85
88
  if (sandbox && typeof sandbox === "object" && !Array.isArray(sandbox)) {
@@ -98,6 +101,13 @@ export class CodeInterpreter extends SandboxInstance {
98
101
  get _jupyterUrl() {
99
102
  return this.process.url;
100
103
  }
104
+ _fetch(input, init) {
105
+ const session = this._sandboxConfig.h2Session;
106
+ if (session && !session.closed && !session.destroyed) {
107
+ return h2RequestDirect(session, input.toString(), init);
108
+ }
109
+ return globalThis.fetch(input, init);
110
+ }
101
111
  static OutputMessage = class {
102
112
  text;
103
113
  timestamp;
@@ -257,7 +267,7 @@ export class CodeInterpreter extends SandboxInstance {
257
267
  }, readTimeout * 1000);
258
268
  }
259
269
  try {
260
- const response = await fetch(`${this._jupyterUrl}/port/8888/execute`, {
270
+ const response = await this._fetch(`${this._jupyterUrl}/port/8888/execute`, {
261
271
  method: "POST",
262
272
  headers: {
263
273
  ...headers,
@@ -356,7 +366,7 @@ export class CodeInterpreter extends SandboxInstance {
356
366
  }, requestTimeout * 1000);
357
367
  }
358
368
  try {
359
- const response = await fetch(`${this._jupyterUrl}/port/8888/contexts`, {
369
+ const response = await this._fetch(`${this._jupyterUrl}/port/8888/contexts`, {
360
370
  method: "POST",
361
371
  headers: {
362
372
  ...headers,
@@ -18,7 +18,7 @@ export class SandboxProcess extends SandboxAction {
18
18
  const done = (async () => {
19
19
  try {
20
20
  const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings.headers;
21
- const stream = await fetch(`${this.url}/process/${identifier}/logs/stream`, {
21
+ const stream = await this.h2Fetch(`${this.url}/process/${identifier}/logs/stream`, {
22
22
  method: 'GET',
23
23
  signal: controller.signal,
24
24
  headers,
@@ -120,7 +120,7 @@ export class SandboxProcess extends SandboxAction {
120
120
  async execWithStreaming(processRequest, options) {
121
121
  const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings.headers;
122
122
  const controller = new AbortController();
123
- const response = await fetch(`${this.url}/process`, {
123
+ const response = await this.h2Fetch(`${this.url}/process`, {
124
124
  method: 'POST',
125
125
  signal: controller.signal,
126
126
  headers: {