Files
galaxy-game/ui/docs/wasm-toolchain.md
T
Ilia Denisov b729036778
Tests · UI / test (push) Successful in 3m48s
Tests · UI / test (pull_request) Successful in 2m35s
build(ui): build core.wasm in CI, stop committing the binary (F6)
core.wasm and wasm_exec.js are no longer tracked (untracked + gitignored).
A reusable composite action .gitea/actions/build-wasm installs TinyGo
(actions/cache'd) and runs `make -C ui wasm`; it runs in all three
frontend-building workflows — ui-test (before Playwright; Vitest uses the
fake Core and needs no build), dev-deploy, and prod-build. ui-test gains a
Go setup (TinyGo shells out to Go); the deploy workflows already had one.

Docs: ui/docs/wasm-toolchain.md, ui/README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:29:33 +02:00

4.7 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/.

Real Ed25519 signing happens outside WASM in a platform-specific keystore — see storage.md for the web implementation (WebCrypto non-extractable keys + IndexedDB).

Why TinyGo

Two viable Go-to-WASM toolchains exist:

Toolchain Bundle size Notes
TinyGo ~903 KB (under 1 MB target) 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), so core.wasm / wasm_exec.js are not committed. CI builds them from source: the .gitea/actions/build-wasm composite action installs TinyGo (cached) and runs make -C ui wasm ahead of every step that builds or serves the frontend bundle — the ui-test Playwright run, dev-deploy, and prod-build.

For local work, run make -C ui wasm once after cloning, and again whenever ui/core/ or ui/wasm/main.go changes. Vitest needs no build (it uses the fake Core in tests/fake-core.ts); only the SvelteKit dev server and Playwright serve the real artefact from frontend/static/.

Bundle size

Build Date Size
Initial land 2026-05-07 903 KB

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