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>
This commit is contained in:
Ilia Denisov
2026-05-07 12:58:37 +02:00
parent cd61868881
commit fbc0260720
25 changed files with 7284 additions and 36 deletions
+51
View File
@@ -0,0 +1,51 @@
// Boots the TinyGo `core.wasm` module under Vitest/JSDOM by reading
// the artefact and the matching wasm_exec.js shim from disk, evaluating
// the shim into the test's global scope, then instantiating WebAssembly
// against `(new Go()).importObject`. Returns a `Core` ready to use in
// tests; subsequent calls return the cached instance so each Vitest
// file pays the boot cost once.
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { runInThisContext } from "node:vm";
import { adaptBridge, requireBridge } from "../src/platform/core/wasm";
import type { Core } from "../src/platform/core/index";
// Vitest is launched from ui/frontend, so the static artefacts produced
// by `make wasm` sit at <cwd>/static/. Resolving via process.cwd avoids
// the JSDOM-specific "URL must be of scheme file" error that
// import.meta.url triggers under the jsdom test environment.
const STATIC_DIR = resolve(process.cwd(), "static") + "/";
let cached: Promise<Core> | undefined;
export function loadWasmCoreForTest(): Promise<Core> {
if (!cached) {
cached = boot();
}
return cached;
}
async function boot(): Promise<Core> {
if (typeof globalThis.Go === "undefined") {
const shim = readFileSync(`${STATIC_DIR}wasm_exec.js`, "utf8");
runInThisContext(shim);
}
const Go = globalThis.Go;
if (!Go) {
throw new Error("setup-wasm: Go runtime missing after wasm_exec.js eval");
}
const go = new Go();
const wasm = readFileSync(`${STATIC_DIR}core.wasm`);
const { instance } = await WebAssembly.instantiate(
wasm,
go.importObject,
);
void go.run(instance);
// `go.run` is async but the Go side registers `globalThis.galaxyCore`
// and then blocks on `select {}`; the registration happens
// synchronously before the first await, so by the time the next
// microtask runs the bridge is already available.
await new Promise<void>((resolve) => setTimeout(resolve, 0));
return adaptBridge(requireBridge());
}