Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ lerna-debug.log*
node_modules
target

# Gradle (Android workspace at repo root)
/.gradle/
/build/
/android/*/build/
local.properties

# Environment / secrets (never commit real env files; keep example templates)
.env
.env.*
Expand Down Expand Up @@ -39,3 +45,16 @@ playground/public/static.files

# Auto-generated by truapi-codegen (typecheck fixtures for rustdoc ts blocks)
playground/test/generated/

# Auto-generated FFI / WASM binding outputs
android/truapi-host/src/main/kotlin/generated/
ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
ios/truapi-host/Sources/truapi_serverFFI/
rust/crates/truapi-server/pkg/
js/packages/truapi/src/generated/
js/packages/truapi/dist/generated/
js/packages/truapi-host/src/generated/
js/packages/truapi-host/dist/generated/
js/packages/truapi-host-wasm/src/generated/
js/packages/truapi-host-wasm/dist/generated/
js/packages/truapi-host-wasm/dist/wasm/
5 changes: 4 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"endOfLine": "lf",
"overrides": [
{
"files": "js/packages/truapi/src/**/*.test.ts",
"files": [
"js/packages/truapi/src/**/*.test.ts",
"js/packages/truapi-host-wasm/src/**/*.test.ts"
],
"options": {
"tabWidth": 4,
"printWidth": 100
Expand Down
11 changes: 11 additions & 0 deletions js/packages/truapi-host-wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
*.tsbuildinfo
# Ignore compiled TS output (top-level + the web/ and electron/ entry subdirs)
# Generated WASM artifacts under dist/wasm/ are ignored by the repo root.
dist/**/*.js
dist/**/*.d.ts
dist/**/*.js.map
dist/**/*.d.ts.map
dist/generated/
# Codegen output from truapi-codegen --platform-ts-output.
src/generated/
64 changes: 64 additions & 0 deletions js/packages/truapi-host-wasm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# @parity/truapi-host-wasm

WASM-backed TrUAPI host runtime. It embeds the `truapi-server` Rust core (compiled to WASM)
behind a Web Worker provider, plus per-environment integration entry points. It is the
counterpart to the native Android/iOS host shells.

## Entry points

The package exposes tree-shakeable subpath exports — import only what your environment needs:

| Import | Provides |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `@parity/truapi-host-wasm` | Shared runtime types plus generated typed host callback contracts. |
| `@parity/truapi-host-wasm/web` | Browser host: `createIframeHost` (iframe MessageChannel handshake) and `createWebWorkerProvider`. |
| `@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. |
| `@parity/truapi-host-wasm/wasm/web` | The raw browser `wasm-bindgen` glue, if you need to instantiate the core yourself. |

## Generated WASM artefacts

The ignored bundle under `dist/wasm/web/` is built with host-owned chain access.
Hosts wire their JSON-RPC provider through `chainConnect`; if they omit it,
chain calls fail with the core's standard unavailable error. The bundled WASM is
about 1 MB (release build with `wasm-opt`).

Build them after editing `rust/crates/truapi-server` and before packaging, publishing, or running
tests that load the raw WASM bundle (requires `wasm-pack` on PATH):

```bash
npm run build:wasm # or `make wasm` from the repo root
```

## Example — browser (Web Worker)

```ts
import HostWorker from "@parity/truapi-host-wasm/worker-runtime?worker";
import { createWebWorkerProvider } from "@parity/truapi-host-wasm/web";

const provider = await createWebWorkerProvider(new HostWorker(), callbacks, {
runtimeConfig,
});
```

`@parity/truapi-host-wasm/web` also exports `createIframeHost` for the protocol-iframe
MessageChannel handshake.

## Publishing

The npm publish workflow is not wired yet. A release-process discussion is needed before adding a
publish job to `.github/workflows/`. Until then, consumers depend on the package via the workspace
`file:` link or by publishing locally with `npm pack`.

## Architecture

```text
JS host code
protocol handlers / typed callbacks
(types from @parity/truapi-host-wasm)
|
v
createWebWorkerProvider
|
v
truapi-server WASM core
```
49 changes: 49 additions & 0 deletions js/packages/truapi-host-wasm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@parity/truapi-host-wasm",
"version": "0.1.0",
"description": "WASM-backed TrUAPI host runtime: embeds the Rust core, with web iframe and Web Worker entry points",
"license": "MIT",
"author": "Parity Technologies <admin@parity.io>",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"sideEffects": [
"./dist/worker-runtime.js",
"./dist/wasm/**"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./web": {
"types": "./dist/web/index.d.ts",
"import": "./dist/web/index.js"
},
"./worker-runtime": {
"types": "./dist/worker-runtime.d.ts",
"import": "./dist/worker-runtime.js"
},
"./wasm/web": {
"types": "./dist/wasm/web/truapi_server.d.ts",
"import": "./dist/wasm/web/truapi_server.js"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc -b",
"build:wasm": "node scripts/build-wasm.mjs",
"test": "bun test"
},
"dependencies": {
"@parity/truapi": "file:../truapi"
},
"devDependencies": {
"@types/bun": "^1.3.0",
"neverthrow": "^8.2.0",
"typescript": "^5.7"
}
}
63 changes: 63 additions & 0 deletions js/packages/truapi-host-wasm/scripts/build-wasm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env node
// Rebuild the browser truapi-server WASM artefacts generated under
// `dist/wasm/web/`. wasm-pack is required.

import { execFile } from "node:child_process";
import { rm } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgRoot = resolve(__dirname, "..");
const repoRoot = resolve(pkgRoot, "../../..");
const rustCrate = resolve(repoRoot, "rust/crates/truapi-server");
const wasmProfile = process.env.TRUAPI_WASM_PROFILE ?? "release";

function args(target, outDir) {
const command = [
"build",
"--target",
target,
"--out-dir",
outDir,
"--out-name",
"truapi_server",
];
if (wasmProfile === "dev") {
command.push("--dev");
} else if (wasmProfile === "profiling") {
command.push("--profiling");
} else if (wasmProfile !== "release") {
throw new Error(
`Unsupported TRUAPI_WASM_PROFILE=${wasmProfile}; expected release, dev, or profiling`,
);
}
command.push(rustCrate, "--no-default-features");
return command;
}

async function build(target, subdir) {
const outDir = resolve(pkgRoot, "dist/wasm", subdir);
process.stdout.write(
`wasm-pack build --target ${target} --${wasmProfile} → ${outDir}\n`,
);
try {
await execFileAsync("wasm-pack", args(target, outDir), { cwd: repoRoot });
} catch (err) {
if (err?.code === "ENOENT") {
console.error(
"wasm-pack is required. Install it with `cargo install wasm-pack` " +
"or see https://rustwasm.github.io/wasm-pack/installer/",
);
process.exit(1);
}
throw err;
}
// wasm-pack writes a nested `.gitignore: *`; the repo-level ignore already
// owns generated WASM outputs.
await rm(resolve(outDir, ".gitignore"), { force: true });
}

await build("web", "web");
125 changes: 125 additions & 0 deletions js/packages/truapi-host-wasm/src/adapter-support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Hand-written runtime support for the generated `createWasmRawCallbacks`
// adapter (`./generated/host-callbacks-adapter.ts`). The adapter is mechanical
// (decode params, call the typed host callback, read the result); the pieces
// here are the genuinely bespoke runtime plumbing it leans on: stream driving
// and the chain-connection handle.

import { type GenericError, type Result } from "@parity/truapi";
import { hexToBytes } from "@parity/truapi/scale";

import type { ChainConnect, ChainConnection } from "./runtime.js";
import type { HostCallbacks } from "./generated/host-callbacks.js";

type WireResult<T, E> =
| { success: true; value: T }
| { success: false; value: E };

type StreamResult<T, E> = Result<T, E> | WireResult<T, E>;

type MaybeAsyncIterable<T> = AsyncIterable<T> | Iterable<T>;

/**
* Normalize both generated `Result<T, GenericError>` values and the plain
* `{ success, value }` envelope used by some JS fixtures into a raw item.
*/
function unwrapStreamResult<T>(item: StreamResult<T, GenericError>): T {
if ("success" in item) {
if (item.success === false) {
throw new Error(item.value.reason);
}
return item.value;
}
if (item.isErr()) {
throw new Error(item.error.reason);
}
return item.value;
}

/**
* Accept sync and async host streams behind one async-iterator interface.
* Host callbacks often use async iterables in production, while tests can use
* small synchronous fixtures without a custom wrapper.
*/
function toAsyncIterator<T>(stream: MaybeAsyncIterable<T>): AsyncIterator<T> {
const asyncIterable = stream as AsyncIterable<T>;
if (typeof asyncIterable[Symbol.asyncIterator] === "function") {
return asyncIterable[Symbol.asyncIterator]();
}
const iterator = (stream as Iterable<T>)[Symbol.iterator]();
const asyncIterator: AsyncIterator<T> = {
next: async () => iterator.next(),
};
if (iterator.return) {
asyncIterator.return = async () => iterator.return!();
}
return asyncIterator;
}

/**
* Drain an async iterator into a sink until disposed. This is used for
* callback streams where the Rust core owns cancellation but JS owns the
* iterator and any transport cleanup behind `return()`.
*/
function pumpIterator<T>(
iterator: AsyncIterator<T>,
onItem: (value: T) => void,
label: string,
): () => void {
let stopped = false;
void (async () => {
try {
while (!stopped) {
const next = await iterator.next();
if (next.done) return;
onItem(next.value);
}
} catch (err) {
console.error(`[truapi host callbacks] ${label} failed:`, err);
}
})();
return () => {
stopped = true;
void iterator.return?.();
};
}

/**
* Drive a typed host stream of `Result` items into the core's `sendItem`
* sink, unwrapping each `Result` (or throwing on its error). Returns a
* disposer that stops iteration.
*/
export function driveResultStream<T>(
stream: MaybeAsyncIterable<StreamResult<T, GenericError>>,
sendItem: (value: T) => void,
): () => void {
return pumpIterator(
toAsyncIterator(stream),
(value) => sendItem(unwrapStreamResult(value)),
"subscription",
);
}

/**
* Bridge the typed `ChainProvider.connect` callback onto the raw
* `chainConnect` the WASM core invokes: decode the genesis hash, pump the
* connection's `responses()` stream into `onResponse`, and expose
* `send`/`close`.
*/
export function chainConnectAdapter(
host: Pick<HostCallbacks, "connect">,
): ChainConnect {
return async (genesisHash, onResponse): Promise<ChainConnection | null> => {
const connection = await host.connect(hexToBytes(genesisHash));
const iterator = connection.responses()[Symbol.asyncIterator]();
const stopResponses = pumpIterator(iterator, onResponse, "chain responses");
return {
send(request: string): void {
connection.send(request);
},
close(): void {
stopResponses();
connection.close();
},
};
};
}
6 changes: 6 additions & 0 deletions js/packages/truapi-host-wasm/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Coerce an unknown thrown value into a human-readable message string. */
export function errorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
return JSON.stringify(err) ?? String(err);
}
Loading
Loading