Contexts
Keep JavaScript variables and imports between calls with a context, and run several contexts in parallel in one VM.
Every call on a VM starts with fresh JavaScript memory. A context keeps it: variables and imports from one call are still there in the next. Use one for a REPL, a notebook, or an agent that builds on its earlier steps.
Files, packages, and processes belong to the VM, not to a context, so you do not need a context for those.
Keep state between calls
import { createVm } from "secure-exec";
const vm = await createVm();
// A context keeps JavaScript variables and imports between calls, like a REPL.
const context = await vm.createContext();
await context.execute("globalThis.cart = []");
await context.execute(`cart.push({ item: "coffee", price: 4 })`);
await context.execute(`cart.push({ item: "bagel", price: 3 })`);
const total = await context.evaluate<number>(
"cart.reduce((sum, line) => sum + line.price, 0)",
);
console.log(total.outcome === "succeeded" ? total.value : total.error); // 7
Each call is its own ES module, so top-level const and let stay scoped to
that call. Put values on globalThis to share them. A context holds memory and
does nothing between calls.
TypeScript
// TypeScript shares the same state. Pass the context's id to `vm.typescript`.
const typed = await vm.typescript.evaluate<number>(
"(globalThis.cart as { price: number }[]).length",
{ contextId: context.contextId },
);
console.log(typed.outcome === "succeeded" ? typed.value : typed.error); // 2
Isolation
// Calls outside the context never see its state, even in the same VM.
const outside = await vm.javascript.evaluate<string>("typeof globalThis.cart");
console.log(outside.outcome === "succeeded" ? outside.value : outside.error); // undefined
Run in parallel
A context runs one call at a time. A second call while one is running fails
with execution_busy instead of queueing. For parallel work, create several
contexts in the same VM. Each has its own memory, and they share the VM’s files
and packages.
import { createVm } from "secure-exec";
const vm = await createVm();
// A context runs one call at a time. For parallel work, create several: each
// has its own JavaScript memory, and they share the VM's files and packages.
const shards = await Promise.all([
vm.createContext(),
vm.createContext(),
vm.createContext(),
]);
const started = performance.now();
const results = await Promise.all(
shards.map((context, shard) =>
context.evaluate<number>(
"new Promise((resolve) => setTimeout(() => resolve(inputs.shard * 10), 500))",
{ inputs: { shard } },
),
),
);
console.log(results.map((result) => ("value" in result ? result.value : null))); // [0, 10, 20]
console.log(`${Math.round(performance.now() - started)}ms`); // well under the 1500ms that running them one after another takes
await vm.dispose();
When parallel contexts need shared mutable state, keep it in a file, or in a background process that they all call.
Reset and dispose
// `reset` clears the state. `dispose` deletes the context and leaves the VM.
await context.reset();
const afterReset = await context.evaluate<string>("typeof globalThis.cart");
console.log(
afterReset.outcome === "succeeded" ? afterReset.value : afterReset.error,
); // undefined
await context.dispose();
await vm.dispose();
context.dispose() deletes the context and leaves the VM running. Disposing the
VM removes every context in it.