quickjs-vm
v0.3.0
Published
QuickJS-powered worker runtime for Node.js
Maintainers
Readme
quickjs-vm
QuickJS-powered worker runtime for Node.js.
If you want to run JavaScript in an isolated QuickJS runtime from Node, this library gives you a QuickJSWorker with support for eval, modules, injected globals, durable handles, message passing, limits, and runtime stats.
Install
npm install quickjs-vmThe Short Version
import { QuickJSWorker } from "quickjs-vm";
const worker = new QuickJSWorker({
maxEvalMs: 500, // max time allowed for each execution
maxCpuMs: 250, // max CPU time allowed for each execution
maxMemoryBytes: 32 * 1024 * 1024, // max memory allowed for the runtime.
});
const answer = await worker.eval("(a, b) => a + b", { args: [20, 22] });
console.log(answer); // 42
await worker.close();What You Get
eval(...)andevalSync(...)module.eval(...),module.import(...),module.register(...),module.clear(...)global.*for reading, writing, and calling things onglobalThishandle.*for durable references to runtime valuespostMessage(...)andon("message", ...)- limits for wall time, CPU time, memory, stack size, and interrupts
- stats for memory, CPU sampling, rates, latency, event loop lag, and last execution
- runtime and error events with host callsite metadata
Basic Evaluation
import { QuickJSWorker } from "quickjs-vm";
const worker = new QuickJSWorker();
console.log(await worker.eval("1 + 1")); // 2
console.log(await worker.eval('"hello".toUpperCase()')); // HELLO
console.log(worker.evalSync("6 * 7")); // 42
await worker.close();If the evaluated source returns a function and you pass args, the function is called for you:
const worker = new QuickJSWorker();
const result = await worker.eval("(name) => `Hello, ${name}`", {
args: ["Ada"],
});
console.log(result); // Hello, Ada
await worker.close();Modules
module.eval(...) returns a namespace-like object. Exported functions stay callable from Node.
import { QuickJSWorker } from "quickjs-vm";
const worker = new QuickJSWorker();
const math = await worker.module.eval(`
export const version = "1.0.0";
export function add(a, b) { return a + b; }
export async function double(n) { return n * 2; }
`);
console.log(math.version); // 1.0.0
console.log(await math.add(20, 22)); // 42
console.log(await math.double(21)); // 42
await worker.close();You can also register named modules:
const worker = new QuickJSWorker();
await worker.module.register("app:math", `
export const tau = 6.28318;
export function mul(a, b) { return a * b; }
`);
const math = await worker.module.import("app:math");
console.log(math.tau);
console.log(await math.mul(6, 7)); // 42
await worker.close();And if you want imports to resolve through host code, provide an imports callback:
const worker = new QuickJSWorker({
imports: (specifier) => {
if (specifier === "./math.js") {
return `
export function add(a, b) { return a + b; }
`;
}
return false; // throw on all other modules
},
});
const mod = await worker.module.eval(`
import { add } from "./math.js";
export const value = add(20, 22);
`);
console.log(mod.value); // 42
await worker.close();Globals
You can push values and functions into the worker:
const worker = new QuickJSWorker();
await worker.global.set("config", {
env: "test",
nested: { retries: 2 },
});
await worker.global.set("double", (n: number) => n * 2);
console.log(await worker.global.get("config.nested.retries")); // 2
console.log(await worker.global.call("double", [21])); // 42
await worker.close();The global.* API mirrors a useful chunk of the handle API, so you can also:
has(path)delete(path)keys(path?)entries(path?)define(path, descriptor)getOwnPropertyDescriptor(path)isCallable(path?)isPromise(path?)construct(path, args?)await(path, options?)clone(path)toJSON(path?)apply(path, ops)getType(path?)instanceOf(path, constructorPath)
Handles
Handles are for when you want a durable reference to a runtime value instead of repeatedly serializing it in and out.
const worker = new QuickJSWorker();
const counter = await worker.handle.eval(`
({
count: 0,
inc() {
this.count += 1;
return this.count;
}
})
`);
console.log(await counter.call("inc")); // 1
console.log(await counter.call("inc")); // 2
console.log(await counter.get("count")); // 2
await counter.dispose();
await worker.close();You can also bind a handle to an existing runtime object:
const worker = new QuickJSWorker();
await worker.eval(`
globalThis.toolbox = {
value: 41,
bump() { this.value += 1; return this.value; }
};
`);
const toolbox = await worker.handle.get("toolbox");
console.log(await toolbox.call("bump")); // 42
await toolbox.dispose();
await worker.close();Messaging
Worker to host:
const worker = new QuickJSWorker();
worker.on("message", (msg) => {
console.log("from worker:", msg);
});
await worker.eval(`postMessage({ hello: "from QuickJS" })`);
await worker.close();Host to worker:
const worker = new QuickJSWorker();
await worker.eval(`
globalThis.received = null;
on("message", (msg) => {
globalThis.received = msg;
});
`);
worker.postMessage({ hello: "from Node" });
setTimeout(async () => {
console.log(await worker.eval("received.hello")); // from Node
await worker.close();
}, 25);Limits
You can set defaults on the worker and override them per call.
const worker = new QuickJSWorker({
maxEvalMs: 100,
maxCpuMs: 50,
maxMemoryBytes: 16 * 1024 * 1024,
});
await worker.eval("1 + 1");
await worker.eval(
`
(() => {
const end = Date.now() + 40;
while (Date.now() < end) {}
return 42;
})()
`,
{ maxCpuMs: 250 },
);
await worker.close();Available limits/options include:
maxEvalMsmaxCpuMsmaxMemoryBytesmaxStackSizeBytesmaxInterruptgcThresholdAllocgcIntervalMs
Stats
The stats API is intentionally practical:
const worker = new QuickJSWorker();
await worker.eval("21 + 21");
console.log(worker.stats.lastExecution);
console.log(await worker.stats.cpu({ measureMs: 100 }));
console.log(await worker.stats.rates({ windowMs: 1000 }));
console.log(await worker.stats.latency({ windowMs: 1000 }));
console.log(await worker.stats.eventLoopLag({ measureMs: 20 }));
console.log(worker.stats.totals);
console.log(await worker.stats.memory());
worker.stats.reset();
await worker.close();Available stats:
stats.lastExecutionstats.activeOpsstats.cpu({ measureMs })stats.rates({ windowMs })stats.latency({ windowMs })stats.eventLoopLag({ measureMs })stats.totalsstats.reset()stats.memory()
Runtime and Error Events
QuickJSWorker exposes:
messagecloseruntimeerror
Runtime and error events include best-effort host callsite metadata, which is handy when some deeply wrapped helper eventually does something regrettable. Runtime and error events include best-effort host callsite metadata, which is useful when errors pass through a few layers before they are handled.
const worker = new QuickJSWorker();
worker.on("runtime", (event) => {
console.log(event.kind, event.hostCallSite);
});
worker.on("error", (event) => {
console.log(event.surface, event.error?.message);
console.log(event.hostCallSite);
});
await worker.eval("1 + 1");
await worker.close();Listeners can be removed with off(...), and listener failures are ignored so one broken callback does not poison the rest.
const onMessage = (msg: any) => console.log(msg);
worker.on("message", onMessage);
worker.off("message", onMessage);
worker.off("runtime"); // clear all runtime listenersBytecode
If you want to precompile some code and run it later, there is support for that too.
const worker = new QuickJSWorker();
const bytes = await worker.getByteCode("globalThis.answer = 42;");
await worker.loadByteCode(bytes);
console.log(await worker.eval("answer")); // 42
await worker.close();Resource Management
Workers and handles should be closed or disposed when you are done with them.
const worker = new QuickJSWorker();
try {
await worker.eval("1 + 1");
} finally {
await worker.close();
}QuickJSWorker and handles also support Symbol.asyncDispose.
Tests
npm testDevelopment
Build the native addon and TypeScript wrapper:
npm run buildAPI Reference
new QuickJSWorker(options?)
Creates a new QuickJS runtime.
Common options:
maxEvalMsmaxCpuMsmaxMemoryBytesmaxStackSizeBytesmaxInterruptgcThresholdAllocgcIntervalMsglobalsmodulesimports
Evaluation
worker.eval(source, options?)Evaluates code asynchronously and resolves with the resulting value. If the evaluated value is a function andoptions.argsis provided, that function is called with the supplied arguments.worker.evalSync(source, options?)Evaluates code synchronously and returns the resulting value directly. This is useful for small, fully synchronous snippets.
Evaluation options:
filenameSets the filename shown in runtime error messages and stacks.argsSupplies arguments when the evaluated source returns a callable function.maxEvalMsOverrides the wall-clock timeout for this call.maxCpuMsOverrides the CPU-time budget for this call.
Modules
worker.module.eval(source, options?)Evaluates module source and returns a namespace-like object containing the module exports.worker.module.import(specifier)Imports a named module or a module resolved through the configuredimportscallback.worker.module.register(moduleName, source, options?)Registers module source under a name so it can be imported later.worker.module.clear(moduleName)Removes a registered module name from the local module registry.
Module options:
- all evaluation options
Module evaluation accepts the same per-call
filename,args,maxEvalMs, andmaxCpuMsoptions aseval(...). moduleNamePins a module to a stable name so it can be imported again later.cjsWraps the provided source as CommonJS and exposes the result as module exports.
Globals
worker.global.set(path, value, options?)Writes a value ontoglobalThisinside the worker.worker.global.get(path, options?)Reads a value fromglobalThisusing a dotted path.worker.global.has(path, options?)Returns whether a dotted path exists onglobalThis.worker.global.delete(path, options?)Deletes a property fromglobalThis.worker.global.keys(path?, options?)Returns enumerable keys from the target object.worker.global.entries(path?, options?)Returns enumerable[key, value]pairs from the target object.worker.global.getOwnPropertyDescriptor(path, options?)Returns the property descriptor for a path if one exists.worker.global.define(path, descriptor, options?)Defines a property using a standard JavaScript property descriptor.worker.global.isCallable(path?, options?)Returns whether the target value is callable.worker.global.isPromise(path?, options?)Returns whether the target value is promise-like.worker.global.call(path, args?, options?)Calls a global function by path and resolves with its return value.worker.global.construct(path, args?, options?)Calls a global constructor withnewand resolves with the created value.worker.global.await(path, options?)Awaits a promise stored onglobalThisand resolves with the fulfilled value.worker.global.clone(path, options?)Creates a handle bound to a value onglobalThis.worker.global.toJSON(path?, options?)Produces a JSON-safe snapshot of the target value.worker.global.apply(path, ops, options?)Clones a global value as a temporary handle and runs a batch of handle operations against it.worker.global.getType(path?, options?)Returns type information for the target value, including whether it is callable.worker.global.instanceOf(path, constructorPath, options?)Returns whether a global value is an instance of a global constructor.
Handles
worker.handle.get(path, options?)Creates a handle for an existing runtime value reachable by path.worker.handle.tryGet(path, options?)Creates a handle if the path exists, otherwise resolves withundefined.worker.handle.eval(source, options?)Evaluates source and returns a durable handle to the resulting value.
Handle instance methods:
handle.get(path?, options?)Reads a property from the handled value.handle.has(path, options?)Returns whether a property exists on the handled value.handle.set(path, value, options?)Writes a property on the handled value.handle.delete(path, options?)Deletes a property from the handled value.handle.keys(path?, options?)Returns enumerable keys from the handled value.handle.entries(path?, options?)Returns enumerable entries from the handled value.handle.getOwnPropertyDescriptor(path, options?)Returns the property descriptor for a property on the handled value.handle.define(path, descriptor, options?)Defines a property on the handled value using a descriptor.handle.instanceOf(constructorPath, options?)Returns whether the handled value is an instance of a global constructor.handle.isCallable(path?, options?)Returns whether the handled value, or a property on it, is callable.handle.isPromise(path?, options?)Returns whether the handled value, or a property on it, is promise-like.handle.call(args?, options?)Calls the handled value itself if it is a function.handle.call(path, args?, options?)Calls a function property on the handled value.handle.construct(args?, options?)Uses the handled value as a constructor and returns the created value.handle.await(options?)Awaits a promise handle and optionally updates the handle to the resolved value.handle.clone(options?)Creates another handle pointing at the same runtime value.handle.toJSON(path?, options?)Produces a JSON-safe snapshot of the handled value or a nested property.handle.apply(ops, options?)Runs a list of handle operations in sequence and returns all results.handle.getType(path?, options?)Returns type information for the handled value or nested property.handle.dispose(options?)Releases the handle so it can no longer be used.
Messaging and Lifecycle
worker.postMessage(message)Sends a message from Node into the worker message channel.worker.on("message", handler)Registers a listener for messages posted from QuickJS.worker.on("close", handler)Registers a listener that fires once when the worker closes.worker.on("runtime", handler)Registers a listener for runtime telemetry such aseval.begin,eval.end, anderror.thrown.worker.on("error", handler)Registers a listener for runtime error events only.worker.off(event, handler?)Removes one listener for an event, or all listeners for that event if no handler is provided.worker.close()Closes the worker and releases the native runtime.worker.isClosed()Returns whether the worker has been closed.
Stats
worker.stats.lastExecutionReturns the last execution stats snapshot, ornullif nothing has run yet.worker.stats.activeOpsReturns the number of currently active async wrapper operations.worker.stats.cpu(options?)Samples CPU usage for the QuickJS thread over a short time window.worker.stats.rates(options?)Returns operation throughput by category over a rolling time window.worker.stats.latency(options?)Returns simple latency aggregates over a rolling time window.worker.stats.eventLoopLag(options?)Measures host event-loop lag over a short timer interval.worker.stats.totalsReturns cumulative counters collected by the wrapper.worker.stats.reset(options?)Clears rolling samples and, by default, resets the totals counters as well.worker.stats.memory()Returns QuickJS memory usage information from the native runtime.
Other Runtime Methods
worker.getByteCode(source)Compiles source to QuickJS bytecode and returns the resulting bytes.worker.loadByteCode(bytes)Loads previously compiled QuickJS bytecode into the runtime.worker.gc()Triggers garbage collection in the QuickJS runtime.worker.memory()Returns the raw QuickJS memory usage payload directly from the runtime.worker[Symbol.asyncDispose]()Async disposal hook that closes the worker when used withawait using.
License
MIT
