sproutboat 0.51.1 → 0.52.0

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/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.52.0] - 2026-11-06
14
+
15
+ Built on `@sproutboat/toolchain` 0.24.1, `@sproutboat/runtime` 0.42.2 and `@sproutboat/wire` 0.22.2. The compiler changes, so every app is recompiled on its next build. Nothing an app can observe changes apart from the two fixes below, so there is no new compatibility date.
16
+
17
+ ### Changed
18
+
19
+ - **Standalone binaries and deploys now build with Porffor alpha 22** (was alpha 19). Binaries are about 5% smaller (hello 6.58 to 6.27 MB, stress 6.62 to 6.30 MB, linux-x64). Apps using jose, yaml, supabase-js and Neon (HTTP, and `Pool` over WebSocket) are checked route by route and give the same results as on alpha 19 and on Bun. Measured against alpha 19 on the same box: idle memory is 3 to 7% lower, requests per second are 5 to 15% lower, p50 latency is 3 to 8% higher, and p95 is up to about 30% (5 to 8 µs) higher on a request-heavy app. The module is composed the way sproutd's workers are, so no Node runtime module is linked, and it is served by the same uWebSockets server as before (WebSockets, streaming, graceful restart). LTO is on for optimized builds.
20
+ - **Fallback:** `SPROUTBOAT_PORFFOR_CHANNEL=alpha-19` builds with the previous alpha 19 compiler for this release only; it will be removed in the next one. The compile cache is keyed by channel, so switching back and forth does not reuse the wrong binary. The artifact manifest and `sproutboat versions view` now report the Porffor release a build actually used (`alpha-22 (a081ebd)`).
21
+ - **`build --standalone --with-restore` stays on alpha 19 for now.** Its offline `ltx-restore` command is built against alpha 19's native C API, which alpha 22 does not provide yet, so such a build uses the alpha 19 compiler. Setting `SPROUTBOAT_PORFFOR_CHANNEL=alpha-22` together with `--with-restore` is an error, not a link failure.
22
+
23
+ ### Fixed
24
+
25
+ - **A `Uint8Array` or `ArrayBuffer` response body is sent with its real length.** On the alpha 22 server the bridge read the length from the buffer's flags word, so such a response went out with `Content-Length: 2147483647`. Found while qualifying alpha 22. No release ever shipped it, since alpha 19 was the default until now.
26
+ - **Neon's `Pool` over WebSocket no longer hangs.** Alpha 22 linked its Node runtime's `process`, `setImmediate`, `clearImmediate` and `global` for any app that named them. pg-pool then queued on `process.nextTick`, and a standalone binary never drains that queue. These four names are now absent, as on alpha 19, so libraries no longer take their Node code paths.
27
+
13
28
  ## [0.51.1] - 2026-11-05
14
29
 
15
30
  Built on `@sproutboat/runtime` 0.42.2, `@sproutboat/toolchain` 0.23.1 and `@sproutboat/wire` 0.22.2. Nothing an app can observe changes except latency, the refused address ranges below and the `startTls()` write order, so there is no new compatibility date.
package/SURFACE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
4
4
  > toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
5
5
 
6
- **Package:** `sproutboat` 0.51.1
6
+ **Package:** `sproutboat` 0.52.0
7
7
 
8
8
  ## Commands
9
9
 
@@ -48,6 +48,8 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
48
48
  | `CXX` | C++ compiler for a host build's native-fetch link step (default `c++` if present, else the vendored Zig cross-compiler targeting this host). |
49
49
  | `AR` | Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's). |
50
50
  | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
51
+ | `SPROUTBOAT_PORFFOR_CHANNEL` | Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release). |
52
+ | `SPROUTBOAT_FLTO` | Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off. |
51
53
  | `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
52
54
  | `SPROUTBOAT_PORFFOR_DIR` | Contributor override for an existing Porffor source checkout. Required files are validated before use. |
53
55
  | `SPROUTBOAT_BUILD_UWS_FROM_SOURCE` | Set to 1 to explicitly allow the contributor-only Git and Make uWebSockets fallback. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "keywords": [
6
6
  "cli",
@@ -65,7 +65,7 @@
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.6.0",
67
67
  "@sproutboat/runtime": "^0.42.2",
68
- "@sproutboat/toolchain": "^0.23.1",
68
+ "@sproutboat/toolchain": "^0.24.1",
69
69
  "@sproutboat/wire": "^0.22.2"
70
70
  },
71
71
  "devDependencies": {
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.51.1",
82
- "@sproutboat/cli-darwin-x64": "0.51.1",
83
- "@sproutboat/cli-linux-arm64": "0.51.1",
84
- "@sproutboat/cli-linux-x64": "0.51.1"
81
+ "@sproutboat/cli-darwin-arm64": "0.52.0",
82
+ "@sproutboat/cli-darwin-x64": "0.52.0",
83
+ "@sproutboat/cli-linux-arm64": "0.52.0",
84
+ "@sproutboat/cli-linux-x64": "0.52.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
package/src/build.ts CHANGED
@@ -7,7 +7,7 @@ import { queueSettings, resourceRefs, type SproutboatConfig } from "./config";
7
7
  import { ensureSqliteObject } from "./sqlite";
8
8
  import { sqliteStamp } from "./sqlite";
9
9
  import { bearsslStamp, ensureBearssl } from "./bearssl";
10
- import { compileSprout, loadPrelude, type Transport } from "./compile";
10
+ import { compileSprout, loadPrelude, porfforChannel, type Transport } from "./compile";
11
11
  import { compileCached } from "./compile-cache";
12
12
  import type { BuildTarget } from "./targets";
13
13
  import {
@@ -20,6 +20,7 @@ import {
20
20
  } from "./manifest";
21
21
  import { ensureZig, esbuildVersion, porfforVersion, toolchainStamp } from "./toolchain";
22
22
  import { PORFFOR_ARCHIVE_SHA256, PORFFOR_COMMIT_FULL } from "./porffor-toolchain";
23
+ import { A21_STANDALONE_FINGERPRINT } from "@sproutboat/toolchain/a21";
23
24
  import { version as toolchainPackageVersion } from "@sproutboat/toolchain/package.json" with { type: "json" };
24
25
  import { version as cliVersion } from "../package.json" with { type: "json" };
25
26
  import { reachesSubtle, wrapNativeFetchHandler } from "./wrap";
@@ -336,6 +337,13 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
336
337
  toolchain: toolchainStamp(),
337
338
  toolchainPackageVersion,
338
339
  porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
340
+ // the alpha 22 channel is a different compiler, server and prelude shape (compile.ts porfforChannel)
341
+ channel: porfforChannel(input.withRestore),
342
+ // the alpha 22 channel's own inputs: the patched tree, the shim, the bridge, the build recipe, LTO
343
+ channelInputs:
344
+ porfforChannel(input.withRestore) === "alpha-22"
345
+ ? [A21_STANDALONE_FINGERPRINT, process.env.SPROUTBOAT_FLTO ?? ""]
346
+ : null,
339
347
  native: embedded ? [sqliteStamp(), bearsslStamp()] : subtle ? [bearsslStamp()] : [],
340
348
  integration: await integrationIdentity(),
341
349
  nativeRestore: input.withRestore ? NATIVE_RESTORE_IDENTITY : null,
@@ -427,7 +435,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
427
435
  target: host ? hostTarget() : target === "linux-arm64" ? target : DEPLOY_TARGET,
428
436
  runtime: RUNTIME,
429
437
  capabilityProfile: CAPABILITY_PROFILE,
430
- porfforVersion: porfforVersion(),
438
+ porfforVersion: porfforVersion(porfforChannel(input.withRestore)),
431
439
  esbuildVersion: esbuildVersion(),
432
440
  buildImage: toolchainStamp(),
433
441
  compatibilityDate: input.config.compatibility_date,
package/src/compile.ts CHANGED
@@ -20,10 +20,20 @@ import {
20
20
  ensureUWebSocketsHost,
21
21
  ensureUWebSocketsStandalone,
22
22
  resolveHostCompiler,
23
+ uwsCommitFull,
23
24
  UwsUnavailableError,
24
25
  } from "./toolchain";
25
26
  import { isLinuxTarget, LINUX_TARGETS, linuxTriple, type BuildTarget } from "./targets";
26
27
  import { ensurePorffor } from "./porffor-toolchain";
28
+ import {
29
+ buildStandaloneA21,
30
+ composeStandaloneA21,
31
+ ensurePorfforA21,
32
+ SHARED_PRELUDE_FILE,
33
+ STANDALONE_A21_COMPILE_FLAGS,
34
+ standaloneChannel,
35
+ type StandaloneChannel,
36
+ } from "@sproutboat/toolchain/a21";
27
37
  // The prelude + transports live in @sproutboat/runtime (next to wrap.ts, which
28
38
  // locates them by file URL). `preludePath`/`transportPath` read them from there
29
39
  // for a dev build; these text imports bake the same files into the `--compile`
@@ -56,6 +66,29 @@ export {
56
66
 
57
67
  const COMPILE_TIMEOUT_MS = Number(process.env.SPROUTBOAT_COMPILE_TIMEOUT_MS || 600_000);
58
68
 
69
+ /**
70
+ * The Porffor channel a native compile uses: `alpha-22` by default (the alpha 21 line's compiler with
71
+ * the toolchain's alpha 19 server shim over its bridge; sproutd-alpha22-standalone-port.md), or
72
+ * `alpha-19` (`SPROUTBOAT_PORFFOR_CHANNEL=alpha-19`: Porffor's native-fetch server, the fallback pin
73
+ * kept for one release). The toolchain's `standaloneChannel()` decides, so the CLI and the runtime's
74
+ * native tests agree on the default.
75
+ *
76
+ * `--with-restore` stays on alpha 19: its restore objects are built against alpha 19's native-fetch
77
+ * C API, which the alpha 22 bridge does not provide, so they do not link there. Unless the channel is
78
+ * set explicitly, such a build uses the alpha 19 compiler; `SPROUTBOAT_PORFFOR_CHANNEL=alpha-22` with
79
+ * `--with-restore` is an error rather than a link failure.
80
+ */
81
+ export type PorfforChannel = StandaloneChannel;
82
+ export function porfforChannel(withRestore = false): PorfforChannel {
83
+ const channel = standaloneChannel();
84
+ if (!withRestore || channel === "alpha-19") return channel;
85
+ if (process.env.SPROUTBOAT_PORFFOR_CHANNEL)
86
+ throw new Error(
87
+ "--with-restore is not available on Porffor alpha 22 yet: unset SPROUTBOAT_PORFFOR_CHANNEL or set it to alpha-19",
88
+ );
89
+ return "alpha-19";
90
+ }
91
+
59
92
  export type CompileInput = {
60
93
  sourcePath: string;
61
94
  /** Stable compiler input for dev rebuilds, independent of the candidate binary path. */
@@ -334,6 +367,22 @@ export async function compileSprout(input: CompileInput): Promise<void> {
334
367
  })
335
368
  : [];
336
369
 
370
+ if (porfforChannel(input.withRestore) === "alpha-22") {
371
+ await compileSproutA21(input, {
372
+ alpha19Root: porffor,
373
+ generatedPath,
374
+ launch: PACKAGED
375
+ ? [process.execPath, "__porffor"]
376
+ : [process.execPath, resolve(import.meta.dir, "main.ts"), "__porffor"],
377
+ path,
378
+ hostCompiler,
379
+ restoreObjects,
380
+ uwsVariant,
381
+ });
382
+ await chmod(input.outPath, 0o555);
383
+ return;
384
+ }
385
+
337
386
  const child = Bun.spawn(
338
387
  [...command, ...porfforArgs(generatedPath, input.outPath, input.target, input.optimize, hostCompiler?.usingZig)],
339
388
  {
@@ -369,3 +418,81 @@ export async function compileSprout(input: CompileInput): Promise<void> {
369
418
  }
370
419
  await chmod(input.outPath, 0o555);
371
420
  }
421
+
422
+ /**
423
+ * The alpha 22 channel's native compile (sproutd-alpha22-standalone-port.md): the generated module is
424
+ * compiled in sproutd's worker shape (the toolchain composes `shared-prelude.js` from it: alpha 19
425
+ * helper names, the alpha 19 web classes, the dispatch shim), the alpha 22 compiler renders the split
426
+ * C units, and the toolchain's build recipe compiles them with the native-fetch bridge and the alpha
427
+ * 19 uWebSockets shim rewritten over flat data, then links with this build's native objects (SQLite,
428
+ * BearSSL, the restore objects) and the uSockets this target already has. Same inputs, same outputs,
429
+ * same environment variables as the alpha 19 path; the only switch is `SPROUTBOAT_PORFFOR_CHANNEL`.
430
+ */
431
+ async function compileSproutA21(
432
+ input: CompileInput,
433
+ build: {
434
+ alpha19Root: string;
435
+ generatedPath: string;
436
+ launch: string[];
437
+ path: string;
438
+ hostCompiler?: { cc: string[]; cxx: string[]; usingZig: boolean };
439
+ restoreObjects: string[];
440
+ uwsVariant?: string;
441
+ },
442
+ ): Promise<void> {
443
+ const alpha22 = await ensurePorfforA21("alpha-22");
444
+ const dir = dirname(build.generatedPath);
445
+ const composed = composeStandaloneA21(await readFile(build.generatedPath, "utf8"), build.alpha19Root);
446
+ // beside sprout.generated.js (which stays the wrapped module, for emit-only, dev and the cache key)
447
+ await writeFile(resolve(dir, SHARED_PRELUDE_FILE), composed.sharedPrelude);
448
+ const entry = resolve(dir, "sprout.a22.js");
449
+ await writeFile(entry, composed.entry);
450
+ const unitsDir = resolve(dir, "units-a22");
451
+ await rm(unitsDir, { recursive: true, force: true }).catch(() => {});
452
+ // the compiler's own build cache (keyed by entry path) is not used: `c` only renders
453
+ const render = Bun.spawn(
454
+ [
455
+ ...build.launch,
456
+ resolve(alpha22, "runtime/index.js"),
457
+ "c",
458
+ entry,
459
+ "-o",
460
+ `${unitsDir}/`,
461
+ ...STANDALONE_A21_COMPILE_FLAGS,
462
+ ],
463
+ { cwd: dir, stdout: "pipe", stderr: "pipe", env: { ...process.env, PATH: build.path } },
464
+ );
465
+ const [code, stdout, stderr] = await Promise.all([
466
+ render.exited,
467
+ new Response(render.stdout).text(),
468
+ new Response(render.stderr).text(),
469
+ ]);
470
+ if (code !== 0) throw new Error(`Porffor (alpha 22) render failed: ${(stderr || stdout).trim() || `exit ${code}`}`);
471
+
472
+ const host = input.target === "host";
473
+ const triple = host ? undefined : linuxTriple(input.target);
474
+ const cc = host ? build.hostCompiler!.cc : [input.zigBin!, "cc", "-target", triple!];
475
+ const cxx = host ? build.hostCompiler!.cxx : [input.zigBin!, "c++", "-target", triple!];
476
+ const uwsDir = resolve(
477
+ homedir(),
478
+ ".cache/porffor/deps",
479
+ host ? `uWebSockets-${uwsCommitFull()}` : `uWebSockets-${uwsCommitFull()}-musl-${build.uwsVariant ?? "sa-x86_64"}`,
480
+ );
481
+ await buildStandaloneA21({
482
+ unitsDir,
483
+ alpha19Root: build.alpha19Root,
484
+ outPath: input.outPath,
485
+ cc,
486
+ cxx,
487
+ uwsDir,
488
+ extraLink: [...(input.extraLink ?? []), ...build.restoreObjects],
489
+ extraCflags: [...(input.extraCflags ?? []), ...(input.withRestore ? ["-Dmain=sb_app_main"] : [])],
490
+ optimize: input.optimize === "dev" && host ? 0 : 3,
491
+ // LTO on for every optimized build, zig's musl cross builds included (romeo, M4: +11% req/s on hello,
492
+ // +7% on stress, and 4% smaller); SPROUTBOAT_FLTO=0 turns it off for measurements
493
+ flto: process.env.SPROUTBOAT_FLTO ? process.env.SPROUTBOAT_FLTO === "1" : !(input.optimize === "dev" && host),
494
+ musl: !host,
495
+ strip: true,
496
+ });
497
+ if (!(await Bun.file(input.outPath).exists())) throw new Error("alpha 22 build produced no binary");
498
+ }
package/src/surface.ts CHANGED
@@ -237,6 +237,15 @@ export const ENV_VARS: readonly EnvVar[] = [
237
237
  purpose: "Archiver used to assemble uSockets.a for a host build (default `ar` if present, else Zig's).",
238
238
  },
239
239
  { name: "SPROUTBOAT_COMPILE_TIMEOUT_MS", purpose: "Porffor compile timeout in ms (default 600000)." },
240
+ {
241
+ name: "SPROUTBOAT_PORFFOR_CHANNEL",
242
+ purpose:
243
+ "Porffor channel for standalone builds: `alpha-22` (default) or `alpha-19` (the previous compiler, kept as a fallback for one release).",
244
+ },
245
+ {
246
+ name: "SPROUTBOAT_FLTO",
247
+ purpose: "Overrides link-time optimization on alpha 22 builds: `1` turns it on, `0` turns it off.",
248
+ },
240
249
  {
241
250
  name: "SPROUTBOAT_TOOLCHAIN_CACHE",
242
251
  purpose: "Managed Porffor and compiler cache root (default ~/.cache/sproutboat).",
package/src/toolchain.ts CHANGED
@@ -16,6 +16,12 @@ import { dirname, resolve } from "node:path";
16
16
  // @ts-expect-error Bun's file loader returns the embedded asset path in a compiled executable.
17
17
  import embeddedUwsArchive from "../vendor/uwebsockets-360c276d-musl.tar.xz" with { type: "file" };
18
18
  import { BEARSSL_VERSION } from "./bearssl";
19
+ import {
20
+ PORFFOR_A22_CHANNEL,
21
+ PORFFOR_A22_COMMIT,
22
+ standaloneChannel,
23
+ type StandaloneChannel,
24
+ } from "@sproutboat/toolchain/a21";
19
25
  import { cachedPorfforRoot, PORFFOR_CHANNEL, PORFFOR_COMMIT } from "./porffor-toolchain";
20
26
  import { SQLITE_VERSION } from "./sqlite";
21
27
  import { LINUX_TARGETS, type LinuxTarget } from "./targets";
@@ -443,7 +449,7 @@ export function inspectToolchain(): ToolchainDoctor {
443
449
  };
444
450
  }
445
451
 
446
- function uwsCommitFull(): string {
452
+ export function uwsCommitFull(): string {
447
453
  try {
448
454
  const src = readFileSync(resolve(porfforRoot(), "compiler/uwebsockets.js"), "utf8");
449
455
  return /UWS_COMMIT\s*=\s*['"]([0-9a-f]{40})/.exec(src)?.[1] ?? UWS_COMMIT_FULL;
@@ -672,8 +678,10 @@ export function porfforRoot(start = import.meta.dir): string {
672
678
  }
673
679
  }
674
680
 
675
- export function porfforVersion(): string {
681
+ /** The Porffor release a build compiles with, on the standalone channel it uses (default: the environment's). */
682
+ export function porfforVersion(channel: StandaloneChannel = standaloneChannel()): string {
676
683
  if (process.env.PORFFOR_VERSION) return process.env.PORFFOR_VERSION;
684
+ if (channel === "alpha-22") return `${PORFFOR_A22_CHANNEL} (${PORFFOR_A22_COMMIT})`;
677
685
  return `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
678
686
  }
679
687