@blaxel/core 0.3.6 → 0.3.7-preview.229

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.
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP/2 runtime-capability detection.
4
+ *
5
+ * The SDK's pooled HTTP/2 transport relies on the runtime's HTTP/2 client
6
+ * managing connection-level flow control correctly. One runtime does not:
7
+ *
8
+ * Bun < 1.3.11 never sends a connection-level WINDOW_UPDATE frame. The pooled
9
+ * H2 session therefore freezes after exactly 65535 cumulative body bytes (the
10
+ * default connection receive window that Bun never grows), and every request
11
+ * on that session hangs until the edge resets the streams (~330s).
12
+ * Fixed in Bun 1.3.11: https://bun.com/blog/bun-v1.3.11
13
+ *
14
+ * This module centralizes the version gate so the settings layer, the unit
15
+ * tests, and the cross-runtime environment tests all agree on ONE definition of
16
+ * "is this a Bun that breaks pooled H2". Keeping a single source of truth is the
17
+ * whole point: the bug this guards against is subtle and the parse is easy to
18
+ * get subtly wrong (see `stripPrerelease`).
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.H2_DEFAULT_CONNECTION_WINDOW_BYTES = exports.BUN_H2_FIXED_VERSION = void 0;
22
+ exports.parseSemver = parseSemver;
23
+ exports.isBrokenBunVersion = isBrokenBunVersion;
24
+ exports.detectBunVersion = detectBunVersion;
25
+ exports.isBrokenBunH2Runtime = isBrokenBunH2Runtime;
26
+ /** First Bun release with a working connection-level WINDOW_UPDATE. */
27
+ exports.BUN_H2_FIXED_VERSION = "1.3.11";
28
+ /**
29
+ * The default HTTP/2 connection-level receive window, in bytes (2^16 - 1). This
30
+ * is the exact number of cumulative body bytes after which a broken-Bun pooled
31
+ * session freezes: the window opens to 65535 and, absent a WINDOW_UPDATE, never
32
+ * reopens. Node grows it (see `establishH2`'s `setLocalWindowSize`); Bun < 1.3.11
33
+ * does not.
34
+ */
35
+ exports.H2_DEFAULT_CONNECTION_WINDOW_BYTES = 65535;
36
+ /**
37
+ * Parse a `major.minor.patch` version, tolerating a pre-release/build suffix
38
+ * (e.g. `1.3.11-canary.1`, `1.4.0+build`). Returns `null` when the input is
39
+ * absent or not a recognizable semver triple.
40
+ */
41
+ function parseSemver(version) {
42
+ if (!version)
43
+ return null;
44
+ // Strip any pre-release ("-canary.1") or build ("+abc") metadata first so a
45
+ // tagged build compares by its release numbers, then take the leading triple.
46
+ const core = version.split("-")[0].split("+")[0].trim();
47
+ const parts = core.split(".");
48
+ if (parts.length === 0)
49
+ return null;
50
+ const nums = parts.slice(0, 3).map((p) => Number(p));
51
+ if (nums.some((n) => !Number.isInteger(n) || n < 0))
52
+ return null;
53
+ const [maj = 0, min = 0, patch = 0] = nums;
54
+ return [maj, min, patch];
55
+ }
56
+ /**
57
+ * Given a Bun version string, is this a Bun whose HTTP/2 client breaks the
58
+ * pooled transport (Bun < 1.3.11)? A falsy/undefined version means "not Bun",
59
+ * which returns `false`. An unparseable version is treated as broken (`true`):
60
+ * we cannot prove it is safe, and forcing H2 off is the safe default.
61
+ */
62
+ function isBrokenBunVersion(version) {
63
+ if (!version)
64
+ return false;
65
+ const parsed = parseSemver(version);
66
+ if (parsed === null)
67
+ return true;
68
+ const [maj, min, patch] = parsed;
69
+ return maj < 1 || (maj === 1 && (min < 3 || (min === 3 && patch < 11)));
70
+ }
71
+ /**
72
+ * The running runtime's Bun version, or `undefined` when not on Bun (Node,
73
+ * Deno, browsers, workers). Deno also exposes `process.versions` but never sets
74
+ * `.bun`, so this stays `undefined` there.
75
+ */
76
+ function detectBunVersion() {
77
+ return globalThis.process?.versions?.bun;
78
+ }
79
+ /**
80
+ * Is the CURRENT runtime a Bun that breaks the pooled H2 transport? This is the
81
+ * predicate the settings layer uses to force H2 off.
82
+ */
83
+ function isBrokenBunH2Runtime() {
84
+ return isBrokenBunVersion(detectBunVersion());
85
+ }
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const env_js_1 = require("./env.js");
38
+ const h2_runtime_js_1 = require("./h2-runtime.js");
39
+ // The Bun H2 flow-control bug (never sends connection-level WINDOW_UPDATE, so a
40
+ // pooled session freezes after 65535 cumulative body bytes) was fixed in
41
+ // 1.3.11. Everything below < 1.3.11 must be classified "broken"; >= 1.3.11 must
42
+ // be "fixed". A non-Bun runtime (no version) is never broken. This is the
43
+ // single most important gate in the SDK's H2 story, so we pin it exhaustively.
44
+ (0, vitest_1.describe)("parseSemver", () => {
45
+ (0, vitest_1.it)("parses a plain major.minor.patch triple", () => {
46
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.3.11")).toEqual([1, 3, 11]);
47
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("0.0.0")).toEqual([0, 0, 0]);
48
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("22.14.0")).toEqual([22, 14, 0]);
49
+ });
50
+ (0, vitest_1.it)("tolerates missing minor/patch", () => {
51
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("2")).toEqual([2, 0, 0]);
52
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.4")).toEqual([1, 4, 0]);
53
+ });
54
+ (0, vitest_1.it)("strips pre-release and build metadata before comparing", () => {
55
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.3.11-canary.1")).toEqual([1, 3, 11]);
56
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.3.10-debug")).toEqual([1, 3, 10]);
57
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.4.0+build.7")).toEqual([1, 4, 0]);
58
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("2.0.0-alpha+exp.sha.5114f85")).toEqual([2, 0, 0]);
59
+ });
60
+ (0, vitest_1.it)("returns null for absent or non-numeric input", () => {
61
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)(undefined)).toBeNull();
62
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)(null)).toBeNull();
63
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("")).toBeNull();
64
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("latest")).toBeNull();
65
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("v1.2.3")).toBeNull();
66
+ (0, vitest_1.expect)((0, h2_runtime_js_1.parseSemver)("1.x.0")).toBeNull();
67
+ });
68
+ });
69
+ (0, vitest_1.describe)("isBrokenBunVersion", () => {
70
+ (0, vitest_1.it)("treats a missing version as not-Bun (never broken)", () => {
71
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(undefined)).toBe(false);
72
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(null)).toBe(false);
73
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)("")).toBe(false);
74
+ });
75
+ // The full boundary matrix. Left column = Bun version string, right = whether
76
+ // the pooled H2 transport is broken on it.
77
+ const matrix = [
78
+ // Ancient / pre-1.0 — all broken.
79
+ ["0.1.0", true],
80
+ ["0.8.1", true],
81
+ ["0.9.9", true],
82
+ // 1.0.x – 1.2.x — broken.
83
+ ["1.0.0", true],
84
+ ["1.1.34", true],
85
+ ["1.2.0", true],
86
+ ["1.2.21", true],
87
+ // 1.3.0 – 1.3.10 — the affected line, still broken.
88
+ ["1.3.0", true],
89
+ ["1.3.1", true],
90
+ ["1.3.9", true],
91
+ ["1.3.10", true],
92
+ // 1.3.11 — the exact fix boundary. Fixed.
93
+ ["1.3.11", false],
94
+ // Everything after — fixed.
95
+ ["1.3.12", false],
96
+ ["1.3.100", false],
97
+ ["1.4.0", false],
98
+ ["1.10.0", false],
99
+ ["2.0.0", false],
100
+ ["10.0.0", false],
101
+ ];
102
+ vitest_1.it.each(matrix)("Bun %s -> broken=%s", (version, expected) => {
103
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(version)).toBe(expected);
104
+ });
105
+ // Pre-release / canary builds must compare by their release numbers so a
106
+ // "1.3.11-canary" is treated as fixed and a "1.3.10-debug" as broken. This is
107
+ // the exact parse subtlety the earlier duplicated gate got wrong.
108
+ const prereleaseMatrix = [
109
+ ["1.3.10-canary.20250101", true],
110
+ ["1.3.11-canary.20250101", false],
111
+ ["1.3.11-debug", false],
112
+ ["1.4.0-canary", false],
113
+ ["1.2.99-alpha", true],
114
+ ];
115
+ vitest_1.it.each(prereleaseMatrix)("Bun %s (pre-release) -> broken=%s", (version, expected) => {
116
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(version)).toBe(expected);
117
+ });
118
+ (0, vitest_1.it)("classifies an unparseable Bun version as broken (safe default)", () => {
119
+ // A non-empty but unrecognizable version means we cannot prove it is safe,
120
+ // so we err toward disabling H2.
121
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)("weird-build")).toBe(true);
122
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)("nightly")).toBe(true);
123
+ });
124
+ (0, vitest_1.it)("agrees with the documented fix boundary constant", () => {
125
+ const [maj, min, patch] = (0, h2_runtime_js_1.parseSemver)(h2_runtime_js_1.BUN_H2_FIXED_VERSION);
126
+ // One patch below the fix is broken; the fix itself is not.
127
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(`${maj}.${min}.${patch - 1}`)).toBe(true);
128
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunVersion)(`${maj}.${min}.${patch}`)).toBe(false);
129
+ });
130
+ });
131
+ (0, vitest_1.describe)("detectBunVersion / isBrokenBunH2Runtime (current runtime)", () => {
132
+ (0, vitest_1.it)("reports the running Bun version, or undefined off Bun", () => {
133
+ const detected = (0, h2_runtime_js_1.detectBunVersion)();
134
+ const actual = globalThis.process?.versions?.bun;
135
+ (0, vitest_1.expect)(detected).toBe(actual);
136
+ });
137
+ (0, vitest_1.it)("runtime predicate matches the version predicate for this runtime", () => {
138
+ (0, vitest_1.expect)((0, h2_runtime_js_1.isBrokenBunH2Runtime)()).toBe((0, h2_runtime_js_1.isBrokenBunVersion)((0, h2_runtime_js_1.detectBunVersion)()));
139
+ });
140
+ });
141
+ (0, vitest_1.describe)("H2_DEFAULT_CONNECTION_WINDOW_BYTES", () => {
142
+ (0, vitest_1.it)("is 2^16 - 1 (the window a broken Bun never grows)", () => {
143
+ (0, vitest_1.expect)(h2_runtime_js_1.H2_DEFAULT_CONNECTION_WINDOW_BYTES).toBe(65535);
144
+ (0, vitest_1.expect)(h2_runtime_js_1.H2_DEFAULT_CONNECTION_WINDOW_BYTES).toBe(2 ** 16 - 1);
145
+ });
146
+ });
147
+ // End-to-end: the settings.disableH2 getter must honor the runtime gate. We
148
+ // simulate different Bun versions by injecting process.versions.bun, since the
149
+ // getter reads it live on every access.
150
+ (0, vitest_1.describe)("settings.disableH2 honors the Bun version gate", () => {
151
+ const originalBun = globalThis.process?.versions?.bun;
152
+ (0, vitest_1.afterEach)(async () => {
153
+ if (globalThis.process?.versions) {
154
+ if (originalBun === undefined) {
155
+ delete globalThis.process.versions.bun;
156
+ }
157
+ else {
158
+ globalThis.process.versions.bun = originalBun;
159
+ }
160
+ }
161
+ delete env_js_1.env.BL_DISABLE_H2;
162
+ const { settings } = await Promise.resolve().then(() => __importStar(require("./settings.js")));
163
+ delete settings.config.disableH2;
164
+ });
165
+ function setBunVersion(version) {
166
+ if (!globalThis.process?.versions)
167
+ return false;
168
+ if (version === undefined) {
169
+ delete globalThis.process.versions.bun;
170
+ }
171
+ else {
172
+ globalThis.process.versions.bun = version;
173
+ }
174
+ return true;
175
+ }
176
+ (0, vitest_1.it)("forces H2 OFF on a broken Bun regardless of config/env", async () => {
177
+ if (!setBunVersion("1.3.10"))
178
+ return;
179
+ delete env_js_1.env.BL_DISABLE_H2;
180
+ const { settings } = await Promise.resolve().then(() => __importStar(require("./settings.js")));
181
+ // Even explicitly asking for H2 on cannot override the safety gate.
182
+ settings.config.disableH2 = false;
183
+ (0, vitest_1.expect)(settings.disableH2).toBe(true);
184
+ });
185
+ (0, vitest_1.it)("leaves H2 ON (default) on a fixed Bun", async () => {
186
+ if (!setBunVersion("1.3.11"))
187
+ return;
188
+ delete env_js_1.env.BL_DISABLE_H2;
189
+ const { settings } = await Promise.resolve().then(() => __importStar(require("./settings.js")));
190
+ delete settings.config.disableH2;
191
+ (0, vitest_1.expect)(settings.disableH2).toBe(false);
192
+ });
193
+ (0, vitest_1.it)("still respects an explicit opt-out on a fixed Bun", async () => {
194
+ if (!setBunVersion("1.4.0"))
195
+ return;
196
+ const { settings } = await Promise.resolve().then(() => __importStar(require("./settings.js")));
197
+ settings.config.disableH2 = true;
198
+ (0, vitest_1.expect)(settings.disableH2).toBe(true);
199
+ });
200
+ });
@@ -11,6 +11,7 @@ const credentials_js_1 = require("../authentication/credentials.js");
11
11
  const index_js_1 = require("../authentication/index.js");
12
12
  const env_js_1 = require("../common/env.js");
13
13
  const errors_js_1 = require("../common/errors.js");
14
+ const h2_runtime_js_1 = require("../common/h2-runtime.js");
14
15
  const logger_js_1 = require("../common/logger.js");
15
16
  const node_js_1 = require("../common/node.js");
16
17
  /**
@@ -29,21 +30,17 @@ function missingCredentialsMessage() {
29
30
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
30
31
  }
31
32
  // Build info - these placeholders are replaced at build time by build:replace-imports
32
- const BUILD_VERSION = "0.3.6";
33
- const BUILD_COMMIT = "3189881cfb78a38e368844ba3fc867f83933cade";
33
+ const BUILD_VERSION = "0.3.7-preview.229";
34
+ const BUILD_COMMIT = "23d40862a7ff5f350d98700c4c2e47adb91eddc5";
34
35
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
35
36
  const BLAXEL_API_VERSION = "2026-04-28";
36
37
  // Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
37
38
  // session freezes after exactly 65535 cumulative body bytes and every request
38
39
  // on it hangs until the edge resets the streams (~330s).
39
40
  // Fixed in Bun 1.3.11: https://bun.com/blog/bun-v1.3.11
40
- function isBrokenBunH2() {
41
- const v = globalThis.process?.versions?.bun;
42
- if (!v)
43
- return false;
44
- const [maj = 0, min = 0, patch = 0] = v.split("-")[0].split(".").map(Number);
45
- return maj < 1 || (maj === 1 && (min < 3 || (min === 3 && patch < 11)));
46
- }
41
+ // The version gate lives in ./h2-runtime.ts so settings, unit tests, and the
42
+ // cross-runtime environment tests share ONE definition.
43
+ const isBrokenBunH2 = h2_runtime_js_1.isBrokenBunH2Runtime;
47
44
  // Warn at most once when H2 is force-disabled on a broken Bun runtime.
48
45
  let brokenBunH2Warned = false;
49
46
  // Cache for config.yaml tracking value
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const vitest_1 = require("vitest");
37
37
  const apikey_js_1 = require("../authentication/apikey.js");
38
38
  const env_js_1 = require("./env.js");
39
+ const h2_runtime_js_1 = require("./h2-runtime.js");
39
40
  (0, vitest_1.describe)('Settings.apiVersion', () => {
40
41
  (0, vitest_1.beforeEach)(() => {
41
42
  // Reset the module-level settings singleton between tests if needed
@@ -68,13 +69,10 @@ const env_js_1 = require("./env.js");
68
69
  const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
69
70
  delete settings.config.disableH2;
70
71
  });
71
- const onBrokenBun = (() => {
72
- const v = globalThis.process?.versions?.bun;
73
- if (!v)
74
- return false;
75
- const [maj = 0, min = 0, patch = 0] = v.split('.').map(Number);
76
- return maj < 1 || (maj === 1 && (min < 3 || (min === 3 && patch < 11)));
77
- })();
72
+ // On a broken-Bun runtime the getter force-returns true regardless of
73
+ // config/env, so the "H2 on by default" expectations below do not hold there.
74
+ // Use the SAME predicate the getter uses so the skip and the behavior agree.
75
+ const onBrokenBun = (0, h2_runtime_js_1.isBrokenBunH2Runtime)();
78
76
  vitest_1.it.skipIf(onBrokenBun)('enables H2 by default', async () => {
79
77
  delete env_js_1.env.BL_DISABLE_H2;
80
78
  const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
@@ -87,3 +85,169 @@ const env_js_1 = require("./env.js");
87
85
  (0, vitest_1.expect)(settings.disableH2).toBe(true);
88
86
  });
89
87
  });
88
+ // The H2 tuning knobs all share the same resolution order: an explicit
89
+ // `settings.config` value wins, then the env var (`env` reads through to
90
+ // process.env), then a hard-coded default. Numeric knobs also reject
91
+ // unparseable/out-of-range env strings and fall back to the default. These are
92
+ // the values that flow into establishH2 (window sizes) and the per-domain
93
+ // concurrency gates, so an off-by-one here silently changes transport behavior.
94
+ (0, vitest_1.describe)('Settings H2 tuning knobs (config > env > default)', () => {
95
+ // Snapshot every H2 env var so a test that sets one cannot leak into another.
96
+ const H2_ENV_VARS = [
97
+ 'BL_DISABLE_CONTROL_PLANE_H2',
98
+ 'BL_FORCE_CONTROL_PLANE_H2',
99
+ 'BL_MAX_H2_INFLIGHT',
100
+ 'BL_MAX_UPLOAD_H2_INFLIGHT',
101
+ 'BL_H2_STREAM_WINDOW',
102
+ 'BL_H2_CONNECTION_WINDOW',
103
+ ];
104
+ const originalEnv = {};
105
+ (0, vitest_1.beforeEach)(() => {
106
+ for (const key of H2_ENV_VARS) {
107
+ originalEnv[key] = process.env[key];
108
+ delete process.env[key];
109
+ }
110
+ });
111
+ (0, vitest_1.afterEach)(async () => {
112
+ for (const key of H2_ENV_VARS) {
113
+ if (originalEnv[key] === undefined)
114
+ delete process.env[key];
115
+ else
116
+ process.env[key] = originalEnv[key];
117
+ }
118
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
119
+ delete settings.config.disableControlPlaneH2;
120
+ delete settings.config.forceControlPlaneH2;
121
+ delete settings.config.maxConcurrentH2Requests;
122
+ delete settings.config.maxConcurrentUploadH2Requests;
123
+ delete settings.config.h2StreamWindowSize;
124
+ delete settings.config.h2ConnectionWindowSize;
125
+ });
126
+ (0, vitest_1.describe)('disableControlPlaneH2', () => {
127
+ (0, vitest_1.it)('defaults to false', async () => {
128
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
129
+ (0, vitest_1.expect)(settings.disableControlPlaneH2).toBe(false);
130
+ });
131
+ vitest_1.it.each(['1', 'true', 'yes', 'on', 'TRUE', 'On'])('treats env %s as true', async (value) => {
132
+ process.env.BL_DISABLE_CONTROL_PLANE_H2 = value;
133
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
134
+ (0, vitest_1.expect)(settings.disableControlPlaneH2).toBe(true);
135
+ });
136
+ vitest_1.it.each(['0', 'false', 'no', 'off', ''])('treats env %s as false', async (value) => {
137
+ process.env.BL_DISABLE_CONTROL_PLANE_H2 = value;
138
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
139
+ (0, vitest_1.expect)(settings.disableControlPlaneH2).toBe(false);
140
+ });
141
+ (0, vitest_1.it)('config value wins over the env var', async () => {
142
+ process.env.BL_DISABLE_CONTROL_PLANE_H2 = '1';
143
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
144
+ settings.config.disableControlPlaneH2 = false;
145
+ (0, vitest_1.expect)(settings.disableControlPlaneH2).toBe(false);
146
+ });
147
+ });
148
+ (0, vitest_1.describe)('forceControlPlaneH2', () => {
149
+ (0, vitest_1.it)('defaults to false', async () => {
150
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
151
+ (0, vitest_1.expect)(settings.forceControlPlaneH2).toBe(false);
152
+ });
153
+ (0, vitest_1.it)('reads a truthy env var', async () => {
154
+ process.env.BL_FORCE_CONTROL_PLANE_H2 = 'yes';
155
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
156
+ (0, vitest_1.expect)(settings.forceControlPlaneH2).toBe(true);
157
+ });
158
+ (0, vitest_1.it)('config value wins over the env var', async () => {
159
+ process.env.BL_FORCE_CONTROL_PLANE_H2 = '1';
160
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
161
+ settings.config.forceControlPlaneH2 = false;
162
+ (0, vitest_1.expect)(settings.forceControlPlaneH2).toBe(false);
163
+ });
164
+ });
165
+ (0, vitest_1.describe)('maxConcurrentH2Requests', () => {
166
+ (0, vitest_1.it)('defaults to 0 (unlimited)', async () => {
167
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
168
+ (0, vitest_1.expect)(settings.maxConcurrentH2Requests).toBe(0);
169
+ });
170
+ (0, vitest_1.it)('parses the env var', async () => {
171
+ process.env.BL_MAX_H2_INFLIGHT = '8';
172
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
173
+ (0, vitest_1.expect)(settings.maxConcurrentH2Requests).toBe(8);
174
+ });
175
+ (0, vitest_1.it)('config value wins over the env var', async () => {
176
+ process.env.BL_MAX_H2_INFLIGHT = '8';
177
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
178
+ settings.config.maxConcurrentH2Requests = 3;
179
+ (0, vitest_1.expect)(settings.maxConcurrentH2Requests).toBe(3);
180
+ });
181
+ (0, vitest_1.it)('falls back to the default on an unparseable env var', async () => {
182
+ process.env.BL_MAX_H2_INFLIGHT = 'not-a-number';
183
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
184
+ (0, vitest_1.expect)(settings.maxConcurrentH2Requests).toBe(0);
185
+ });
186
+ });
187
+ (0, vitest_1.describe)('maxConcurrentUploadH2Requests', () => {
188
+ (0, vitest_1.it)('defaults to 2 (the measured rapid-reset-safe cap)', async () => {
189
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
190
+ (0, vitest_1.expect)(settings.maxConcurrentUploadH2Requests).toBe(2);
191
+ });
192
+ (0, vitest_1.it)('parses the env var, including 0 to disable the cap', async () => {
193
+ process.env.BL_MAX_UPLOAD_H2_INFLIGHT = '0';
194
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
195
+ (0, vitest_1.expect)(settings.maxConcurrentUploadH2Requests).toBe(0);
196
+ });
197
+ (0, vitest_1.it)('config value wins over the env var', async () => {
198
+ process.env.BL_MAX_UPLOAD_H2_INFLIGHT = '5';
199
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
200
+ settings.config.maxConcurrentUploadH2Requests = 4;
201
+ (0, vitest_1.expect)(settings.maxConcurrentUploadH2Requests).toBe(4);
202
+ });
203
+ (0, vitest_1.it)('falls back to the default on an unparseable env var', async () => {
204
+ process.env.BL_MAX_UPLOAD_H2_INFLIGHT = 'xyz';
205
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
206
+ (0, vitest_1.expect)(settings.maxConcurrentUploadH2Requests).toBe(2);
207
+ });
208
+ });
209
+ (0, vitest_1.describe)('h2StreamWindowSize', () => {
210
+ (0, vitest_1.it)('defaults to 16 MiB', async () => {
211
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
212
+ (0, vitest_1.expect)(settings.h2StreamWindowSize).toBe(16 * 1024 * 1024);
213
+ });
214
+ (0, vitest_1.it)('parses a positive env var', async () => {
215
+ process.env.BL_H2_STREAM_WINDOW = String(4 * 1024 * 1024);
216
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
217
+ (0, vitest_1.expect)(settings.h2StreamWindowSize).toBe(4 * 1024 * 1024);
218
+ });
219
+ (0, vitest_1.it)('config value wins over the env var', async () => {
220
+ process.env.BL_H2_STREAM_WINDOW = '123456';
221
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
222
+ settings.config.h2StreamWindowSize = 654321;
223
+ (0, vitest_1.expect)(settings.h2StreamWindowSize).toBe(654321);
224
+ });
225
+ vitest_1.it.each(['0', '-1', 'nan'])('ignores non-positive/unparseable env value %s and uses the default', async (value) => {
226
+ process.env.BL_H2_STREAM_WINDOW = value;
227
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
228
+ (0, vitest_1.expect)(settings.h2StreamWindowSize).toBe(16 * 1024 * 1024);
229
+ });
230
+ });
231
+ (0, vitest_1.describe)('h2ConnectionWindowSize', () => {
232
+ (0, vitest_1.it)('defaults to 32 MiB', async () => {
233
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
234
+ (0, vitest_1.expect)(settings.h2ConnectionWindowSize).toBe(32 * 1024 * 1024);
235
+ });
236
+ (0, vitest_1.it)('parses a positive env var', async () => {
237
+ process.env.BL_H2_CONNECTION_WINDOW = String(8 * 1024 * 1024);
238
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
239
+ (0, vitest_1.expect)(settings.h2ConnectionWindowSize).toBe(8 * 1024 * 1024);
240
+ });
241
+ (0, vitest_1.it)('config value wins over the env var', async () => {
242
+ process.env.BL_H2_CONNECTION_WINDOW = '111';
243
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
244
+ settings.config.h2ConnectionWindowSize = 222;
245
+ (0, vitest_1.expect)(settings.h2ConnectionWindowSize).toBe(222);
246
+ });
247
+ vitest_1.it.each(['0', '-100', 'foo'])('ignores non-positive/unparseable env value %s and uses the default', async (value) => {
248
+ process.env.BL_H2_CONNECTION_WINDOW = value;
249
+ const { settings } = await Promise.resolve().then(() => __importStar(require('./settings.js')));
250
+ (0, vitest_1.expect)(settings.h2ConnectionWindowSize).toBe(32 * 1024 * 1024);
251
+ });
252
+ });
253
+ });
@@ -19,6 +19,7 @@ __exportStar(require("./agents/index.js"), exports);
19
19
  __exportStar(require("./client/client.js"), exports);
20
20
  __exportStar(require("./common/autoload.js"), exports);
21
21
  __exportStar(require("./common/env.js"), exports);
22
+ __exportStar(require("./common/h2-runtime.js"), exports);
22
23
  __exportStar(require("./common/node.js"), exports);
23
24
  __exportStar(require("./common/errors.js"), exports);
24
25
  __exportStar(require("./common/internal.js"), exports);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * HTTP/2 runtime-capability detection.
3
+ *
4
+ * The SDK's pooled HTTP/2 transport relies on the runtime's HTTP/2 client
5
+ * managing connection-level flow control correctly. One runtime does not:
6
+ *
7
+ * Bun < 1.3.11 never sends a connection-level WINDOW_UPDATE frame. The pooled
8
+ * H2 session therefore freezes after exactly 65535 cumulative body bytes (the
9
+ * default connection receive window that Bun never grows), and every request
10
+ * on that session hangs until the edge resets the streams (~330s).
11
+ * Fixed in Bun 1.3.11: https://bun.com/blog/bun-v1.3.11
12
+ *
13
+ * This module centralizes the version gate so the settings layer, the unit
14
+ * tests, and the cross-runtime environment tests all agree on ONE definition of
15
+ * "is this a Bun that breaks pooled H2". Keeping a single source of truth is the
16
+ * whole point: the bug this guards against is subtle and the parse is easy to
17
+ * get subtly wrong (see `stripPrerelease`).
18
+ */
19
+ /** First Bun release with a working connection-level WINDOW_UPDATE. */
20
+ export declare const BUN_H2_FIXED_VERSION: "1.3.11";
21
+ /**
22
+ * The default HTTP/2 connection-level receive window, in bytes (2^16 - 1). This
23
+ * is the exact number of cumulative body bytes after which a broken-Bun pooled
24
+ * session freezes: the window opens to 65535 and, absent a WINDOW_UPDATE, never
25
+ * reopens. Node grows it (see `establishH2`'s `setLocalWindowSize`); Bun < 1.3.11
26
+ * does not.
27
+ */
28
+ export declare const H2_DEFAULT_CONNECTION_WINDOW_BYTES: 65535;
29
+ /**
30
+ * Parse a `major.minor.patch` version, tolerating a pre-release/build suffix
31
+ * (e.g. `1.3.11-canary.1`, `1.4.0+build`). Returns `null` when the input is
32
+ * absent or not a recognizable semver triple.
33
+ */
34
+ export declare function parseSemver(version: string | undefined | null): [number, number, number] | null;
35
+ /**
36
+ * Given a Bun version string, is this a Bun whose HTTP/2 client breaks the
37
+ * pooled transport (Bun < 1.3.11)? A falsy/undefined version means "not Bun",
38
+ * which returns `false`. An unparseable version is treated as broken (`true`):
39
+ * we cannot prove it is safe, and forcing H2 off is the safe default.
40
+ */
41
+ export declare function isBrokenBunVersion(version: string | undefined | null): boolean;
42
+ /**
43
+ * The running runtime's Bun version, or `undefined` when not on Bun (Node,
44
+ * Deno, browsers, workers). Deno also exposes `process.versions` but never sets
45
+ * `.bun`, so this stays `undefined` there.
46
+ */
47
+ export declare function detectBunVersion(): string | undefined;
48
+ /**
49
+ * Is the CURRENT runtime a Bun that breaks the pooled H2 transport? This is the
50
+ * predicate the settings layer uses to force H2 off.
51
+ */
52
+ export declare function isBrokenBunH2Runtime(): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,7 @@ export * from "./agents/index.js";
3
3
  export * from "./client/client.js";
4
4
  export * from "./common/autoload.js";
5
5
  export * from "./common/env.js";
6
+ export * from "./common/h2-runtime.js";
6
7
  export * from "./common/node.js";
7
8
  export * from "./common/errors.js";
8
9
  export * from "./common/internal.js";