@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,4 @@
1
+ // Browser stub - H2 fetch is Node.js only, falls back to global fetch
2
+ export function createH2Fetch(session) { return (input) => globalThis.fetch(input); }
3
+ export function createPoolBackedH2Fetch(pool, domain) { return (input) => globalThis.fetch(input); }
4
+ export function h2RequestDirect(session, url, init) { return globalThis.fetch(url, init); }
@@ -0,0 +1,4 @@
1
+ // Browser stub - H2 pool is Node.js only
2
+ const noop = () => {};
3
+ const noopAsync = async () => null;
4
+ export const h2Pool = { warm: noop, get: noopAsync, tryGet: () => null, closeAll: noop };
@@ -0,0 +1,2 @@
1
+ // Browser stub - H2 warming is Node.js only
2
+ export async function establishH2(sniHostname) { return null; }
@@ -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: {
@@ -21,6 +21,8 @@ export class SandboxInstance {
21
21
  codegen;
22
22
  system;
23
23
  drives;
24
+ /* eslint-disable @typescript-eslint/no-explicit-any */
25
+ h2Session;
24
26
  constructor(sandbox) {
25
27
  this.sandbox = sandbox;
26
28
  this.process = new SandboxProcess(sandbox);
@@ -31,6 +33,7 @@ export class SandboxInstance {
31
33
  this.codegen = new SandboxCodegen(sandbox);
32
34
  this.system = new SandboxSystem(sandbox);
33
35
  this.drives = new SandboxDrive(sandbox);
36
+ this.h2Session = null;
34
37
  }
35
38
  get metadata() {
36
39
  return this.sandbox.metadata;
@@ -47,6 +50,27 @@ export class SandboxInstance {
47
50
  get lastUsedAt() {
48
51
  return this.sandbox.lastUsedAt;
49
52
  }
53
+ /**
54
+ * Warm and attach an H2 session based on the sandbox's region.
55
+ * Shared by create(), get(), list(), and update helpers.
56
+ */
57
+ static async attachH2Session(instance) {
58
+ const region = instance.spec?.region;
59
+ if (!region)
60
+ return instance;
61
+ const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
62
+ const edgeDomain = `any.${region}.${edgeSuffix}`;
63
+ try {
64
+ const { h2Pool } = await import("../common/h2pool.js");
65
+ const h2Session = await h2Pool.get(edgeDomain);
66
+ instance.h2Session = h2Session;
67
+ instance.sandbox.h2Session = h2Session;
68
+ }
69
+ catch {
70
+ // H2 warming is best-effort; fall back to regular fetch
71
+ }
72
+ return instance;
73
+ }
50
74
  get expiresIn() {
51
75
  return this.sandbox.expiresIn;
52
76
  }
@@ -128,11 +152,24 @@ export class SandboxInstance {
128
152
  }
129
153
  sandbox.spec.runtime.image = sandbox.spec.runtime.image || defaultImage;
130
154
  sandbox.spec.runtime.memory = sandbox.spec.runtime.memory || defaultMemory;
131
- const { data } = await createSandbox({
132
- body: sandbox,
133
- throwOnError: true,
134
- });
135
- const instance = new SandboxInstance(data);
155
+ const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
156
+ const edgeDomain = sandbox.spec?.region ? `any.${sandbox.spec.region}.${edgeSuffix}` : null;
157
+ // Kick off warming so h2Pool.get() can join it during the API call
158
+ if (edgeDomain) {
159
+ import("../common/h2pool.js").then(({ h2Pool }) => h2Pool.warm(edgeDomain)).catch(() => { });
160
+ }
161
+ const [{ data }, h2Session] = await Promise.all([
162
+ createSandbox({
163
+ body: sandbox,
164
+ throwOnError: true,
165
+ }),
166
+ edgeDomain ? import("../common/h2pool.js").then(({ h2Pool }) => h2Pool.get(edgeDomain)).catch(() => null) : Promise.resolve(null),
167
+ ]);
168
+ // Inject the H2 session into the config so subsystems can use it
169
+ const config = { ...data, h2Session };
170
+ const instance = new SandboxInstance(config);
171
+ instance.h2Session = h2Session;
172
+ // Note: H2 session already attached via Promise.all above, no need for attachH2Session()
136
173
  // TODO remove this part once we have a better way to handle this
137
174
  if (safe) {
138
175
  try {
@@ -149,11 +186,13 @@ export class SandboxInstance {
149
186
  },
150
187
  throwOnError: true,
151
188
  });
152
- return new SandboxInstance(data);
189
+ const instance = new SandboxInstance(data);
190
+ return SandboxInstance.attachH2Session(instance);
153
191
  }
154
192
  static async list() {
155
193
  const { data } = await listSandboxes({ throwOnError: true });
156
- return data.map((sandbox) => new SandboxInstance(sandbox));
194
+ const instances = data.map((sandbox) => new SandboxInstance(sandbox));
195
+ return Promise.all(instances.map((instance) => SandboxInstance.attachH2Session(instance)));
157
196
  }
158
197
  static async delete(sandboxName) {
159
198
  const { data } = await deleteSandbox({
@@ -165,6 +204,8 @@ export class SandboxInstance {
165
204
  return data;
166
205
  }
167
206
  async delete() {
207
+ // Don't close the H2 session — it's shared via h2Pool
208
+ this.h2Session = null;
168
209
  return await SandboxInstance.delete(this.metadata.name);
169
210
  }
170
211
  static async updateMetadata(sandboxName, metadata) {
@@ -176,7 +217,7 @@ export class SandboxInstance {
176
217
  throwOnError: true,
177
218
  });
178
219
  const instance = new SandboxInstance(data);
179
- return instance;
220
+ return SandboxInstance.attachH2Session(instance);
180
221
  }
181
222
  static async updateTtl(sandboxName, ttl) {
182
223
  const sandbox = await SandboxInstance.get(sandboxName);
@@ -187,7 +228,7 @@ export class SandboxInstance {
187
228
  throwOnError: true,
188
229
  });
189
230
  const instance = new SandboxInstance(data);
190
- return instance;
231
+ return SandboxInstance.attachH2Session(instance);
191
232
  }
192
233
  static async updateLifecycle(sandboxName, lifecycle) {
193
234
  const sandbox = await SandboxInstance.get(sandboxName);
@@ -198,7 +239,7 @@ export class SandboxInstance {
198
239
  throwOnError: true,
199
240
  });
200
241
  const instance = new SandboxInstance(data);
201
- return instance;
242
+ return SandboxInstance.attachH2Session(instance);
202
243
  }
203
244
  static async createIfNotExists(sandbox) {
204
245
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blaxel/core",
3
- "version": "0.2.71-dev.101",
3
+ "version": "0.2.71-dev.105",
4
4
  "description": "Blaxel Core SDK for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": "Blaxel, INC (https://blaxel.ai)",