b729036778
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>
121 lines
4.7 KiB
Markdown
121 lines
4.7 KiB
Markdown
# 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`](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
|
|
|
|
```bash
|
|
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`](../../.gitea/actions/build-wasm/action.yml)
|
|
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.
|