Skip to content

js: configurable wasm module/memory quotas, instantiateStreaming/compileStreaming - #341

Merged
tannevaled merged 1 commit into
mainfrom
wasm-streaming-and-quotas
Nov 3, 2026
Merged

tannevaled merged 1 commit into
mainfrom
wasm-streaming-and-quotas

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

Milestone 1 of the go-wasmcontainers WebGPU-readiness roadmap (requested
directly, not corpus-found): go-wasmcontainers asked whether go-webengine
could validate their WebGPU stack's own WebAssembly layer — their own
Go-compiled modules reach several hundred MiB, toward the wasm32 4 GiB
address-space ceiling, which this engine's own hardcoded 32 MiB module cap
and 16 MiB linear-memory cap (round 189) could never accommodate. Dedicated
Workers, SharedArrayBuffer/Atomics, OPFS, and WebGPU compute are each their
own planned, larger future milestone — not attempted here.

  • Engine.MaxWasmModuleBytes/MaxWasmMemoryBytes (new fields): the
    compile-input-size and linear-memory-growth caps are now configurable,
    following MaxJSHeapBytes's own "zero selects a sane default" convention
    exactly — zero keeps today's unchanged behaviour (32 MiB / 16 MiB), any
    higher value raises the ceiling toward real module sizes. A byte value is
    rounded UP to WebAssembly's own 64 KiB page size and clamped to the
    wasm32 ceiling (65536 pages = 4 GiB) — wazero's own
    WithMemoryLimitPages panics past that value, so the clamp is
    load-bearing, not cosmetic. There is deliberately no "disable" value for
    either cap: a module always has SOME finite bound, matching wazero's own
    API, which has no unlimited-size mode either.
  • WebAssembly.compileStreaming/instantiateStreaming: did not exist
    at all before this PR. Not genuine incremental streaming — this engine's
    own fetch() already fully buffers a response before resolving it (no
    .body ReadableStream exists anywhere), and wazero's own
    CompileModule([]byte) takes a complete byte slice regardless — but the
    JS-facing contract (Promise-based, accepting a Response or a promise of
    one, the overwhelmingly common real shape
    instantiateStreaming(fetch(url))) is satisfied exactly. A new small
    primitive, awaitSettled, reads goja's own Promise.State()/Result()
    directly rather than driving real job-queue continuations — correct
    because every promise this engine's own code produces (including
    Response.arrayBuffer()) is already synchronously settled by
    construction.
  • FIDELITY.md/README.md updated (new log entry, two Known Gaps entries
    updated to reflect what's now closed vs. what round 189 already
    disclosed and this PR doesn't touch).

Test plan

  • go test ./... — all green, no coverage-gate regression
  • TestMaxWasmModuleBytesResolution/TestMaxWasmMemoryPagesResolution:
    the byte-to-page rounding/clamping math directly, including past the
    wasm32 ceiling
  • TestWasmModuleSizeQuotaEnforcedEndToEnd: the quota actually
    consulted by the real compile() path, not just the resolver in
    isolation
  • TestInstantiateStreamingAndCompileStreaming: the real streaming
    round trip against a genuine fetch()-returned Response (a new
    /add.wasm case on the shared netServer() test fixture)
  • TestStreamingWasmBytesRejectsVariousShapes: every error shape the
    Response-argument resolution can hit — a rejected promise, a promise
    resolving to a non-object, a direct non-promise object with no
    arrayBuffer() method, and arrayBuffer() itself throwing or
    returning a rejected promise — each correctly rejecting with a
    TypeError, never panicking
  • go build ./... && go vet ./... clean; js package coverage net
    improved (removed a provably-dead defensive branch the coverage
    ratchet surfaced, closed the real gaps it also surfaced)

🤖 Generated with Claude Code

…ileStreaming

Requested directly, not corpus-found: go-wasmcontainers asked whether
go-webengine could validate their WebGPU stack's own WebAssembly
layer - their own Go-compiled modules reach several hundred MiB,
toward the wasm32 4 GiB address-space ceiling, which this engine's own
hardcoded 32 MiB module cap and 16 MiB linear-memory cap (round 189)
could never accommodate. First milestone of a longer roadmap (Dedicated
Workers, SharedArrayBuffer/Atomics, OPFS, and WebGPU compute are each
their own planned, larger future round).

MaxWasmModuleBytes/MaxWasmMemoryBytes (new Engine fields): the
compile-input-size and linear-memory-growth caps are now configurable,
following this engine's own established convention exactly
(MaxJSHeapBytes' own "zero selects a sane default" shape) - zero keeps
today's unchanged behaviour (32 MiB / 16 MiB), any higher value raises
the ceiling toward real module sizes. A byte value is rounded up to
WebAssembly's own 64 KiB page size and clamped to the wasm32 ceiling
(65536 pages = 4 GiB) - wazero's own WithMemoryLimitPages panics past
that value, so the clamp is load-bearing, not cosmetic. There is
deliberately no "disable" value for either cap: a module always has
SOME finite bound, matching wazero's own API, which has no
unlimited-size mode either.

WebAssembly.compileStreaming/instantiateStreaming: did not exist at
all before this change. Not genuine incremental streaming - this
engine's own fetch() already fully buffers a response before resolving
it (no .body ReadableStream exists anywhere), and wazero's own
CompileModule([]byte) takes a complete byte slice regardless - but the
JS-facing contract (Promise-based, accepting a Response or a promise of
one, the overwhelmingly common real shape
instantiateStreaming(fetch(url))) is satisfied exactly. Resolving the
Response argument reuses a new small primitive, awaitSettled, which
reads goja's own Promise.State()/Result() directly rather than driving
real job-queue continuations - correct because every promise this
engine's own code produces (including Response.arrayBuffer()) is
already synchronously settled by construction.

Tests cover the byte-to-page rounding/clamping math directly, the
quota actually being consulted end-to-end, the real streaming round
trip against a genuine fetch()-returned Response, and every error
shape the Response-argument resolution can hit (a rejected promise, a
promise resolving to a non-object, a direct non-promise object with no
arrayBuffer() method, and arrayBuffer() itself throwing or returning a
rejected promise) - each correctly rejecting with a TypeError, never
panicking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 99f90fc into main Nov 3, 2026
13 checks passed
@tannevaled
tannevaled deleted the wasm-streaming-and-quotas branch November 3, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant