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