# 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

```sh
npm install quickjs-vm
```

## The Short Version

```ts
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(...)` and `evalSync(...)`
- `module.eval(...)`, `module.import(...)`, `module.register(...)`, `module.clear(...)`
- `global.*` for reading, writing, and calling things on `globalThis`
- `handle.*` for durable references to runtime values
- `postMessage(...)` and `on("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

```ts
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:

```ts
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.

```ts
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:

```ts
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:

```ts
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:

```ts
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.

```ts
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:

```ts
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:

```ts
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:

```ts
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.

```ts
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:

- `maxEvalMs`
- `maxCpuMs`
- `maxMemoryBytes`
- `maxStackSizeBytes`
- `maxInterrupt`
- `gcThresholdAlloc`
- `gcIntervalMs`

## Stats

The stats API is intentionally practical:

```ts
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.lastExecution`
- `stats.activeOps`
- `stats.cpu({ measureMs })`
- `stats.rates({ windowMs })`
- `stats.latency({ windowMs })`
- `stats.eventLoopLag({ measureMs })`
- `stats.totals`
- `stats.reset()`
- `stats.memory()`

## Runtime and Error Events

`QuickJSWorker` exposes:

- `message`
- `close`
- `runtime`
- `error`

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.

```ts
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.

```ts
const onMessage = (msg: any) => console.log(msg);

worker.on("message", onMessage);
worker.off("message", onMessage);
worker.off("runtime"); // clear all runtime listeners
```

## Bytecode

If you want to precompile some code and run it later, there is support for that too.

```ts
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.

```ts
const worker = new QuickJSWorker();

try {
  await worker.eval("1 + 1");
} finally {
  await worker.close();
}
```

`QuickJSWorker` and handles also support `Symbol.asyncDispose`.

## Tests

```sh
npm test
```

## Development

Build the native addon and TypeScript wrapper:

```sh
npm run build
```

## API Reference

### `new QuickJSWorker(options?)`

Creates a new QuickJS runtime.

Common options:

- `maxEvalMs`
- `maxCpuMs`
- `maxMemoryBytes`
- `maxStackSizeBytes`
- `maxInterrupt`
- `gcThresholdAlloc`
- `gcIntervalMs`
- `globals`
- `modules`
- `imports`

### Evaluation

- `worker.eval(source, options?)`
  Evaluates code asynchronously and resolves with the resulting value. If the evaluated value is a function and `options.args` is 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:

- `filename`
  Sets the filename shown in runtime error messages and stacks.
- `args`
  Supplies arguments when the evaluated source returns a callable function.
- `maxEvalMs`
  Overrides the wall-clock timeout for this call.
- `maxCpuMs`
  Overrides 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 configured `imports` callback.
- `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`, and `maxCpuMs` options as `eval(...)`.
- `moduleName`
  Pins a module to a stable name so it can be imported again later.
- `cjs`
  Wraps the provided source as CommonJS and exposes the result as module exports.

### Globals

- `worker.global.set(path, value, options?)`
  Writes a value onto `globalThis` inside the worker.
- `worker.global.get(path, options?)`
  Reads a value from `globalThis` using a dotted path.
- `worker.global.has(path, options?)`
  Returns whether a dotted path exists on `globalThis`.
- `worker.global.delete(path, options?)`
  Deletes a property from `globalThis`.
- `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 with `new` and resolves with the created value.
- `worker.global.await(path, options?)`
  Awaits a promise stored on `globalThis` and resolves with the fulfilled value.
- `worker.global.clone(path, options?)`
  Creates a handle bound to a value on `globalThis`.
- `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 with `undefined`.
- `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 as `eval.begin`, `eval.end`, and `error.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.lastExecution`
  Returns the last execution stats snapshot, or `null` if nothing has run yet.
- `worker.stats.activeOps`
  Returns 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.totals`
  Returns 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 with `await using`.

## License

MIT
