Node.js bridge for the Io language WASM runtime (io_browser.wasm).
Io is a small, prototype-based language with first-class actors, coroutines, and heap-based execution frames. This package lets you embed the Io VM in a Node.js process and evaluate Io code, exchange values across the JS↔Io boundary, and drive Io's async actor model from Node.js.
Io supports actors and coroutines at the language level, backed by heap-based
execution frames. There is no function coloring — every method is just a method,
regardless of whether it touches async I/O. Concurrency is expressed with @,
not with keywords scattered across the call stack.
result := SlowActor @compute(payload) // returns FutureProxy immediately
// other work here ...
result await println // blocks only when the value is neededUnder the hood, await suspends the entire WASM instance (status=2) and
returns control to the JS event loop. The execution state survives intact on the
heap — no stack to unwind, no async to propagate upward. When the awaited
Promise settles, Node.js calls io_resume_eval() and the Io world resumes
exactly where it left off. From the Io code's perspective, await is just a
message send like any other.
ORM queries, HTTP calls, message queues — any npm package can be wrapped in an actor that speaks the Io bridge protocol. Domain code sends messages; it never sees a Promise or a callback.
// domain only sends messages
result := UserRepo @findById(42)
user := result await
// the actor handles the JS side
UserRepo findById := method(id,
jsfunction("return orm.users.findOne({ where: { id: arguments[0] } })") call(id)
)Rejected Promises cross the bridge as catchable Io exceptions. Adapters catch at the boundary and return domain values — the domain never deals with raw JS errors.
run := method(
e := try(jsfunction("return Promise.reject(new Error('db timeout'))") call await)
e catch(Exception, exception := "ERR:" .. e error)
exception
)The entire Io world runs inside a single WASM instance. Load js-yielder.io at
bootstrap to guarantee a JS event loop trip at least every 100 ms, keeping timers,
I/O callbacks, and other Node.js machinery alive.
doFile("js-yielder.io") // define JSYielder
JSYielder @@start // background loop, fire-and-forgetThis bridge is derived from the official io.js browser loader bundled with
io_browser.wasm (located at browser/io.js in the Io WASM distribution).
io.js provides the canonical WASM import implementations (WASI shim, JS bridge
serialization protocol, IoProxy factory) that io_browser.wasm depends on.
io.js (browser) |
io-nodejs-bridge (this package) |
|
|---|---|---|
| WASM loading | fetch + instantiateStreaming |
fs.readFileSync + instantiate |
loadIo signature |
loadIo() — URL hardcoded |
loadIo(wasmPath) — path as argument |
clock_time_get |
performance.now() * 1e6 (ms → ns) |
process.hrtime.bigint() (native ns precision) |
ioEval return |
{status, output} sync only; async wired manually into DOM |
Promise<{status, output}> for both sync and async paths |
| Exports | Browser globals (window.io, etc.) |
ES Module named exports |
| REPL / DOM | Full REPL UI, keyboard history, boot() |
Removed — pure VM bridge |
getHandle null check |
|| (falsy coercion) |
?? (nullish only — 0 and false handled correctly) |
io.js targets the browser streaming API:
// io.js
const response = await fetch(IO_WASM_URL);
const { instance } = await WebAssembly.instantiateStreaming(response, importObject);This bridge uses the Node.js filesystem:
// io-nodejs-bridge
const bytes = readFileSync(wasmPath);
const { instance } = await WebAssembly.instantiate(bytes, { wasi_snapshot_preview1, js });loadIo accepts a path, so the .wasm file can live anywhere on the filesystem.
io.js derives nanoseconds from performance.now(), which has millisecond
granularity in many browsers. This bridge uses process.hrtime.bigint(), which
returns true nanosecond-precision monotonic time — important for Io's internal
timer and scheduler.
io.js returns a plain {status, output} object from ioEval in all cases,
and wires the async resume path (pendingAsyncResolve) directly into the DOM
REPL callback. This bridge promotes ioEval to a proper async boundary:
// always awaitable — works for both sync and AWAIT_JS (status=2) paths
const { status, output } = await ioEval('someIoCode');When Io hits FRAME_STATE_AWAIT_JS (status=2), ioEval returns a Promise that
resolves once all chained JS Promises settle and io_resume_eval completes.
npm install @on-the-ground/io-nodejsYou also need the io_browser.wasm binary. It is not bundled in this
package — supply your own path when calling loadIo.
import { loadIo, ioEval, io } from '@on-the-ground/io-nodejs';
// Load the Io VM (once per process)
await loadIo('/path/to/io_browser.wasm');
// Evaluate Io code — returns { status, output }
const { status, output } = await ioEval('"hello" .. " world"');
console.log(output); // ==> hello world
// Call Io methods from JS via the Lobby proxy
await ioEval('Lobby greet := method(name, "Hello, " .. name)');
const result = io.lobby.greet('world');Loads and initializes the Io VM from a .wasm file path. Must be called once
before any other API. Runs _initialize() (WASI reactor) then io_init().
Evaluates a string of Io code. Returns a Promise that resolves with:
status—0= success,1= uncaught exception,-1= input too longoutput— captured stdout, including the REPL==> valueline for the last expression
Internally handles FRAME_STATE_AWAIT_JS (status=2): if Io suspends waiting for
a JS Promise, ioEval returns a Promise that resolves once the Io session
completes.
A JS Proxy over the Io Lobby object. Property access sends a message to Io and
returns the result deserialized to a JS value.
io.lobby.someMethod(arg1, arg2);Low-level message send to an arbitrary Io object by handle.
Values crossing the JS↔Io boundary are serialized via a shared 64 KB buffer
using the binary protocol defined in Bridge.md (part of the Io WASM
distribution). Supported types:
| JS type | Wire type |
|---|---|
null |
TYPE_NIL |
undefined |
TYPE_UNDEFINED |
boolean |
TYPE_TRUE / TYPE_FALSE |
number |
TYPE_NUMBER (float64) |
string |
TYPE_STRING |
bigint |
TYPE_BIGINT (decimal string) |
Array |
TYPE_ARRAY |
Map |
TYPE_OBJECT |
Set |
TYPE_ARRAY |
TypedArray |
TYPE_TYPEDARRAY |
Promise / thenable |
TYPE_FUTURE → IoFuture |
| other objects | TYPE_JSREF (handle) |
Cyclic structures throw. Symbol throws.
See the Io language project for the io_browser.wasm license.
This bridge code is MIT.