Documentation
WASM Languages
Kynetra FX guest handlers can be written in any language that compiles to WebAssembly. Three language systems from the Kynetra ecosystem target this capability: ClearScript (.NET/C#), NovaC (systems language), and KYRx (reactive/functional). Two toolchains bridge these languages to your TypeScript host: wasm-pack for ClearScript and NovaC, and jco with the WASM Component Model for KYRx.
Overview#
The Kynetra FX WASM system is built around a simple guest contract: the guest module exports a handle function that receives a serialised request and returns a serialised response. The host (your TypeScript app) passes requests to the guest and routes the returned response back to the caller. The contract is shallow enough to implement in any language — the only requirement is that the module can be loaded by the @kynetra/fx-wasm adapter.
- ClearScript — .NET/C# scripting runtime compiled to WASM via
wasm-pack. Uses Rust-stylewasm-bindgenbindings with JSON serialisation for the request/response envelope. - NovaC — Kynetra's systems language. Strongly typed, zero-overhead, also compiled via
wasm-packand Rust-style bindgen. Suited to performance-critical handlers where allocation cost matters. - KYRx — Kynetra's reactive/functional language. Compiles to the WASM Component Model format. Uses
jco transpileto generate JS bindings from thekynetra:fx/guestWIT world.
The guest contract#
Before choosing a toolchain, understand what the host expects. The WASM handlers page covers the full contract; the relevant surface here is the handle export.
For wasm-pack guests (ClearScript, NovaC), the export is a plain function that takes and returns a JSON string:
// The contract the host callshandle(requestJson: string): stringFor Component Model guests (KYRx), the contract is expressed as a WIT interface. The jco toolchain generates JS bindings from the compiled component that satisfy this interface automatically.
Note
wasmHandler() function from @kynetra/fx-wasm accepts either a plain object { handle } or an async factory function that returns one. Both wasm-pack and jco guests satisfy this shape.WIT interface definition#
The canonical interface for KYRx (and any Component Model guest) is defined in the kynetra:fx package. This WIT file is the source of truth for the request and response record shapes.
package kynetra:fx@0.1.0; interface guest { record wasm-request { method: string, url: string, path: string, headers: list<tuple<string, string>>, body: option<string>, } record wasm-response { status: option<u16>, headers: list<tuple<string, string>>, body: option<string>, } handle: func(req: wasm-request) -> wasm-response;} world fx-guest { export guest;}The wasm-request record captures the essential fields of an HTTP request. Headers are a flat list of name/value tuples — the same shape used by the Fetch API. The response status is optional; the host defaults to 200 when it is absent.
wasm-pack toolchain — ClearScript and NovaC#
Both ClearScript and NovaC use the wasm-pack toolchain. The build output is a pkg/ directory containing a .wasm binary and a JS glue module generated by wasm-bindgen. The glue module exports init (the async initialiser) and any functions annotated with #[wasm_bindgen].
ClearScript guest implementation
ClearScript exposes its scripting surface via a Rust shim that delegates to the managed runtime. The handle export receives the request as a JSON string, deserialises it, runs the handler logic, and returns a JSON-serialised response.
use wasm_bindgen::prelude::*;use serde::{Deserialize, Serialize}; #[derive(Deserialize)]struct WasmRequest { method: String, url: String, path: String, body: Option<String>,} #[derive(Serialize)]struct WasmResponse { status: u16, body: String,} #[wasm_bindgen]pub fn handle(request_json: &str) -> String { let req: WasmRequest = serde_json::from_str(request_json).unwrap(); let resp = WasmResponse { status: 200, body: format!(r#"{{"path":"{}","ok":true}}"#, req.path), }; serde_json::to_string(&resp).unwrap()}Build the guest with wasm-pack build --target web. The resulting pkg/ directory can be committed or published as a package and imported directly by the TypeScript host.
Integrating a wasm-pack guest in TypeScript
After building, import the generated glue module, call init() once to instantiate the WASM module, then pass the handle export to wasmHandler().
import init, { handle } from './pkg/clearscript_guest'import { wasmHandler } from '@kynetra/fx-wasm'import { createFX } from '@kynetra/fx' await init() const app = createFX()app.get('/script/*', wasmHandler({ handle })) return appTip
await init() at module load time (top-level await is supported in Cloudflare Workers and modern runtimes). The WASM module is compiled once and reused for every request — there is no per-request overhead from instantiation.NovaC guest
NovaC guests use the same wasm-pack pipeline. Because NovaC is strongly typed and zero-overhead, it is the preferred choice when the guest needs to perform CPU-intensive work — data transforms, parsing, or in-module caching — without the overhead of the managed ClearScript runtime. The bindgen annotations and JSON serialisation pattern are identical to the ClearScript example above.
use wasm_bindgen::prelude::*;use serde::{Deserialize, Serialize}; #[derive(Deserialize)]struct WasmRequest { method: String, path: String, body: Option<String>,} #[derive(Serialize)]struct WasmResponse { status: u16, headers: Vec<(String, String)>, body: String,} #[wasm_bindgen]pub fn handle(request_json: &str) -> String { let req: WasmRequest = serde_json::from_str(request_json).unwrap_or_else(|_| { WasmRequest { method: String::new(), path: String::new(), body: None } }); let resp = WasmResponse { status: 200, headers: vec![ ("content-type".into(), "application/json".into()), ("x-novac-runtime".into(), "1".into()), ], body: format!(r#"{{"method":"{}","path":"{}"}}"#, req.method, req.path), }; serde_json::to_string(&resp).unwrap()}Component Model / jco toolchain — KYRx#
KYRx targets the WASM Component Model, a higher-level standard that encodes typed interfaces directly in the binary format. Instead of a JSON string envelope, the component exposes the kynetra:fx/guest WIT world with structured record types.
The jco toolchain (JavaScript Component Model toolchain from the Bytecode Alliance) transpiles the component into a standard ES module with generated TypeScript bindings. The generated handle export accepts and returns plain JavaScript objects matching the WIT record shapes — no manual JSON serialisation required.
Transpiling with jco
Install jco
npm install --save-dev @bytecodealliance/jcoCompile the KYRx source to a WASM component
.wasm file targeting the Component Model. Consult the KYRx language documentation for compiler invocation.kyrxc build --target wasm-component -o guest.wasm src/handler.kyrxTranspile the component to JS bindings
jco transpile reads the component, inspects the WIT interface, and emits a JS module plus TypeScript declaration file.npx jco transpile guest.wasm -o ./bindingsImport the generated bindings
bindings/guest.js module exports a handle function that satisfies the FX guest contract directly.Integrating a KYRx guest in TypeScript
// After: jco transpile guest.wasm -o ./bindingsimport { handle } from './bindings/guest.js'import { wasmHandler } from '@kynetra/fx-wasm'import { createFX } from '@kynetra/fx' const app = createFX() // jco-generated handle satisfies the guest contractapp.get('/kyrx/*', wasmHandler({ handle })) return appNote
init() call. The jco transpiler emits synchronous initialisation in the module itself. The handleexport is ready to use as soon as the module is imported.Build commands reference#
The following commands cover the full build pipeline for each language and toolchain combination.
wasm-pack build --target web— build a ClearScript or NovaC guest; outputspkg/with.wasm+ JS glue.wasm-pack build --target bundler— alternative target for bundler-based setups (webpack, Vite, esbuild). Use when your host is built with a bundler that understands WASM imports natively.kyrxc build --target wasm-component -o guest.wasm src/— compile KYRx source to a WASM Component binary.npx jco transpile guest.wasm -o ./bindings— transpile a Component to JS bindings. Add--mapflags to customise import paths.npx jco types guest.wasm -o ./bindings— generate TypeScript declarations only, without transpiling, for type-checking purposes.wasm-pack test --node— run unit tests for a wasm-pack guest in Node.js. Useful for testing the JSON round-trip without deploying.
Caching WASM modules#
In runtimes where the module-level await pattern is unavailable or where you need to lazy-load guests on first use, the wasmHandler() function accepts an async factory instead of a plain { handle } object. The host calls the factory on the first request and caches the result for subsequent calls.
import { wasmHandler } from '@kynetra/fx-wasm'import { createFX } from '@kynetra/fx' const app = createFX() // Source can be a factory — called once and cached by wasmHandlerlet cachedGuest: { handle: (s: string) => string } | null = null const guestFactory = async () => { if (!cachedGuest) { const { default: init, handle } = await import('./pkg/my_guest') await init() cachedGuest = { handle } } return cachedGuest} app.get('/cached', wasmHandler(guestFactory))This pattern is especially useful on Cloudflare Workers where dynamic imports are resolved at bundle time and the first request triggers module-level initialisation. The factory runs at most once per isolate lifetime — subsequent requests hit the cached cachedGuest reference with zero overhead.
Tip
await init() pattern when possible. V8 isolates are typically reused across requests, so the module initialises once per isolate, not once per request. The factory approach is most useful on runtimes that do not support top-level await, or when you need conditional loading based on request properties.Choosing between toolchains#
Both toolchains satisfy the FX guest contract. The choice comes down to your language preference, interface complexity, and build pipeline requirements.
- Use wasm-pack (ClearScript, NovaC) when you want a minimal build pipeline, JSON-based interop is sufficient, and you prefer a Rust/C-style workflow.
- Use jco / Component Model (KYRx) when you want typed record exchange without manual serialisation, need to share WIT interfaces across multiple components, or are building for a Component Model-native hosting environment.
- Both approaches produce standard
.wasmfiles that can be deployed to Cloudflare Workers, Bun, Deno, or Node.js without modification.
Note
wasmHandler() API reference and request/response lifecycle, and Cloudflare Workers for deployment considerations when bundling WASM binaries in a Worker script.