Skip to content

Commit 23b8487

Browse files
committed
feat(host-wasm): add @parity/truapi-host-wasm runtime
New WASM-backed host runtime package embedding the Rust core, with web iframe and Web Worker entry points. Updates the @parity/truapi client (SCALE, sandbox, transport) and drops the obsolete explorer 0.3.2 codegen snapshot.
1 parent 691b822 commit 23b8487

31 files changed

Lines changed: 3613 additions & 4661 deletions

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ lerna-debug.log*
1111
node_modules
1212
target
1313

14+
# Gradle (Android workspace at repo root)
15+
/.gradle/
16+
/build/
17+
/android/*/build/
18+
local.properties
19+
1420
# Environment / secrets (never commit real env files; keep example templates)
1521
.env
1622
.env.*
@@ -39,3 +45,16 @@ playground/public/static.files
3945

4046
# Auto-generated by truapi-codegen (typecheck fixtures for rustdoc ts blocks)
4147
playground/test/generated/
48+
49+
# Auto-generated FFI / WASM binding outputs
50+
android/truapi-host/src/main/kotlin/generated/
51+
ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
52+
ios/truapi-host/Sources/truapi_serverFFI/
53+
rust/crates/truapi-server/pkg/
54+
js/packages/truapi/src/generated/
55+
js/packages/truapi/dist/generated/
56+
js/packages/truapi-host/src/generated/
57+
js/packages/truapi-host/dist/generated/
58+
js/packages/truapi-host-wasm/src/generated/
59+
js/packages/truapi-host-wasm/dist/generated/
60+
js/packages/truapi-host-wasm/dist/wasm/

.prettierrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"endOfLine": "lf",
1111
"overrides": [
1212
{
13-
"files": "js/packages/truapi/src/**/*.test.ts",
13+
"files": [
14+
"js/packages/truapi/src/**/*.test.ts",
15+
"js/packages/truapi-host-wasm/src/**/*.test.ts"
16+
],
1417
"options": {
1518
"tabWidth": 4,
1619
"printWidth": 100
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules/
2+
*.tsbuildinfo
3+
# Ignore compiled TS output (top-level + the web/ and electron/ entry subdirs)
4+
# Generated WASM artifacts under dist/wasm/ are ignored by the repo root.
5+
dist/**/*.js
6+
dist/**/*.d.ts
7+
dist/**/*.js.map
8+
dist/**/*.d.ts.map
9+
dist/generated/
10+
# Codegen output from truapi-codegen --platform-ts-output.
11+
src/generated/
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# @parity/truapi-host-wasm
2+
3+
WASM-backed TrUAPI host runtime. It embeds the `truapi-server` Rust core (compiled to WASM)
4+
behind a Web Worker provider, plus per-environment integration entry points. It is the
5+
counterpart to the native Android/iOS host shells.
6+
7+
## Entry points
8+
9+
The package exposes tree-shakeable subpath exports — import only what your environment needs:
10+
11+
| Import | Provides |
12+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
13+
| `@parity/truapi-host-wasm` | Shared runtime types plus generated typed host callback contracts. |
14+
| `@parity/truapi-host-wasm/web` | Browser host: `createIframeHost` (iframe MessageChannel handshake) and `createWebWorkerProvider`. |
15+
| `@parity/truapi-host-wasm/worker-runtime` | Web Worker entrypoint (import with your bundler's `?worker` suffix) so the WASM core runs off the page main thread. |
16+
| `@parity/truapi-host-wasm/wasm/web` | The raw browser `wasm-bindgen` glue, if you need to instantiate the core yourself. |
17+
18+
## Generated WASM artefacts
19+
20+
The ignored bundle under `dist/wasm/web/` is built with host-owned chain access.
21+
Hosts wire their JSON-RPC provider through `chainConnect`; if they omit it,
22+
chain calls fail with the core's standard unavailable error. The bundled WASM is
23+
about 1 MB (release build with `wasm-opt`).
24+
25+
Build them after editing `rust/crates/truapi-server` and before packaging, publishing, or running
26+
tests that load the raw WASM bundle (requires `wasm-pack` on PATH):
27+
28+
```bash
29+
npm run build:wasm # or `make wasm` from the repo root
30+
```
31+
32+
## Example — browser (Web Worker)
33+
34+
```ts
35+
import HostWorker from "@parity/truapi-host-wasm/worker-runtime?worker";
36+
import { createWebWorkerProvider } from "@parity/truapi-host-wasm/web";
37+
38+
const provider = await createWebWorkerProvider(new HostWorker(), callbacks, {
39+
runtimeConfig,
40+
});
41+
```
42+
43+
`@parity/truapi-host-wasm/web` also exports `createIframeHost` for the protocol-iframe
44+
MessageChannel handshake.
45+
46+
## Publishing
47+
48+
The npm publish workflow is not wired yet. A release-process discussion is needed before adding a
49+
publish job to `.github/workflows/`. Until then, consumers depend on the package via the workspace
50+
`file:` link or by publishing locally with `npm pack`.
51+
52+
## Architecture
53+
54+
```text
55+
JS host code
56+
protocol handlers / typed callbacks
57+
(types from @parity/truapi-host-wasm)
58+
|
59+
v
60+
createWebWorkerProvider
61+
|
62+
v
63+
truapi-server WASM core
64+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@parity/truapi-host-wasm",
3+
"version": "0.1.0",
4+
"description": "WASM-backed TrUAPI host runtime: embeds the Rust core, with web iframe and Web Worker entry points",
5+
"license": "MIT",
6+
"author": "Parity Technologies <admin@parity.io>",
7+
"type": "module",
8+
"main": "dist/index.js",
9+
"types": "dist/index.d.ts",
10+
"sideEffects": [
11+
"./dist/worker-runtime.js",
12+
"./dist/wasm/**"
13+
],
14+
"exports": {
15+
".": {
16+
"types": "./dist/index.d.ts",
17+
"import": "./dist/index.js"
18+
},
19+
"./web": {
20+
"types": "./dist/web/index.d.ts",
21+
"import": "./dist/web/index.js"
22+
},
23+
"./worker-runtime": {
24+
"types": "./dist/worker-runtime.d.ts",
25+
"import": "./dist/worker-runtime.js"
26+
},
27+
"./wasm/web": {
28+
"types": "./dist/wasm/web/truapi_server.d.ts",
29+
"import": "./dist/wasm/web/truapi_server.js"
30+
}
31+
},
32+
"files": [
33+
"dist",
34+
"README.md"
35+
],
36+
"scripts": {
37+
"build": "tsc -b",
38+
"build:wasm": "node scripts/build-wasm.mjs",
39+
"test": "bun test"
40+
},
41+
"dependencies": {
42+
"@parity/truapi": "file:../truapi"
43+
},
44+
"devDependencies": {
45+
"@types/bun": "^1.3.0",
46+
"neverthrow": "^8.2.0",
47+
"typescript": "^5.7"
48+
}
49+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env node
2+
// Rebuild the browser truapi-server WASM artefacts generated under
3+
// `dist/wasm/web/`. wasm-pack is required.
4+
5+
import { execFile } from "node:child_process";
6+
import { rm } from "node:fs/promises";
7+
import { dirname, resolve } from "node:path";
8+
import { fileURLToPath } from "node:url";
9+
import { promisify } from "node:util";
10+
11+
const execFileAsync = promisify(execFile);
12+
const __dirname = dirname(fileURLToPath(import.meta.url));
13+
const pkgRoot = resolve(__dirname, "..");
14+
const repoRoot = resolve(pkgRoot, "../../..");
15+
const rustCrate = resolve(repoRoot, "rust/crates/truapi-server");
16+
const wasmProfile = process.env.TRUAPI_WASM_PROFILE ?? "release";
17+
18+
function args(target, outDir) {
19+
const command = [
20+
"build",
21+
"--target",
22+
target,
23+
"--out-dir",
24+
outDir,
25+
"--out-name",
26+
"truapi_server",
27+
];
28+
if (wasmProfile === "dev") {
29+
command.push("--dev");
30+
} else if (wasmProfile === "profiling") {
31+
command.push("--profiling");
32+
} else if (wasmProfile !== "release") {
33+
throw new Error(
34+
`Unsupported TRUAPI_WASM_PROFILE=${wasmProfile}; expected release, dev, or profiling`,
35+
);
36+
}
37+
command.push(rustCrate, "--no-default-features");
38+
return command;
39+
}
40+
41+
async function build(target, subdir) {
42+
const outDir = resolve(pkgRoot, "dist/wasm", subdir);
43+
process.stdout.write(
44+
`wasm-pack build --target ${target} --${wasmProfile}${outDir}\n`,
45+
);
46+
try {
47+
await execFileAsync("wasm-pack", args(target, outDir), { cwd: repoRoot });
48+
} catch (err) {
49+
if (err?.code === "ENOENT") {
50+
console.error(
51+
"wasm-pack is required. Install it with `cargo install wasm-pack` " +
52+
"or see https://rustwasm.github.io/wasm-pack/installer/",
53+
);
54+
process.exit(1);
55+
}
56+
throw err;
57+
}
58+
// wasm-pack writes a nested `.gitignore: *`; the repo-level ignore already
59+
// owns generated WASM outputs.
60+
await rm(resolve(outDir, ".gitignore"), { force: true });
61+
}
62+
63+
await build("web", "web");
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Hand-written runtime support for the generated `createWasmRawCallbacks`
2+
// adapter (`./generated/host-callbacks-adapter.ts`). The adapter is mechanical
3+
// (decode params, call the typed host callback, read the result); the pieces
4+
// here are the genuinely bespoke runtime plumbing it leans on: stream driving
5+
// and the chain-connection handle.
6+
7+
import { type GenericError, type Result } from "@parity/truapi";
8+
import { hexToBytes } from "@parity/truapi/scale";
9+
10+
import type { ChainConnect, ChainConnection } from "./runtime.js";
11+
import type { HostCallbacks } from "./generated/host-callbacks.js";
12+
13+
type WireResult<T, E> =
14+
| { success: true; value: T }
15+
| { success: false; value: E };
16+
17+
type StreamResult<T, E> = Result<T, E> | WireResult<T, E>;
18+
19+
type MaybeAsyncIterable<T> = AsyncIterable<T> | Iterable<T>;
20+
21+
/**
22+
* Normalize both generated `Result<T, GenericError>` values and the plain
23+
* `{ success, value }` envelope used by some JS fixtures into a raw item.
24+
*/
25+
function unwrapStreamResult<T>(item: StreamResult<T, GenericError>): T {
26+
if ("success" in item) {
27+
if (item.success === false) {
28+
throw new Error(item.value.reason);
29+
}
30+
return item.value;
31+
}
32+
if (item.isErr()) {
33+
throw new Error(item.error.reason);
34+
}
35+
return item.value;
36+
}
37+
38+
/**
39+
* Accept sync and async host streams behind one async-iterator interface.
40+
* Host callbacks often use async iterables in production, while tests can use
41+
* small synchronous fixtures without a custom wrapper.
42+
*/
43+
function toAsyncIterator<T>(stream: MaybeAsyncIterable<T>): AsyncIterator<T> {
44+
const asyncIterable = stream as AsyncIterable<T>;
45+
if (typeof asyncIterable[Symbol.asyncIterator] === "function") {
46+
return asyncIterable[Symbol.asyncIterator]();
47+
}
48+
const iterator = (stream as Iterable<T>)[Symbol.iterator]();
49+
const asyncIterator: AsyncIterator<T> = {
50+
next: async () => iterator.next(),
51+
};
52+
if (iterator.return) {
53+
asyncIterator.return = async () => iterator.return!();
54+
}
55+
return asyncIterator;
56+
}
57+
58+
/**
59+
* Drain an async iterator into a sink until disposed. This is used for
60+
* callback streams where the Rust core owns cancellation but JS owns the
61+
* iterator and any transport cleanup behind `return()`.
62+
*/
63+
function pumpIterator<T>(
64+
iterator: AsyncIterator<T>,
65+
onItem: (value: T) => void,
66+
label: string,
67+
): () => void {
68+
let stopped = false;
69+
void (async () => {
70+
try {
71+
while (!stopped) {
72+
const next = await iterator.next();
73+
if (next.done) return;
74+
onItem(next.value);
75+
}
76+
} catch (err) {
77+
console.error(`[truapi host callbacks] ${label} failed:`, err);
78+
}
79+
})();
80+
return () => {
81+
stopped = true;
82+
void iterator.return?.();
83+
};
84+
}
85+
86+
/**
87+
* Drive a typed host stream of `Result` items into the core's `sendItem`
88+
* sink, unwrapping each `Result` (or throwing on its error). Returns a
89+
* disposer that stops iteration.
90+
*/
91+
export function driveResultStream<T>(
92+
stream: MaybeAsyncIterable<StreamResult<T, GenericError>>,
93+
sendItem: (value: T) => void,
94+
): () => void {
95+
return pumpIterator(
96+
toAsyncIterator(stream),
97+
(value) => sendItem(unwrapStreamResult(value)),
98+
"subscription",
99+
);
100+
}
101+
102+
/**
103+
* Bridge the typed `ChainProvider.connect` callback onto the raw
104+
* `chainConnect` the WASM core invokes: decode the genesis hash, pump the
105+
* connection's `responses()` stream into `onResponse`, and expose
106+
* `send`/`close`.
107+
*/
108+
export function chainConnectAdapter(
109+
host: Pick<HostCallbacks, "connect">,
110+
): ChainConnect {
111+
return async (genesisHash, onResponse): Promise<ChainConnection | null> => {
112+
const connection = await host.connect(hexToBytes(genesisHash));
113+
const iterator = connection.responses()[Symbol.asyncIterator]();
114+
const stopResponses = pumpIterator(iterator, onResponse, "chain responses");
115+
return {
116+
send(request: string): void {
117+
connection.send(request);
118+
},
119+
close(): void {
120+
stopResponses();
121+
connection.close();
122+
},
123+
};
124+
};
125+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** Coerce an unknown thrown value into a human-readable message string. */
2+
export function errorMessage(err: unknown): string {
3+
if (err instanceof Error) return err.message;
4+
if (typeof err === "string") return err;
5+
return JSON.stringify(err) ?? String(err);
6+
}

0 commit comments

Comments
 (0)