Files
galaxy-game/ui/docs/wasm-toolchain.md
T
Ilia Denisov fbc0260720 phase 5: wasm core, GalaxyClient skeleton, Connect-Web stubs
Compile `ui/core` to WebAssembly via TinyGo (903 KB) and expose four
canonical-bytes / signature-verification functions on
`globalThis.galaxyCore` from `ui/wasm/main.go`. The TypeScript-side
`Core` interface plus a `WasmCore` adapter (browser + JSDOM loader)
bridge those into a typed shape, and a `GalaxyClient` skeleton wires
`Core.signRequest` → injected `Signer` → typed Connect client →
`Core.verifyPayloadHash` / `verifyResponse`.

Wire `ui/buf.gen.yaml` against the local
`@bufbuild/protoc-gen-es` v2 binary (devDependency) so the codegen
step does not depend on the buf.build BSR. Vitest covers the bridge
end-to-end: per-method WasmCore tests under JSDOM, byte-for-byte
canon parity against the gateway fixtures committed in Phase 3, and
a `GalaxyClient` orchestration test using
`createRouterTransport`. The committed `core.wasm` snapshot tracks
TinyGo output so contributors run `make wasm` only when `ui/core/`
changes; CI consumes the snapshot directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:58:37 +02:00

4.2 KiB

WASM Toolchain

The Galaxy UI client compiles the Go module ui/core (canonical bytes, signature verification, keypair helpers) to WebAssembly via TinyGo. The compiled artefact core.wasm and its companion runtime shim wasm_exec.js ship under ui/frontend/static/.

Why TinyGo

Two viable Go-to-WASM toolchains exist:

Toolchain Bundle size (Phase 5) Notes
TinyGo ~903 KB (under 1 MB acceptance bar) LLVM-based, no full GC, fast cold-start
Standard Go ~2 MB (GOOS=js GOARCH=wasm) Drops in without extra tooling

ui/core was written under the TinyGo invariants documented in ui/core/README.md (no crypto/x509, no encoding/pem, no goroutines or sync primitives in production files), so the TinyGo build is a drop-in compile.

The standard-Go fallback stays available in case TinyGo lags behind a future Go release we depend on. To switch the build, swap the tinygo build invocation in ui/Makefile for GOOS=js GOARCH=wasm go build -o frontend/static/core.wasm ./wasm, and copy the matching shim from $(go env GOROOT)/lib/wasm/wasm_exec.js.

Prerequisites

  • TinyGo ≥ 0.41 (brew install tinygo).
  • Go 1.26+ (TinyGo's host compiler).
  • buf 1.67+ on PATH for TS protobuf generation (brew install bufbuild/buf/buf).
  • pnpm + Node 22+ for the JS runtime.

Build commands

make -C ui wasm        # produces frontend/static/{core.wasm,wasm_exec.js}
make -C ui ts-protos   # regenerates frontend/src/proto/* from gateway/proto

make wasm runs tinygo build -target=wasm and copies the matching TinyGo shim into the static asset directory. The shim must be the TinyGo one — the standard Go shim is ABI-incompatible. The Makefile resolves the shim path via tinygo env TINYGOROOT.

Loading recipes

Browser

SvelteKit serves static/ at the application root, so the WASM adapter at ui/frontend/src/platform/core/wasm.ts does the following on first call:

  1. Inject <script src="/wasm_exec.js"> if globalThis.Go is undefined.
  2. fetch('/core.wasm') and instantiate via WebAssembly.instantiate.
  3. Spawn the Go runtime via go.run(instance).
  4. Read globalThis.galaxyCore and wrap it as the typed Core interface.

The module is cached for the life of the page; subsequent loadCore() calls reuse the same Core instance.

Vitest under JSDOM

JSDOM does not implement fetch against file:// URLs, so the test loader at ui/frontend/tests/setup-wasm.ts reads both files from disk and evaluates wasm_exec.js via Node's vm.runInThisContext. The result is the same Core shape; the JS-side bridging is identical.

Field-name convention

The TS Core interface uses camelCase keys (matching the rest of the SvelteKit / TS-side conventions). The Go bridge in ui/wasm/main.go reads the same camelCase keys (protocolVersion, deviceSessionId, etc.) directly from the JS objects via syscall/js. There is no snake-case translation layer.

Byte marshalling

The bridge avoids js.CopyBytesToGo and instead does an explicit per-element copy via value.Index(i).Int(). TinyGo 0.41's js.CopyBytesToGo panics on Uint8Array instances that originate from Node's Buffer (which surfaces in Vitest's JSDOM environment when fixtures are read with crypto.createHash). The per-element loop is slower in theory but irrelevant in practice: the canonical envelope payloads stay below a few hundred bytes.

Reproducibility

TinyGo builds are not bit-for-bit deterministic (the binary embeds build-machine identifiers). Treat the committed core.wasm as a snapshot rebuilt by make wasm whenever ui/core/ or ui/wasm/main.go changes. CI rebuilds the artefact from source for its own asserts; the committed copy keeps Vitest from depending on TinyGo being installed in every environment.

Bundle size

Build Date Size
Phase 5 land 2026-05-07 903 KB

If the artefact ever crosses the 1 MB target, profile via tinygo build -size full and trim before committing.