npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@on-the-ground/io-nodejs

v0.2.0

Published

Node.js bridge for the Io language WASM runtime

Readme

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 call Io domain logic from JS adapters — including Express routes, NestJS controllers, or any other Node.js framework.

Each worker thread runs an independent WASM instance. Concurrent calls from multiple requests are queued and drained automatically, so the Node.js event loop is never starved.

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.

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
)

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, options) — path + worker pool config | | 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 | | Concurrency | Single VM, no isolation | Worker thread pool — each worker owns a WASM instance | | REPL / DOM | Full REPL UI, keyboard history, boot() | Removed — pure VM bridge | | getHandle null check | \|\| (falsy coercion) | ?? (nullish only — 0 and false handled correctly) |


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, ioCall, ioEval, closeIo } from '@on-the-ground/io-nodejs';

// Initialize the worker pool (once per process)
await loadIo('/path/to/io_browser.wasm', {
    workers: 4,          // one WASM instance per worker thread (default: 1)
    ioFiles: ['js-yielder.io'], // Io files evaluated at startup in each worker
});

// Call a Lobby method — dispatched to the next idle worker
const result = await ioCall('placeOrder', { userId: 1, items: [] });

// Evaluate raw Io code — also queued through the pool
const { status, output } = await ioEval('"hello" .. " world"');
console.log(output); // hello world

await closeIo();

Using with Express

import express from 'express';
import { loadIo, ioCall, closeIo } from '@on-the-ground/io-nodejs';

const app = express();
app.use(express.json());

await loadIo('/path/to/io.wasm', { workers: 4, ioFiles: ['js-yielder.io'] });

app.post('/orders', async (req, res) => {
    try {
        const result = await ioCall('placeOrder', req.body);
        res.json({ result });
    } catch (e) {
        res.status(500).json({ error: e.message });
    }
});

await closeIo();

Multiple concurrent requests each get a Promise immediately. Workers process them as they become available; the queue absorbs bursts up to maxQueue.


API

loadIo(wasmPath, options?): Promise<void>

Initializes the worker pool. Must be called once before any other API.

| Option | Type | Default | Description | |---|---|---|---| | workers | number | 1 | Worker threads to spawn. Each owns a WASM instance. | | maxQueue | number | 1000 | Max pending jobs. Excess calls reject immediately. | | ioFiles | string[] | [] | Io source files evaluated at startup in every worker. |

// minimal
await loadIo('/path/to/io.wasm');

// production
await loadIo('/path/to/io.wasm', {
    workers: 4,
    maxQueue: 500,
    ioFiles: ['js-yielder.io', 'domain.io'],
});

ioCall(method, args?): Promise<string>

Calls a Lobby method with a single structured argument object. Dispatched to the next idle worker; queued if all workers are busy.

Returns the last Io expression value with the "==> " prefix stripped. Throws if the Io method raises an uncaught exception, or if the queue is full.

Both sides must follow the single-object args convention:

// JS caller (adapter layer)
const result = await ioCall('placeOrder', { userId: 1, items: [{ sku: 'A', qty: 2 }] });
// Io implementer (domain layer)
Lobby placeOrder := method(args,
    userId := args get("userId")
    items  := args get("items")
    ...
)

Args are serialized through the bridge-buffer binary format, so nested objects, arrays, numbers, and strings all round-trip correctly.

closeIo(): void

Terminates all worker threads and drains the pool. Call this during process shutdown to avoid keeping the Node.js process alive.

process.on('SIGTERM', closeIo);

// or in tests / scripts:
test().catch(console.error).finally(closeIo);

Pending jobs still in the queue will reject. In-flight jobs may not complete.

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

Evaluates a string of Io code on any available worker. Queued if all workers are busy; rejects if the queue is full.

  • status0 = success, 1 = uncaught exception, -1 = input too long
  • output — captured stdout; the leading ==> prefix and trailing newline are stripped automatically

Handles FRAME_STATE_AWAIT_JS (status=2) transparently: if Io suspends waiting for a JS Promise, the returned Promise resolves only after the full async chain completes and io_resume_eval finishes.


Concurrency model

loadIo({ workers: N })
  → N Worker threads, each with an independent WASM instance

ioCall() / ioEval()
  → enqueue job, return Promise immediately
  → idle worker picks job, processes it
  → resolves Promise on completion

back-pressure
  → queue.length >= maxQueue → reject("Queue full")

Each WASM instance is single-threaded internally. Multiple workers provide parallelism across CPU cores. All workers load the same Io code, so any worker can serve any request — no routing or consistent hashing required as long as domain state is backed by external storage (the recommended pattern).


Actor async contract

The Io WASM runtime has two independent suspension mechanisms. Understanding how they interact is essential for writing correct actor methods that touch async JS work.

Mechanism 1 — Io coroutine scheduler (inside the VM)

@ sends a message asynchronously by creating a new coroutine inside the WASM VM. The JS side is not involved at all. The Io scheduler decides which coroutine runs next.

io_eval_input() running ──────────────────────────────────────
  main coroutine:  (actor @method(args)) await   ← yields, waits for actor
  actor coroutine: actor method(args) executing  ← scheduler switches here

Mechanism 2 — FRAME_STATE_AWAIT_JS (whole-VM suspend)

When Io awaits a JS Promise, the entire WASM instance pauses: io_eval_input() returns status=2. Node.js holds control until the Promise settles, then calls io_resume_eval() to continue.

io_eval_input() → status=2  (WASM paused)
Node.js: waiting for Promise...
5 s later: io_resume_eval() → WASM resumes

Why they must not be combined

If an actor uses await internally on a JS Promise, both mechanisms activate at once:

io_eval_input() ─────────────────────────────────────────────────
  main coroutine: (actor @sleep(ms)) await      ← [1] yields for actor
    actor coroutine: jsfunction(...) call(ms) await  ← [2] FRAME_STATE_AWAIT_JS
io_eval_input() → status=2

After 5 s, io_resume_eval() fires:

io_resume_eval() ────────────────────────────────────────────────
  actor coroutine resumes → computes result → actor finishes
  [3] scheduler must now switch back to main coroutine and continue it
  ← io_resume_eval() cannot perform this second scheduler hop → status=1

io_resume_eval() resumes from exactly the point where FRAME_STATE_AWAIT_JS was triggered (inside the actor coroutine). After the actor finishes and the FutureProxy must propagate its result back to the suspended main coroutine, the VM throws — status=1, no output.

The rule

Actor methods must not await a JS Promise directly. Return the Promise; let the caller's await trigger FRAME_STATE_AWAIT_JS in the main coroutine.

// WRONG — await inside the actor coroutine
Actor sleep := method(ms,
    jsfunction("return new Promise(r => setTimeout(r, arguments[0]))") call(ms) await
    "done"
)
start := Date now
(Actor @sleep(5000)) await println   // → status=1, no output
// CORRECT — actor returns the Promise; caller awaits
Actor sleep := method(ms,
    jsfunction("return new Promise(r => setTimeout(r, arguments[0]))") call(ms)
    // ↑ Promise returned as-is — no await here
)
start := Date now
(Actor @sleep(5000)) await           // ← FRAME_STATE_AWAIT_JS fires here,
                                     //   in the main coroutine, as io_eval_input
                                     //   can directly handle it
start secondsSinceNow println        // → ~5.001

When an actor method's last expression is a JS Promise (or any object that wraps one), Io automatically chains the FutureProxy to wait for that Promise before resolving. The await in the caller triggers FRAME_STATE_AWAIT_JS at the correct level — the main coroutine — so io_eval_input() / io_resume_eval() can manage the full lifecycle without the extra scheduler hop.


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.