@blaxel/core 0.2.80-preview.138 → 0.2.80-preview.140

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 (35) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/client/interceptors.js +29 -0
  3. package/dist/cjs/common/autoload.js +8 -27
  4. package/dist/cjs/common/h2fetch.js +35 -33
  5. package/dist/cjs/common/h2pool.js +22 -2
  6. package/dist/cjs/common/lazyInit.js +98 -0
  7. package/dist/cjs/common/settings.js +18 -6
  8. package/dist/cjs/types/common/autoload.d.ts +6 -2
  9. package/dist/cjs/types/common/h2fetch.d.ts +5 -2
  10. package/dist/cjs/types/common/h2pool.d.ts +7 -2
  11. package/dist/cjs/types/common/lazyInit.d.ts +19 -0
  12. package/dist/cjs/types/common/settings.d.ts +3 -1
  13. package/dist/cjs-browser/.tsbuildinfo +1 -1
  14. package/dist/cjs-browser/client/interceptors.js +29 -0
  15. package/dist/cjs-browser/common/autoload.js +8 -27
  16. package/dist/cjs-browser/common/lazyInit.js +98 -0
  17. package/dist/cjs-browser/common/settings.js +18 -6
  18. package/dist/cjs-browser/types/common/autoload.d.ts +6 -2
  19. package/dist/cjs-browser/types/common/h2fetch.d.ts +5 -2
  20. package/dist/cjs-browser/types/common/h2pool.d.ts +7 -2
  21. package/dist/cjs-browser/types/common/lazyInit.d.ts +19 -0
  22. package/dist/cjs-browser/types/common/settings.d.ts +3 -1
  23. package/dist/esm/.tsbuildinfo +1 -1
  24. package/dist/esm/client/interceptors.js +29 -0
  25. package/dist/esm/common/autoload.js +6 -27
  26. package/dist/esm/common/h2fetch.js +35 -33
  27. package/dist/esm/common/h2pool.js +21 -2
  28. package/dist/esm/common/lazyInit.js +62 -0
  29. package/dist/esm/common/settings.js +18 -6
  30. package/dist/esm-browser/.tsbuildinfo +1 -1
  31. package/dist/esm-browser/client/interceptors.js +29 -0
  32. package/dist/esm-browser/common/autoload.js +6 -27
  33. package/dist/esm-browser/common/lazyInit.js +62 -0
  34. package/dist/esm-browser/common/settings.js +18 -6
  35. package/package.json +1 -1
@@ -1,7 +1,36 @@
1
+ import { ensureAutoloaded } from "../common/lazyInit.js";
1
2
  import { settings } from "../common/settings.js";
3
+ // Default baseUrl baked into the generated control-plane client. Requests
4
+ // carrying this prefix came from the module-load client config, before
5
+ // `ensureAutoloaded()` had a chance to resolve the real env from
6
+ // `~/.blaxel/config.yaml`.
7
+ const DEFAULT_CONTROLPLANE_BASE_URL = "https://api.blaxel.ai/v0";
2
8
  export const interceptors = [
3
9
  // Authentication interceptor
4
10
  async (request, options) => {
11
+ // Trigger deferred autoload side-effects (lazy credential resolution,
12
+ // client baseUrl sync, Sentry init, H2 warming) on the first actual SDK
13
+ // use, rather than at module-import time.
14
+ ensureAutoloaded();
15
+ // If lazy env resolution just moved the effective baseUrl off the
16
+ // module-load default (e.g. a user with `env: dev` in config.yaml but
17
+ // no BL_ENV env var), the very first request was built against the
18
+ // stale prod URL. Rebase it to the correct environment. Subsequent
19
+ // requests use the updated `client.setConfig()` applied in
20
+ // `ensureAutoloaded()`, so this branch only fires once.
21
+ //
22
+ // This must happen before the `authenticated === false` early return:
23
+ // the OAuth token request issued by `ClientCredentials.authenticate()`
24
+ // itself is unauthenticated, and if the user calls `authenticate()`
25
+ // before any other SDK call it is the very first request and also
26
+ // needs to be rebased to the correct environment.
27
+ const correctBase = settings.baseUrl;
28
+ if (correctBase !== DEFAULT_CONTROLPLANE_BASE_URL &&
29
+ request.url.startsWith(DEFAULT_CONTROLPLANE_BASE_URL)) {
30
+ const newUrl = correctBase.replace(/\/$/, "") +
31
+ request.url.slice(DEFAULT_CONTROLPLANE_BASE_URL.length);
32
+ request = new Request(newUrl, request);
33
+ }
5
34
  if (options.authenticated === false) {
6
35
  return request;
7
36
  }
@@ -2,8 +2,8 @@ import { client } from "../client/client.gen.js";
2
2
  import { interceptors } from "../client/interceptors.js";
3
3
  import { responseInterceptors } from "../client/responseInterceptor.js";
4
4
  import { client as clientSandbox } from "../sandbox/client/client.gen.js";
5
- import { initSentry } from "./sentry.js";
6
5
  import { settings } from "./settings.js";
6
+ export { ensureAutoloaded } from "./lazyInit.js";
7
7
  client.setConfig({
8
8
  baseUrl: settings.baseUrl,
9
9
  });
@@ -19,34 +19,15 @@ for (const interceptor of responseInterceptors) {
19
19
  client.interceptors.response.use(interceptor);
20
20
  clientSandbox.interceptors.response.use(interceptor);
21
21
  }
22
- // Initialize Sentry for SDK error tracking immediately when module loads
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
- }
46
22
  /**
47
23
  * Configure the SDK programmatically at runtime, instead of relying on
48
24
  * environment variables or config files.
49
25
  *
26
+ * You do not need to call {@link authenticate} yourself: the SDK
27
+ * transparently authenticates (and refreshes tokens when needed) on every
28
+ * request. Only call `authenticate()` directly if you want to fail fast on
29
+ * invalid credentials before making any API call.
30
+ *
50
31
  * @example
51
32
  * // With an API key
52
33
  * initialize({ workspace: 'my-workspace', apiKey: 'bl_...' });
@@ -57,7 +38,6 @@ if (isNode && !isBrowser) {
57
38
  * workspace: 'my-workspace',
58
39
  * clientCredentials: { clientId: '...', clientSecret: '...' },
59
40
  * });
60
- * await authenticate();
61
41
  *
62
42
  * @example
63
43
  * // With client credentials (pre-encoded Base64 string)
@@ -65,7 +45,6 @@ if (isNode && !isBrowser) {
65
45
  * workspace: 'my-workspace',
66
46
  * clientCredentials: 'base64-encoded-string',
67
47
  * });
68
- * await authenticate();
69
48
  */
70
49
  export function initialize(config) {
71
50
  settings.setConfig(config);
@@ -1,8 +1,10 @@
1
- const H2_REQUEST_TIMEOUT_MS = 10_000;
2
1
  /**
3
2
  * 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.
3
+ * HTTP/2 session. Falls back to globalThis.fetch() only when the session is
4
+ * closed/destroyed at call time (pre-flight, nothing sent on the wire).
5
+ *
6
+ * Any failure after session.request() succeeds propagates to the caller:
7
+ * this transport never retries. Retry and timeout policy are caller concerns.
6
8
  */
7
9
  export function createH2Fetch(session) {
8
10
  return (input) => {
@@ -73,7 +75,8 @@ export function h2RequestDirect(session, url, init) {
73
75
  }
74
76
  else {
75
77
  // FormData, ReadableStream, Blob, etc. can't be serialized to Buffer
76
- // for manual H2 framing — fall back to regular fetch.
78
+ // for manual H2 framing — fall back to regular fetch (pre-flight,
79
+ // nothing has been sent on the wire yet).
77
80
  return globalThis.fetch(url, init);
78
81
  }
79
82
  if (!h2Headers["content-length"]) {
@@ -111,42 +114,49 @@ async function _h2Request(session, input) {
111
114
  }
112
115
  function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
113
116
  return new Promise((resolve, reject) => {
117
+ let settled = false;
118
+ let responded = false;
119
+ let streamController = null;
120
+ let streamClosed = false;
114
121
  let req;
115
122
  try {
116
123
  req = session.request(h2Headers);
117
124
  }
118
125
  catch {
119
- return globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
126
+ // Pre-flight fallback: session.request() threw synchronously, so no
127
+ // H2 frames were sent. Safe to retry over globalThis.fetch.
128
+ globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
129
+ return;
120
130
  }
121
- const timer = setTimeout(() => {
122
- if (settled)
123
- return;
124
- settled = true;
131
+ const abort = () => {
125
132
  req.close();
126
- globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
127
- }, H2_REQUEST_TIMEOUT_MS);
133
+ const abortError = new DOMException("The operation was aborted.", "AbortError");
134
+ if (!responded) {
135
+ if (!settled) {
136
+ settled = true;
137
+ reject(abortError);
138
+ }
139
+ return;
140
+ }
141
+ if (!streamClosed) {
142
+ streamClosed = true;
143
+ streamController?.error(abortError);
144
+ }
145
+ };
128
146
  if (signal) {
129
147
  if (signal.aborted) {
130
- clearTimeout(timer);
131
148
  req.close();
149
+ settled = true;
132
150
  reject(new DOMException("The operation was aborted.", "AbortError"));
133
151
  return;
134
152
  }
135
- signal.addEventListener("abort", () => {
136
- clearTimeout(timer);
137
- req.close();
138
- if (!settled) {
139
- settled = true;
140
- reject(new DOMException("The operation was aborted.", "AbortError"));
141
- }
142
- }, { once: true });
153
+ signal.addEventListener("abort", abort, { once: true });
143
154
  }
144
- let settled = false;
145
155
  req.on("response", (headers) => {
146
- clearTimeout(timer);
147
156
  if (settled)
148
157
  return;
149
158
  settled = true;
159
+ responded = true;
150
160
  const status = headers[":status"] ?? 200;
151
161
  const resHeaders = new Headers();
152
162
  for (const [k, v] of Object.entries(headers)) {
@@ -156,9 +166,9 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
156
166
  continue;
157
167
  resHeaders.set(k, Array.isArray(v) ? v.join(", ") : String(v));
158
168
  }
159
- let streamClosed = false;
160
169
  const readable = new ReadableStream({
161
170
  start(controller) {
171
+ streamController = controller;
162
172
  req.on("data", (chunk) => {
163
173
  if (!streamClosed)
164
174
  controller.enqueue(new Uint8Array(chunk));
@@ -175,23 +185,15 @@ function _h2Send(session, h2Headers, body, signal, fallbackUrl, fallbackInit) {
175
185
  controller.error(err);
176
186
  }
177
187
  });
178
- signal?.addEventListener("abort", () => {
179
- req.close();
180
- if (!streamClosed) {
181
- streamClosed = true;
182
- controller.error(new DOMException("The operation was aborted.", "AbortError"));
183
- }
184
- }, { once: true });
185
188
  },
186
189
  });
187
190
  resolve(new Response(readable, { status, headers: resHeaders }));
188
191
  });
189
- req.on("error", () => {
190
- clearTimeout(timer);
192
+ req.on("error", (err) => {
191
193
  if (settled)
192
194
  return;
193
195
  settled = true;
194
- globalThis.fetch(fallbackUrl, fallbackInit).then(resolve, reject);
196
+ reject(err);
195
197
  });
196
198
  if (body) {
197
199
  req.end(body);
@@ -6,20 +6,39 @@
6
6
  * an in-flight warming, or establishes a fresh one.
7
7
  * - Closed / destroyed sessions are automatically evicted.
8
8
  */
9
- class H2Pool {
9
+ export class H2Pool {
10
10
  sessions = new Map();
11
11
  inflight = new Map();
12
12
  _establish = null;
13
13
  /**
14
14
  * Lazily resolve the establish function so the http2 / tls / dns modules
15
15
  * are only imported in Node.js environments.
16
+ *
17
+ * Wires up self-healing eviction: the session is removed from the cache
18
+ * as soon as it emits `goaway`, `error`, or `close`, so `tryGet()` never
19
+ * returns a dead session. This replaces the old behavior of papering
20
+ * over session failures at the fetch layer.
16
21
  */
17
22
  async establish(domain) {
18
23
  if (!this._establish) {
19
24
  const { establishH2 } = await import("./h2warm.js");
20
25
  this._establish = establishH2;
21
26
  }
22
- return this._establish(domain);
27
+ const session = await this._establish(domain);
28
+ this.attachEvictionListeners(domain, session);
29
+ return session;
30
+ }
31
+ attachEvictionListeners(domain, session) {
32
+ const evict = () => {
33
+ // Only evict if this specific session is still the cached one.
34
+ // A newer session may have taken its place after reconnect.
35
+ if (this.sessions.get(domain) === session) {
36
+ this.sessions.delete(domain);
37
+ }
38
+ };
39
+ session.on("goaway", evict);
40
+ session.on("error", evict);
41
+ session.on("close", evict);
23
42
  }
24
43
  /**
25
44
  * Fire-and-forget background warming. Safe to call multiple times for
@@ -0,0 +1,62 @@
1
+ import { client } from "../client/client.gen.js";
2
+ import { client as clientSandbox } from "../sandbox/client/client.gen.js";
3
+ import { initSentry } from "./sentry.js";
4
+ import { settings } from "./settings.js";
5
+ let autoloaded = false;
6
+ /**
7
+ * Perform the observable side-effects that the SDK needs for error
8
+ * reporting, low-latency sandbox sessions, and correct environment
9
+ * routing:
10
+ *
11
+ * - Resolve credentials (which reads `~/.blaxel/config.yaml` once and
12
+ * populates `BL_ENV` if the user has a dev workspace configured).
13
+ * - Re-apply the control-plane and sandbox clients' `baseUrl` so they
14
+ * target the now-env-aware endpoint.
15
+ * - Initialize the lightweight Sentry client (registers
16
+ * `uncaughtExceptionMonitor` and patches `console.error` in Node).
17
+ * - Pre-warm the edge H2 connection pool for `settings.region`.
18
+ *
19
+ * These used to run at module load, but that meant `import "@blaxel/core"`
20
+ * alone had observable side-effects. They are now deferred until the SDK
21
+ * is first actually used — the request interceptor calls this on the
22
+ * first HTTP request.
23
+ */
24
+ export function ensureAutoloaded() {
25
+ if (autoloaded)
26
+ return;
27
+ autoloaded = true;
28
+ // Trigger lazy credential resolution. This may read `~/.blaxel/config.yaml`
29
+ // and set `process.env.BL_ENV` if the user has a dev workspace configured,
30
+ // which in turn affects `settings.baseUrl`.
31
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
32
+ settings.credentials;
33
+ // Keep the clients' baseUrl in sync with the now-resolved env. Without
34
+ // this, the module-load `client.setConfig({ baseUrl })` would be stuck on
35
+ // the prod default for users who rely on `config.yaml` (no env vars).
36
+ client.setConfig({ baseUrl: settings.baseUrl });
37
+ clientSandbox.setConfig({ baseUrl: settings.baseUrl });
38
+ // Initialize Sentry for SDK error tracking.
39
+ initSentry();
40
+ // Background H2 connection warming (Node.js only)
41
+ const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
42
+ /* eslint-disable */
43
+ const isBrowser = typeof globalThis !== "undefined" && globalThis?.window !== undefined;
44
+ if (isNode && !isBrowser) {
45
+ try {
46
+ // Pre-warm edge H2 for the configured region so the first
47
+ // SandboxInstance.create() gets an instant session via the pool.
48
+ // The control-plane client (api.blaxel.ai) stays on regular fetch
49
+ // which already benefits from undici's built-in connection pooling.
50
+ const region = settings.region;
51
+ if (region) {
52
+ import("./h2pool.js").then(({ h2Pool }) => {
53
+ const edgeSuffix = settings.env === "prod" ? "bl.run" : "runv2.blaxel.dev";
54
+ h2Pool.warm(`any.${region}.${edgeSuffix}`);
55
+ }).catch(() => { });
56
+ }
57
+ }
58
+ catch {
59
+ // Silently ignore warming failures
60
+ }
61
+ }
62
+ }
@@ -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.80-preview.138";
9
- const BUILD_COMMIT = "a58f8cc475d6ca3147ed31106d71a68e504c55a0";
8
+ const BUILD_VERSION = "0.2.80-preview.140";
9
+ const BUILD_COMMIT = "800934e57ec03de060b909ccb1f5599343cae9e5";
10
10
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
11
11
  // Cache for config.yaml tracking value
12
12
  let configTrackingValue = null;
@@ -66,20 +66,32 @@ function getOsArch() {
66
66
  return "browser/unknown";
67
67
  }
68
68
  class Settings {
69
- credentials;
69
+ _credentials;
70
70
  config;
71
71
  constructor() {
72
- this.credentials = authentication();
72
+ // `credentials` are resolved lazily on first access so that simply
73
+ // importing `@blaxel/core` does not read `~/.blaxel/config.yaml`
74
+ // or mutate `process.env.BL_ENV`. See the `credentials` getter.
75
+ this._credentials = null;
73
76
  this.config = {
74
77
  proxy: "",
75
78
  apikey: "",
76
79
  workspace: "",
77
80
  };
78
81
  }
82
+ get credentials() {
83
+ if (this._credentials === null) {
84
+ this._credentials = authentication();
85
+ }
86
+ return this._credentials;
87
+ }
88
+ set credentials(value) {
89
+ this._credentials = value;
90
+ }
79
91
  setConfig(config) {
80
92
  this.config = config;
81
93
  if (config.apiKey) {
82
- this.credentials = new ApiKey({
94
+ this._credentials = new ApiKey({
83
95
  apiKey: config.apiKey,
84
96
  workspace: config.workspace,
85
97
  });
@@ -88,7 +100,7 @@ class Settings {
88
100
  const encoded = typeof config.clientCredentials === 'string'
89
101
  ? config.clientCredentials
90
102
  : btoa(`${config.clientCredentials.clientId}:${config.clientCredentials.clientSecret}`);
91
- this.credentials = new ClientCredentials({
103
+ this._credentials = new ClientCredentials({
92
104
  clientCredentials: encoded,
93
105
  workspace: config.workspace,
94
106
  });