Files
galaxy-game/ui/docs/wasm-toolchain.md
Ilia Denisov ecd2bc9348 phase 6: web storage layer (KeyStore, Cache, session)
KeyStore + Cache TS interfaces with WebCrypto non-extractable Ed25519
keys persisted via IndexedDB (idb), plus thin api/session.ts that
loads or creates the device session at app startup. Vitest unit
tests under fake-indexeddb cover both adapters; Playwright e2e
verifies the keypair survives reload and produces signatures still
verifiable under the persisted public key (gateway round-trip moves
to Phase 7's existing acceptance bullet).

Browser baseline: WebCrypto Ed25519 — Chrome >=137, Firefox >=130,
Safari >=17.4. No JS fallback; ui/docs/storage.md documents the
matrix and the WebKit non-determinism quirk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 14:08:09 +02:00

4.3 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 (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.