Skip to content

on-the-ground/io-nodejs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

io-nodejs-bridge

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.

What you can build

Concurrent domain logic without async/await

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 needed

Under 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.

Push Node.js infrastructure behind actor boundaries

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)
)

JS errors become Io exceptions

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
)

Cooperative JS event loop yielding

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-forget

Based on io.js

This 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.

Key differences from io.js

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)

WASM loading

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.

clock_time_get precision

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.

ioEval async handling

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.

Installation

npm install @on-the-ground/io-nodejs

You also need the io_browser.wasm binary. It is not bundled in this package — supply your own path when calling loadIo.

Usage

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');

API

loadIo(wasmPath: string): Promise<void>

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().

ioEval(code: string): Promise<{ status: number, output: string }>

Evaluates a string of Io code. Returns a Promise that resolves with:

  • status0 = success, 1 = uncaught exception, -1 = input too long
  • output — captured stdout, including the REPL ==> value line 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.

io.lobby

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);

io.send(handle, message, ...args)

Low-level message send to an arbitrary Io object by handle.

Bridge serialization

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_FUTUREIoFuture
other objects TYPE_JSREF (handle)

Cyclic structures throw. Symbol throws.

License

See the Io language project for the io_browser.wasm license. This bridge code is MIT.

About

Node.js bridge for the Io language WASM runtime

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors