diff --git a/.changeset/drop-quickjs-process-host.md b/.changeset/drop-quickjs-process-host.md new file mode 100644 index 0000000..e2f935f --- /dev/null +++ b/.changeset/drop-quickjs-process-host.md @@ -0,0 +1,5 @@ +--- +"@execbox/quickjs": minor +--- + +Remove `QuickJsExecutor({ host: "process" })` and its process-hosted runner entrypoint. Use `host: "worker"` for local hosted QuickJS execution, or `@execbox/remote` for app-owned process, container, or VM boundaries. diff --git a/README.md b/README.md index 0e68aa8..be862c9 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Portable code execution for [Model Context Protocol](https://modelcontextprotoco -Execbox turns host tool catalogs into callable guest namespaces, supports MCP wrapping on both sides of the boundary, and lets you place guest JavaScript where it fits your deployment: inline, behind a worker or child-process host, or across your own remote transport. +Execbox turns host tool catalogs into callable guest namespaces, supports MCP wrapping on both sides of the boundary, and lets you place guest JavaScript where it fits your deployment: inline, behind a worker host, or across your own remote transport. ## Package Map | Package | npm | What it is for | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [`@execbox/core`](./packages/core/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fcore?style=flat-square)](https://www.npmjs.com/package/@execbox/core) | Core execution contract, provider resolution, MCP adapters, and runtime/protocol subpaths | -| [`@execbox/quickjs`](./packages/quickjs/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fquickjs?style=flat-square)](https://www.npmjs.com/package/@execbox/quickjs) | QuickJS executor for inline, worker, and process hosts | +| [`@execbox/quickjs`](./packages/quickjs/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fquickjs?style=flat-square)](https://www.npmjs.com/package/@execbox/quickjs) | QuickJS executor for inline and worker hosts | | [`@execbox/remote`](./packages/remote/) | [![npm](https://img.shields.io/npm/v/%40execbox%2Fremote?style=flat-square)](https://www.npmjs.com/package/@execbox/remote) | Advanced transport-backed executor for app-owned runtime boundaries | ## Examples diff --git a/benchmarks/benchmark.ts b/benchmarks/benchmark.ts index 83401fb..db41747 100644 --- a/benchmarks/benchmark.ts +++ b/benchmarks/benchmark.ts @@ -2,7 +2,7 @@ * Comprehensive execbox performance benchmark suite. * * Covers: - * 1. Single-execution latency across all in-process & out-of-process executors + * 1. Single-execution latency across inline and worker-hosted executors * 2. Cold-start vs warm (pooled/prewarmed) first-execution cost * 3. Tool-call overhead scaling (0, 1, 5, 10 tool calls) * 4. Schema validation overhead (with vs without input/output schemas) @@ -349,7 +349,7 @@ async function suiteToolCalls(): Promise { ).join(";\n"); } - // Use quickjs (fastest) and worker-pooled (representative out-of-process) + // Use quickjs (fastest) and worker-pooled (representative hosted local mode) const testFactories = factories.filter( (f) => f.name === "quickjs (in-process)" || f.name === "worker (pooled)", ); @@ -507,31 +507,29 @@ async function suiteContention(): Promise { const poolSizes = [1, 2, 4]; const concurrency = 8; - for (const executor_type of ["worker", "process"] as const) { - console.log(`\n [${executor_type}]`); + console.log("\n [worker]"); - for (const poolSize of poolSizes) { - const executor = createContentionExecutor(executor_type, poolSize); - try { - await executor.prewarm?.(poolSize); - const { totalMs, perRequestMs } = await benchConcurrent( - executor, - code, - [simpleProvider], - concurrency, - totalRuns, - ); - const throughput = (totalRuns / totalMs) * 1000; - console.log( - ` pool_size=${String(poolSize).padEnd(2)} concurrency=${concurrency} ` + - `throughput=${throughput.toFixed(1)} exec/s ` + - `median=${fmt(median(perRequestMs))} ` + - `p95=${fmt(percentile(perRequestMs, 95))} ` + - `max=${fmt(max(perRequestMs))}`, - ); - } finally { - await executor.dispose?.(); - } + for (const poolSize of poolSizes) { + const executor = createContentionExecutor(poolSize); + try { + await executor.prewarm?.(poolSize); + const { totalMs, perRequestMs } = await benchConcurrent( + executor, + code, + [simpleProvider], + concurrency, + totalRuns, + ); + const throughput = (totalRuns / totalMs) * 1000; + console.log( + ` pool_size=${String(poolSize).padEnd(2)} concurrency=${concurrency} ` + + `throughput=${throughput.toFixed(1)} exec/s ` + + `median=${fmt(median(perRequestMs))} ` + + `p95=${fmt(percentile(perRequestMs, 95))} ` + + `max=${fmt(max(perRequestMs))}`, + ); + } finally { + await executor.dispose?.(); } } } diff --git a/benchmarks/config.ts b/benchmarks/config.ts index 3ac1157..e21df45 100644 --- a/benchmarks/config.ts +++ b/benchmarks/config.ts @@ -70,24 +70,6 @@ export function createBenchmarkFactories(): ExecutorFactory[] { }) as DisposableExecutor, supportsExplicitPrewarm: true, }, - { - name: "process (ephemeral)", - create: () => - new QuickJsExecutor({ - host: "process", - mode: "ephemeral", - }) as DisposableExecutor, - supportsExplicitPrewarm: false, - }, - { - name: "process (pooled)", - create: () => - new QuickJsExecutor({ - host: "process", - ...createPooledBenchmarkOptions(), - }) as DisposableExecutor, - supportsExplicitPrewarm: true, - }, ]; } @@ -98,12 +80,7 @@ export function createMemoryBenchmarkFactories( return factories.filter((factory) => allowedNames.has(factory.name)); } -export function createContentionExecutor( - executorType: "worker" | "process", - poolSize: number, -): DisposableExecutor { +export function createContentionExecutor(poolSize: number): DisposableExecutor { const pool = createContentionPoolOptions(poolSize); - return executorType === "worker" - ? (new QuickJsExecutor({ host: "worker", pool }) as DisposableExecutor) - : (new QuickJsExecutor({ host: "process", pool }) as DisposableExecutor); + return new QuickJsExecutor({ host: "worker", pool }) as DisposableExecutor; } diff --git a/benchmarks/results.md b/benchmarks/results.md index ac33435..467987c 100644 --- a/benchmarks/results.md +++ b/benchmarks/results.md @@ -16,14 +16,11 @@ This file records the current local benchmark run. Treat the tables as measured | QuickJS (in-process) | **1.47ms** | **2.71ms** | **3.94ms** | | Worker (ephemeral) | 62.40ms | 64.16ms | 65.67ms | | Worker (pooled) | 1.53ms | 2.81ms | 4.10ms | -| Process (ephemeral) | 202.37ms | 204.10ms | 207.70ms | -| Process (pooled) | 1.61ms | 2.92ms | 4.18ms | ### Notes - On this machine, warmed pooled executors stayed close to QuickJS for trivial scripts. -- Ephemeral executors remained far slower than pooled executors because each execution still pays worker or process startup cost. -- Process pooled stayed low-latency in median terms, but its process startup path remained much more expensive than worker startup. +- Ephemeral executors remained far slower than pooled executors because each execution still pays worker startup cost. --- @@ -36,12 +33,10 @@ Only pooled executors expose explicit `prewarm()`. QuickJS and ephemeral executo | QuickJS (in-process) | 1.46ms | N/A | N/A | | Worker (ephemeral) | 62.70ms | N/A | N/A | | Worker (pooled) | 62.30ms | 1.81ms | 97.1% | -| Process (ephemeral) | 202.51ms | N/A | N/A | -| Process (pooled) | 202.31ms | 2.32ms | 98.9% | ### Notes -- True `prewarm()` delivered the intended first-request behavior in this run: pooled worker and pooled process executors dropped from shell-plus-guest startup latency to low-single-digit warm execution latency. +- True `prewarm()` delivered the intended first-request behavior in this run: pooled worker executors dropped from shell-plus-guest startup latency to low-single-digit warm execution latency. - `prewarm()` pays the host-shell and guest-startup path before live traffic arrives. --- @@ -92,13 +87,11 @@ The pooled benchmark factories in this suite use a fixed `pool.maxSize: 2`. | -------------------- | --------------- | ------ | ------ | ---------- | | QuickJS (in-process) | **395.3** | 684.6 | 1078.8 | **1635.1** | | Worker (pooled) | 356.5 | 722.9 | 706.4 | 749.8 | -| Process (pooled) | 352.3 | 653.2 | 634.5 | 675.1 | ### Notes - QuickJS stayed the strongest path for trusted, in-process workloads at low and high concurrency in this run, while worker pooled edged ahead at concurrency 2. -- Worker and process pooled executors paid visible queueing once demand moved past the benchmark pool size. -- Process pooled stayed competitive, but it still trailed worker pooled at every tested concurrency level. +- Worker pooled executors paid visible queueing once demand moved past the benchmark pool size. --- @@ -109,21 +102,16 @@ The pooled benchmark factories in this suite use a fixed `pool.maxSize: 2`. | Worker | 1 | 362.6 | 22.18ms | 22.51ms | 22.53ms | | Worker | 2 | 704.2 | 10.97ms | 11.64ms | 12.39ms | | Worker | 4 | **1327.2** | 5.60ms | 6.95ms | 6.99ms | -| Process | 1 | 351.0 | 22.54ms | 23.66ms | 23.97ms | -| Process | 2 | 693.3 | 11.02ms | 12.95ms | 12.99ms | -| Process | 4 | 1111.3 | 6.50ms | 8.73ms | 8.75ms | ### Notes -- Increasing pool size improved both worker and process executors in this run. -- Worker pooled still kept the better mix of throughput and tail latency at every pool size tested. -- Process pooled also scaled up well here, but its tails remained wider than worker pooled at the same pool size. +- Increasing pool size improved worker throughput and tail latency in this run. --- ## 7. Host-Process Memory Delta -This suite only measures the parent Node process. It does not attempt to attribute child-process RSS back to `QuickJsExecutor({ host: "process" })`. +This suite only measures the parent Node process. | Executor | Heap Delta | RSS Delta | External Delta | | -------------------- | ---------- | --------- | -------------- | @@ -144,13 +132,11 @@ This suite only measures the parent Node process. It does not attempt to attribu ### High-value takeaways from this snapshot - QuickJS remained the lowest-latency option for trusted, in-process workloads on this machine. -- True `prewarm()` delivered the intended first-request benefit for pooled worker and pooled process executors. +- True `prewarm()` delivered the intended first-request benefit for pooled worker executors. - Worker pooled remained the strongest general-purpose local trade-off between isolation, throughput, and tail latency. -- Process pooled stayed viable when process isolation matters, but it still trailed worker pooled on throughput and tail latency. - Ephemeral modes remained dramatically slower than pooled modes and are best reserved for cases that need a fresh host boundary per execution. ### What this snapshot does not prove - It does not prove exact throughput rankings for every workload or host. The concurrency and tool-call suites are still sensitive to local scheduler noise. -- It does not prove memory behavior for `QuickJsExecutor({ host: "process" })`, because the memory suite intentionally avoids reporting child-process RSS as if it were host-process memory. - It does not measure `RemoteExecutor`, because remote performance depends on the caller-owned transport and remote runtime deployment. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 8a56653..e0f6990 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -40,11 +40,11 @@ flowchart LR ### Package Roles -| Package | Role | -| ------------------ | ------------------------------------------------------------------------------------------------------------ | -| `@execbox/core` | App-facing core types, provider resolution, MCP adapters, plus runtime and protocol subpaths | -| `@execbox/quickjs` | Default QuickJS executor package with inline, worker-hosted, and process-hosted modes plus a reusable runner | -| `@execbox/remote` | Transport-backed executor that runs against an app-defined runner boundary | +| Package | Role | +| ------------------ | -------------------------------------------------------------------------------------------- | +| `@execbox/core` | App-facing core types, provider resolution, MCP adapters, plus runtime and protocol subpaths | +| `@execbox/quickjs` | Default QuickJS executor package with inline and worker-hosted modes plus a reusable runner | +| `@execbox/remote` | Transport-backed executor that runs against an app-defined runner boundary | ## End-to-End Execution Model diff --git a/docs/architecture/execbox-core.md b/docs/architecture/execbox-core.md index 2a49710..e2b77b3 100644 --- a/docs/architecture/execbox-core.md +++ b/docs/architecture/execbox-core.md @@ -189,12 +189,12 @@ Executors are responsible for their own runtime-specific classification rules, b ## Why the Core Stays Small -The core package does not own QuickJS, worker threads, child processes, or transport mechanics. That separation keeps the core useful for: +The core package does not own QuickJS, worker threads, process boundaries, or transport mechanics. That separation keeps the core useful for: - direct in-process runtimes - worker-backed runtimes - MCP wrapper servers -- process or remote execution models +- remote execution models The consequence is deliberate separation between: diff --git a/docs/architecture/execbox-executors.md b/docs/architecture/execbox-executors.md index 351dd87..8a82011 100644 --- a/docs/architecture/execbox-executors.md +++ b/docs/architecture/execbox-executors.md @@ -4,12 +4,11 @@ This page explains how the available executors and QuickJS host modes differ and ## Executor Comparison -| Executor or mode | Runtime boundary | Tool bridge style | Main strengths | Main constraints | -| ---------------------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------ | -------------------------------------------------- | -| `QuickJsExecutor` | Fresh in-process QuickJS runtime | Shared runner callback | No native addon, simple install, default backend | Still in-process | -| `QuickJsExecutor` with `host: "worker"` | Worker thread + fresh QuickJS runtime | Shared host session + messages | Hard-stop worker termination, pooled by default | Still same OS process; ephemeral mode is slower | -| `QuickJsExecutor` with `host: "process"` | Child process + fresh QuickJS runtime | Shared host session + IPC | Hard-kill process termination, pooled by default | Still not a container/VM; ephemeral mode is slower | -| `RemoteExecutor` | App-defined remote transport boundary | Shared host session + transport | Same API across a remote boundary | You own transport/runtime deployment | +| Executor or mode | Runtime boundary | Tool bridge style | Main strengths | Main constraints | +| --------------------------------------- | ------------------------------------- | ------------------------------- | ------------------------------------------------ | ----------------------------------------------- | +| `QuickJsExecutor` | Fresh in-process QuickJS runtime | Shared runner callback | No native addon, simple install, default backend | Still in-process | +| `QuickJsExecutor` with `host: "worker"` | Worker thread + fresh QuickJS runtime | Shared host session + messages | Hard-stop worker termination, pooled by default | Still same OS process; ephemeral mode is slower | +| `RemoteExecutor` | App-defined remote transport boundary | Shared host session + transport | Same API across a remote boundary | You own transport/runtime deployment | ```mermaid flowchart LR @@ -19,24 +18,18 @@ flowchart LR REM["RemoteExecutor"] REMRT["Remote runner"] WQJS["QuickJsExecutor\nhost: worker"] - PQJS["QuickJsExecutor\nhost: process"] - PROCCHILD["Child process"] THREAD["Worker thread"] RUNNER["core runner semantics"] PROTO["@execbox/core/protocol
messages + host session"] WQJSRT["QuickJS runtime in worker"] - PQJSRT["QuickJS runtime in child process"] HOST --> QJS --> QJSRT HOST --> REM --> REMRT - HOST --> PQJS --> PROCCHILD --> PQJSRT HOST --> WQJS --> THREAD --> WQJSRT QJS --> RUNNER REM --> PROTO REMRT --> RUNNER - PQJS --> PROTO WQJS --> PROTO - PROCCHILD --> RUNNER THREAD --> RUNNER ``` @@ -47,7 +40,7 @@ flowchart LR That design gives QuickJS two useful properties: - the runtime semantics are centralized in one runner implementation -- the same guest/tool-call model can be reused behind hosted worker, hosted process, and remote transport boundaries +- the same guest/tool-call model can be reused behind hosted worker and remote transport boundaries ## Worker-Hosted QuickJS @@ -70,53 +63,29 @@ sequenceDiagram Exec-->>Host: ExecuteResult ``` -## Process-Hosted QuickJS - -`QuickJsExecutor` with `host: "process"` uses the same message-driven model as the worker host, but runs it behind a child-process boundary. It loads the same QuickJS session runner used by the inline QuickJS executor, reuses the same QuickJS protocol endpoint inside the child, and uses the shared `@execbox/core/protocol` host session on the parent side. By default it keeps a child-process shell warm between executions; `mode: "ephemeral"` switches to a fresh child process per execution. - -```mermaid -sequenceDiagram - participant Host as Host app - participant Exec as QuickJsExecutor(host=process) - participant Child as Child process - participant Runner as QuickJS runner - - Host->>Exec: execute(code, providers) - Exec->>Child: execute message + manifests - Child->>Runner: start QuickJS session - Runner-->>Exec: started - Runner-->>Exec: tool_call - Exec-->>Runner: tool_result - Runner-->>Exec: done - Exec-->>Host: ExecuteResult -``` - ## Timeout, Memory, and Abort Trade-offs The available executors expose the same public result shape, but they enforce limits differently. -| Concern | QuickJS inline | Remote | QuickJS host: process | QuickJS host: worker | -| ------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| Timeout | QuickJS interrupt/deadline handling | Shared host-session timeout + remote cancel + transport teardown | Shared host-session timeout + process cancellation + `SIGKILL` backstop | Shared host-session timeout + worker cancellation + worker termination backstop | -| Memory | QuickJS runtime memory limit | Remote runtime decides the hard boundary; execbox still forwards limits | QuickJS runtime memory limit inside the child process | QuickJS memory limit inside worker, optional worker resource limits as backstop | -| Abort to host tools | Abort signal passed through core callback | Abort signal passed through shared host session | Abort signal passed through shared host session | Abort signal passed through shared host session | -| Log capture | Captured inside runner | Captured inside the remote runner and returned over the transport boundary | Captured inside child-side QuickJS runner | Captured inside worker-side QuickJS runner | +| Concern | QuickJS inline | Remote | QuickJS host: worker | +| ------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Timeout | QuickJS interrupt/deadline handling | Shared host-session timeout + remote cancel + transport teardown | Shared host-session timeout + worker cancellation + worker termination backstop | +| Memory | QuickJS runtime memory limit | Remote runtime decides the hard boundary; execbox still forwards limits | QuickJS memory limit inside worker, optional worker resource limits as backstop | +| Abort to host tools | Abort signal passed through core callback | Abort signal passed through shared host session | Abort signal passed through shared host session | +| Log capture | Captured inside runner | Captured inside the remote runner and returned over the transport boundary | Captured inside worker-side QuickJS runner | ## Security and Operational Trade-offs - All executor modes provide defense-in-depth measures, not standalone hard hostile-code boundaries. - QuickJS is the easiest operational default and has the cleanest shared runtime story. - Remote execution keeps the same executor API while moving the runtime behind an app-defined boundary, but execbox deliberately does not ship the network stack for you. -- Process-hosted QuickJS gives a stronger lifecycle split than worker threads, but it is still not equivalent to a container or VM boundary. - Worker-hosted QuickJS improves lifecycle isolation and hard-stop behavior, but not process-level trust isolation. -- Worker and process executors share the same host-session and child-endpoint semantics, so most behavioral differences come from the transport boundary rather than from duplicated executor logic. ## Pooled QuickJS Shells The QuickJS-backed executors that benefit from pooling can keep the expensive outer shell warm without reusing guest runtime state: - `QuickJsExecutor` with `host: "worker"`: pooled-by-default reusable worker threads, with `mode: "ephemeral"` opt-out -- `QuickJsExecutor` with `host: "process"`: pooled-by-default reusable child processes, with `mode: "ephemeral"` opt-out Every `execute()` call still creates a fresh QuickJS runtime/context, reinjects providers, and discards guest globals afterward. Timeouts and internal transport failures evict the affected shell from the pool instead of returning it to circulation. @@ -125,8 +94,8 @@ Every `execute()` call still creates a fresh QuickJS runtime/context, reinjects Pooling is implemented at the host-shell layer, not at the QuickJS runtime layer. - `@execbox/core/protocol` exposes a small bounded async `createResourcePool()` helper that owns reusable shells, idle eviction, and `prewarm()` / `dispose()` support. -- Hosted `QuickJsExecutor` pools either `ChildProcess` or `Worker` shells. Each shell owns one long-lived transport wrapper plus one attached QuickJS protocol endpoint. -- The child/worker entrypoint only attaches `attachQuickJsProtocolEndpoint(...)` once. That endpoint accepts one active `execute` message at a time and starts a fresh `runQuickJsSession()` for each message. +- Hosted `QuickJsExecutor` pools `Worker` shells. Each shell owns one long-lived transport wrapper plus one attached QuickJS protocol endpoint. +- The worker entrypoint only attaches `attachQuickJsProtocolEndpoint(...)` once. That endpoint accepts one active `execute` message at a time and starts a fresh `runQuickJsSession()` for each message. - Concurrency therefore comes from pool size, not from multiplexing several executions through one shell. At execution time the flow is: @@ -151,7 +120,7 @@ If all shells are busy and the pool is already at `maxSize`, the next `acquire() ### Early Exit Handling -In pooled mode, a child or worker can exit before the host session subscribes to close events. The pooled transport wrappers retain the first close reason and replay it to later `onClose(...)` subscribers, so an early shell death still resolves as `internal_error` instead of hanging the execution. +In pooled mode, a worker can exit before the host session subscribes to close events. The pooled transport wrappers retain the first close reason and replay it to later `onClose(...)` subscribers, so an early shell death still resolves as `internal_error` instead of hanging the execution. ### What Is Not Pooled @@ -162,5 +131,4 @@ In pooled mode, a child or worker can exit before the host session subscribes to - Choose `QuickJsExecutor` when you want the default backend with the least operational friction. - Choose `RemoteExecutor` when you want the same execution API but need the runtime to live behind an application-defined transport boundary. -- Choose `QuickJsExecutor` with `host: "process"` when you want the QuickJS semantics behind a child-process boundary with a hard-kill timeout path and low-latency pooled reuse by default. - Choose `QuickJsExecutor` with `host: "worker"` when you want the QuickJS semantics off the main thread with a hard-stop termination path and low-latency pooled reuse by default. diff --git a/docs/architecture/execbox-mcp-and-protocol.md b/docs/architecture/execbox-mcp-and-protocol.md index 96b13ed..6cf1a35 100644 --- a/docs/architecture/execbox-mcp-and-protocol.md +++ b/docs/architecture/execbox-mcp-and-protocol.md @@ -45,8 +45,8 @@ It owns: - the `execute`, `cancel`, `started`, `tool_call`, `tool_result`, and `done` message types - the shared host transport session used by hosted `@execbox/quickjs` modes and `@execbox/remote` -- Node transport bootstrap helpers for worker and child-process execution -- the reusable async resource pool used by pooled worker and process shells +- Node transport bootstrap helpers for worker execution +- the reusable async resource pool used by pooled worker shells The architecture split is: @@ -57,10 +57,10 @@ The architecture split is: ## How The Packages Fit Together - `QuickJsExecutor` uses the shared runner semantics from `@execbox/core/runtime` directly. -- `QuickJsExecutor` in `host: "process"` and `host: "worker"` modes uses the shared host session from `@execbox/core/protocol` plus the shared QuickJS protocol endpoint inside the child or worker shell. +- `QuickJsExecutor` in `host: "worker"` mode uses the shared host session from `@execbox/core/protocol` plus the shared QuickJS protocol endpoint inside the worker shell. - `RemoteExecutor` uses that same host session across an app-owned transport boundary. - The runner side of a remote deployment attaches a runtime-owned endpoint adapter; `@execbox/quickjs/remote-endpoint` is the shipped QuickJS adapter. -- Pooled process and worker execution reuse only the outer host shell. Each `execute()` call still starts a fresh QuickJS runtime through the shared protocol endpoint. +- Pooled worker execution reuses only the outer host shell. Each `execute()` call still starts a fresh QuickJS runtime through the shared protocol endpoint. ```mermaid flowchart TB @@ -76,7 +76,7 @@ flowchart TB PROTO["@execbox/core/protocol
messages + host session + resource pool"] HOSTED["QuickJsExecutor host modes"] REM["RemoteExecutor"] - QJS_ENDPOINT["QuickJS protocol endpoint
worker/process side"] + QJS_ENDPOINT["QuickJS protocol endpoint
worker side"] REMOTE_ENDPOINT["Runtime-owned remote endpoint
QuickJS adapter is shipped"] end diff --git a/docs/examples.md b/docs/examples.md index 4f0ca5f..8e5840a 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -15,7 +15,6 @@ npm run examples | Example | What it shows | When to start here | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | [`execbox-basic.ts`](https://github.com/aallam/execbox/blob/main/examples/execbox-basic.ts) | Resolve a provider and execute guest code with QuickJS. | You want the smallest end-to-end example. | -| [`execbox-process.ts`](https://github.com/aallam/execbox/blob/main/examples/execbox-process.ts) | Run the same provider flow with QuickJS hosted in a child process. | You want a stronger lifecycle boundary than inline QuickJS. | | [`execbox-worker.ts`](https://github.com/aallam/execbox/blob/main/examples/execbox-worker.ts) | Run the same provider flow with QuickJS hosted in a worker thread. | You want QuickJS off the main thread without leaving the process. | | [`execbox-remote.ts`](https://github.com/aallam/execbox/blob/main/examples/execbox-remote.ts) | Run the same provider flow through a transport-backed executor. | You already own the transport/runtime boundary and want execbox to plug into it. | | [`execbox-mcp-provider.ts`](https://github.com/aallam/execbox/blob/main/examples/execbox-mcp-provider.ts) | Wrap MCP tools into a provider and execute against them. | You want guest code to call upstream MCP tools as code. | diff --git a/docs/getting-started.md b/docs/getting-started.md index e984d96..dd6df12 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -47,7 +47,6 @@ console.log(result); - Use `@execbox/quickjs` first unless you already know you need a separate runtime boundary. - Use `new QuickJsExecutor({ host: "worker" })` when you want QuickJS off the main thread with pooled workers. -- Use `new QuickJsExecutor({ host: "process" })` when you want QuickJS semantics in a separate child process. - Use `@execbox/remote` only when your runtime already lives behind an application-owned transport. ## Run the examples diff --git a/docs/index.md b/docs/index.md index 9731c81..809a59f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ features: - title: Code-first tool use details: Let agents write code against typed tool namespaces instead of serializing every intermediate step back through tool calls. - title: Execution boundary choice - details: Start with inline QuickJS, move to worker or child-process hosts, or push execution behind your own remote transport. + details: Start with inline QuickJS, move to worker hosts, or push execution behind your own remote transport. - title: MCP on both sides details: Wrap upstream MCP tools into guest namespaces and expose execbox execution back out as MCP tools for downstream clients. --- @@ -32,16 +32,15 @@ Execbox is a Node.js library for running guest JavaScript against host-defined t - where guest code runs - how calls cross boundaries -That split gives you one execution contract across inline QuickJS, hosted QuickJS, and remote transport-backed execution. +That split gives you one execution contract across inline QuickJS, worker-hosted QuickJS, and remote transport-backed execution. ## Choose a backend -| Backend | Package | Start here when | -| ---------------------- | ------------------ | -------------------------------------------------------------------------------- | -| In-process QuickJS | `@execbox/quickjs` | You want the easiest install and the default development path. | -| Worker-backed QuickJS | `@execbox/quickjs` | You want the runtime off the main thread with pooled worker reuse. | -| Process-backed QuickJS | `@execbox/quickjs` | You want a stronger lifecycle boundary and hard-kill timeout behavior. | -| Remote transport | `@execbox/remote` | You already own the transport/runtime boundary and want execbox to plug into it. | +| Backend | Package | Start here when | +| --------------------- | ------------------ | -------------------------------------------------------------------------------- | +| In-process QuickJS | `@execbox/quickjs` | You want the easiest install and the default development path. | +| Worker-backed QuickJS | `@execbox/quickjs` | You want the runtime off the main thread with pooled worker reuse. | +| Remote transport | `@execbox/remote` | You already own the transport/runtime boundary and want execbox to plug into it. | ## Security posture diff --git a/docs/performance/index.md b/docs/performance/index.md index 8dc5515..c5d1647 100644 --- a/docs/performance/index.md +++ b/docs/performance/index.md @@ -6,20 +6,18 @@ This page summarizes practical performance guidance for choosing and configuring Each executor makes a different trade-off between isolation strength, startup cost, and steady-state latency. -| Executor | Isolation | Relative Latency | Best For | -| ------------------------------------------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | -| `QuickJsExecutor` | In-process QuickJS runtime | Fastest steady-state path | Trusted code and the lowest possible latency | -| `QuickJsExecutor` (`host: "worker"`, pooled) | Worker thread shell with a fresh guest runtime per execution | Low latency after startup | General use when you want strong local isolation with good throughput | -| `QuickJsExecutor` (`host: "process"`, pooled) | Child-process shell with a fresh guest runtime per execution | Low latency after startup, but typically wider tails than worker pooled | Cases where process isolation matters more than tail latency | -| `QuickJsExecutor` (`host: "worker"`, ephemeral) | Fresh worker per execution | High startup cost | Per-execution host isolation when you accept much higher latency | -| `QuickJsExecutor` (`host: "process"`, ephemeral) | Fresh child process per execution | Highest startup cost | Strict per-execution process boundaries | -| `RemoteExecutor` | Network transport | Depends on the transport and remote host | Remote isolation, remote capacity, or remote scheduling | +| Executor | Isolation | Relative Latency | Best For | +| ----------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------- | --------------------------------------------------------------------- | +| `QuickJsExecutor` | In-process QuickJS runtime | Fastest steady-state path | Trusted code and the lowest possible latency | +| `QuickJsExecutor` (`host: "worker"`, pooled) | Worker thread shell with a fresh guest runtime per execution | Low latency after startup | General use when you want strong local isolation with good throughput | +| `QuickJsExecutor` (`host: "worker"`, ephemeral) | Fresh worker per execution | High startup cost | Per-execution host isolation when you accept much higher latency | +| `RemoteExecutor` | Network transport | Depends on the transport and remote host | Remote isolation, remote capacity, or remote scheduling | ## Practical Characteristics ### Pooled executors are the default hot-path choice -Hosted `QuickJsExecutor` modes both default to pooled mode. Pooling reuses the worker or process shell while still creating a fresh guest runtime for each execution. That keeps the host boundary in place without paying shell startup cost on every call. +Worker-hosted `QuickJsExecutor` defaults to pooled mode. Pooling reuses the worker shell while still creating a fresh guest runtime for each execution. That keeps the host boundary in place without paying worker startup cost on every call. If you care about request latency or throughput, start with pooled mode. Ephemeral mode is primarily an isolation choice, not a speed choice. @@ -29,18 +27,16 @@ Tool calls still pay for host dispatch, serialization, and the round-trip back i ### First execution is different from steady state -Fresh hosted executors are slower than steady-state pooled executors because they still need to stand up the host shell and the guest runtime. Use `prewarm()` on pooled worker or process hosts when you want those costs paid before traffic arrives; it warms both the outer shell and a real no-op guest execution path. +Fresh hosted executors are slower than steady-state pooled executors because they still need to stand up the host shell and the guest runtime. Use `prewarm()` on pooled worker hosts when you want those costs paid before traffic arrives; it warms both the outer shell and a real no-op guest execution path. ## Pool Sizing -Pool `maxSize` is the main throughput control for hosted worker and process executors. +Pool `maxSize` is the main throughput control for hosted worker executors. - `QuickJsExecutor` with `host: "worker"` starts with a CPU-aware default pool size based on available parallelism, capped at `4`. -- `QuickJsExecutor` with `host: "process"` keeps a conservative default pool size of `1`. - Size the pool to the concurrency you actually expect, not just to the default. - When requests outnumber available pooled shells, extra work queues behind the pool and latency rises quickly. - `QuickJsExecutor` with `host: "worker"` generally gives the best throughput and tighter tail latency for local execution workloads. -- `QuickJsExecutor` with `host: "process"` is still a valid choice when process isolation matters, but benchmark your own workload instead of assuming that a larger pool always helps. ```ts const executor = new QuickJsExecutor({ @@ -56,13 +52,12 @@ const executor = new QuickJsExecutor({ ## Choosing An Executor -| Priority | Recommended | -| ---------------------------------------------- | ------------------------------------------------------- | -| Lowest latency for trusted code | `QuickJsExecutor` | -| Best local balance of isolation and throughput | `QuickJsExecutor` with `host: "worker"` in pooled mode | -| Process isolation with warm shells | `QuickJsExecutor` with `host: "process"` in pooled mode | -| Fresh host boundary every execution | `QuickJsExecutor` with a hosted `ephemeral` mode | -| Remote isolation or remote capacity | `RemoteExecutor` | +| Priority | Recommended | +| ---------------------------------------------- | ------------------------------------------------------ | +| Lowest latency for trusted code | `QuickJsExecutor` | +| Best local balance of isolation and throughput | `QuickJsExecutor` with `host: "worker"` in pooled mode | +| Fresh host boundary every execution | `QuickJsExecutor` with a hosted `ephemeral` mode | +| Remote isolation or remote capacity | `RemoteExecutor` | For most applications, start with `QuickJsExecutor` and move to `host: "worker"` when you want stronger local isolation without leaving the package. diff --git a/docs/security.md b/docs/security.md index 4b67494..22162e4 100644 --- a/docs/security.md +++ b/docs/security.md @@ -25,14 +25,13 @@ Providers are explicit capability grants. If guest code can call a dangerous too ## Choosing the right boundary -| Need | Recommended path | -| --------------------------------------------------- | ----------------------------------------- | -| Lowest friction | `@execbox/quickjs` | -| Off-main-thread lifecycle isolation | `@execbox/quickjs` with `host: "worker"` | -| Stronger lifecycle split and hard-kill timeout path | `@execbox/quickjs` with `host: "process"` | -| Application-owned remote/runtime boundary | `@execbox/remote` | - -For hostile-code or multi-tenant deployments, prefer `@execbox/quickjs` with `host: "process"` or `@execbox/remote` behind a container, VM, or equivalent boundary that you control operationally. +| Need | Recommended path | +| ----------------------------------------- | ---------------------------------------- | +| Lowest friction | `@execbox/quickjs` | +| Off-main-thread lifecycle isolation | `@execbox/quickjs` with `host: "worker"` | +| Application-owned remote/runtime boundary | `@execbox/remote` | + +For hostile-code or multi-tenant deployments, prefer `@execbox/remote` behind a container, VM, or equivalent boundary that you control operationally. ## Deeper reading diff --git a/examples/README.md b/examples/README.md index b03311b..01b7625 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,6 @@ See the public [Examples docs](https://execbox.aallam.com/examples) for when to | File | What it shows | | ------------------------------------------------------ | ----------------------------------------------------------------- | | [`execbox-basic.ts`](./execbox-basic.ts) | Resolve a provider and execute guest code with QuickJS | -| [`execbox-process.ts`](./execbox-process.ts) | Run the same provider flow with QuickJS hosted in a child process | | [`execbox-remote.ts`](./execbox-remote.ts) | Run the same provider flow through a transport-backed executor | | [`execbox-worker.ts`](./execbox-worker.ts) | Run the same provider flow with QuickJS hosted in a worker thread | | [`execbox-mcp-provider.ts`](./execbox-mcp-provider.ts) | Wrap MCP tools into a provider and execute against them | diff --git a/examples/execbox-process.ts b/examples/execbox-process.ts deleted file mode 100644 index 8de4f47..0000000 --- a/examples/execbox-process.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { resolveProvider } from "@execbox/core"; -import { QuickJsExecutor } from "@execbox/quickjs"; - -async function main(): Promise { - const provider = resolveProvider({ - name: "tools", - tools: { - add: { - description: "Add two numbers together.", - inputSchema: { - type: "object", - required: ["x", "y"], - properties: { - x: { type: "number" }, - y: { type: "number" }, - }, - }, - execute: async (input) => { - const { x, y } = input as { x: number; y: number }; - return { sum: x + y }; - }, - }, - logValue: { - description: "Echo a message for console capture.", - inputSchema: { - type: "object", - required: ["message"], - properties: { - message: { type: "string" }, - }, - }, - execute: async (input) => { - const { message } = input as { message: string }; - return { message }; - }, - }, - }, - }); - - const executor = new QuickJsExecutor({ - host: "process", - }); - const result = await executor.execute( - ` - const sum = await tools.add({ x: 2, y: 5 }); - const echoed = await tools.logValue({ message: "captured from process sandbox" }); - console.log(echoed.message); - ({ sum: sum.sum }); - `, - [provider], - ); - - console.log("process example result"); - console.log(JSON.stringify(result, null, 2)); -} - -void main(); diff --git a/package.json b/package.json index 99279db..b8e122a 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,10 @@ "example:execbox": "node --import tsx examples/execbox-basic.ts", "example:execbox-mcp-provider": "node --import tsx examples/execbox-mcp-provider.ts", "example:execbox-mcp-server": "node --import tsx examples/execbox-mcp-server.ts", - "example:execbox-process": "node --import tsx examples/execbox-process.ts", "example:execbox-remote": "node --import tsx examples/execbox-remote.ts", "example:execbox-worker": "node --import tsx examples/execbox-worker.ts", "benchmark": "node --expose-gc --import tsx benchmarks/benchmark.ts", - "examples": "npm run example:execbox && npm run example:execbox-process && npm run example:execbox-remote && npm run example:execbox-worker && npm run example:execbox-mcp-provider && npm run example:execbox-mcp-server", + "examples": "npm run example:execbox && npm run example:execbox-remote && npm run example:execbox-worker && npm run example:execbox-mcp-provider && npm run example:execbox-mcp-server", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint . --ext .ts", @@ -35,7 +34,7 @@ "package:check": "npm_config_cache=$PWD/.npm-cache CI=1 npm run build", "test": "vitest run", "test:dist-smoke": "node --import tsx scripts/test-dist-smoke.ts", - "test:security": "node ./node_modules/vitest/vitest.mjs run packages/core/__tests__/security/isJsonSerializable.test.ts packages/core/__tests__/core/createToolCallDispatcher.test.ts packages/core/__tests__/protocol/hostSession.test.ts packages/quickjs/__tests__/protocolEndpoint.test.ts packages/quickjs/__tests__/remoteEndpoint.test.ts && node ./node_modules/vitest/vitest.mjs run packages/core/__tests__/mcp/penetration.test.ts packages/remote/__tests__/penetration.test.ts packages/quickjs/__tests__/hostedPenetration.test.ts && node ./node_modules/vitest/vitest.mjs run packages/quickjs/__tests__/processHostLifecycle.test.ts packages/quickjs/__tests__/workerHostLifecycle.test.ts", + "test:security": "node ./node_modules/vitest/vitest.mjs run packages/core/__tests__/security/isJsonSerializable.test.ts packages/core/__tests__/core/createToolCallDispatcher.test.ts packages/core/__tests__/protocol/hostSession.test.ts packages/quickjs/__tests__/protocolEndpoint.test.ts packages/quickjs/__tests__/remoteEndpoint.test.ts && node ./node_modules/vitest/vitest.mjs run packages/core/__tests__/mcp/penetration.test.ts packages/remote/__tests__/penetration.test.ts packages/quickjs/__tests__/hostedPenetration.test.ts && node ./node_modules/vitest/vitest.mjs run packages/quickjs/__tests__/workerHostLifecycle.test.ts", "test:watch": "vitest", "typecheck": "tsc --noEmit && node --import tsx scripts/check-workspace-entrypoints.ts" }, diff --git a/packages/core/README.md b/packages/core/README.md index 859fc40..0ef5182 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -18,7 +18,7 @@ Core execution contract for execbox. Use it to resolve host tools into callable | Package | Start here when | | -------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| [`@execbox/quickjs`](https://www.npmjs.com/package/@execbox/quickjs) | You want the default path with inline, worker, or process-hosted QuickJS. | +| [`@execbox/quickjs`](https://www.npmjs.com/package/@execbox/quickjs) | You want the default path with inline or worker-hosted QuickJS. | | [`@execbox/remote`](https://www.npmjs.com/package/@execbox/remote) | Your runtime already lives behind an application-owned transport boundary. | ## Install diff --git a/packages/core/src/protocol/hostSession.ts b/packages/core/src/protocol/hostSession.ts index 2d01d77..bd86dab 100644 --- a/packages/core/src/protocol/hostSession.ts +++ b/packages/core/src/protocol/hostSession.ts @@ -27,7 +27,7 @@ export interface TransportCloseReason { } /** - * Minimal parent-side transport contract for worker/process-backed execution. + * Minimal parent-side transport contract for transport-backed execution. */ export interface HostTransport { dispose(): Promise | void; diff --git a/packages/quickjs/README.md b/packages/quickjs/README.md index 5fc955f..33a5987 100644 --- a/packages/quickjs/README.md +++ b/packages/quickjs/README.md @@ -1,6 +1,6 @@ # @execbox/quickjs -Default execbox executor for most deployments. It runs guest JavaScript in QuickJS and lets you keep the same API as you move between inline, worker-hosted, and process-hosted execution. +Default execbox executor for most deployments. It runs guest JavaScript in QuickJS and lets you keep the same API as you move between inline and worker-hosted execution. [![npm version](https://img.shields.io/npm/v/%40execbox%2Fquickjs?style=flat-square)](https://www.npmjs.com/package/@execbox/quickjs) [![License](https://img.shields.io/github/license/aallam/execbox?style=flat-square)](https://github.com/aallam/execbox/blob/main/LICENSE) @@ -10,7 +10,7 @@ Default execbox executor for most deployments. It runs guest JavaScript in Quick - you want the default execbox path with the easiest setup - you do not want a native addon in local development or CI -- you want one package that can stay inline, move off-thread, or move into a child process later +- you want one package that can stay inline or move off-thread later ## Install @@ -45,11 +45,10 @@ console.log(result); `QuickJsExecutor` keeps the same execution API while changing where the runtime lives: -| Mode | Use it when | -| ----------------- | --------------------------------------------------------------------------- | -| Inline (default) | You want the lowest-friction development path. | -| `host: "worker"` | You want QuickJS off the main thread with pooled worker shells. | -| `host: "process"` | You want QuickJS hosted in a child process with a stronger lifecycle split. | +| Mode | Use it when | +| ---------------- | --------------------------------------------------------------- | +| Inline (default) | You want the lowest-friction development path. | +| `host: "worker"` | You want QuickJS off the main thread with pooled worker shells. | ```ts const executor = new QuickJsExecutor({ @@ -66,14 +65,14 @@ await executor.prewarm(); ## Advanced Imports - `@execbox/quickjs/runner` exports the reusable QuickJS runner -- `@execbox/quickjs/runner/protocol-endpoint` exports the low-level QuickJS protocol loop used by hosted worker/process integrations +- `@execbox/quickjs/runner/protocol-endpoint` exports the low-level QuickJS protocol loop used by hosted worker integrations - `@execbox/quickjs/remote-endpoint` adapts the QuickJS protocol loop to `@execbox/remote` runner ports ## Operational Notes - Each execution gets a fresh QuickJS runtime with JSON-only tool and result boundaries. - This package is the default deployment path, not a hard security boundary for hostile or multi-tenant code. -- If you need a stronger boundary, prefer `host: "process"` or move execution behind `@execbox/remote`. +- If you need a stronger deployment boundary, move execution behind `@execbox/remote`. ## Read Next diff --git a/packages/quickjs/__tests__/hostedPenetration.test.ts b/packages/quickjs/__tests__/hostedPenetration.test.ts index cccf639..f124be3 100644 --- a/packages/quickjs/__tests__/hostedPenetration.test.ts +++ b/packages/quickjs/__tests__/hostedPenetration.test.ts @@ -10,13 +10,3 @@ runWrappedMcpPenetrationSuite( }); }, ); - -runWrappedMcpPenetrationSuite( - "QuickJsExecutor process host wrapped MCP", - (options) => { - return new QuickJsExecutor({ - ...options, - host: "process", - }); - }, -); diff --git a/packages/quickjs/__tests__/processHostLifecycle.test.ts b/packages/quickjs/__tests__/processHostLifecycle.test.ts deleted file mode 100644 index 411646f..0000000 --- a/packages/quickjs/__tests__/processHostLifecycle.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { EventEmitter } from "node:events"; - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -class FakeChildProcess extends EventEmitter { - connected = true; - - kill = vi.fn(() => true); - send = vi.fn(); - - disconnect(): void { - this.connected = false; - this.emit("disconnect"); - } -} - -const state = vi.hoisted(() => ({ - autoExitOnStart: true, - child: undefined as FakeChildProcess | undefined, - options: undefined as Record | undefined, -})); - -vi.mock("node:child_process", () => ({ - fork: vi.fn( - (_path: string, _args: string[], options?: Record) => { - const child = new FakeChildProcess(); - state.options = options; - state.child = child; - if (state.autoExitOnStart) { - queueMicrotask(() => { - child.emit("exit", 17, null); - }); - } - return child; - }, - ), -})); - -describe("QuickJsExecutor process host lifecycle", () => { - afterEach(() => { - vi.useRealTimers(); - }); - - beforeEach(() => { - state.autoExitOnStart = true; - state.child = undefined; - state.options = undefined; - }); - - it("returns internal_error when the child exits before sending a result", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ host: "process" }); - - const result = await executor.execute("1 + 1", []); - - expect(result).toMatchObject({ - error: { - code: "internal_error", - message: "Child process exited unexpectedly with code 17", - }, - ok: false, - }); - }); - - it("uses explicit source bootstrap conditions in repo source mode", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ host: "process" }); - - await executor.execute("1 + 1", []); - - expect(state.options).toMatchObject({ - execArgv: expect.arrayContaining([ - "--conditions=source", - "--import", - "tsx", - ]), - }); - }); - - it("force-kills a silent child process only once when execution times out", async () => { - vi.useFakeTimers(); - state.autoExitOnStart = false; - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - cancelGraceMs: 0, - timeoutMs: 10, - }); - - const resultPromise = executor.execute("while (true) {}", []); - await vi.advanceTimersByTimeAsync(200); - const result = await resultPromise; - - expect(result).toMatchObject({ - error: { - code: "timeout", - }, - ok: false, - }); - expect(state.child?.kill).toHaveBeenCalledTimes(1); - expect(state.child?.kill).toHaveBeenCalledWith("SIGKILL"); - }); - - it("does not create a child process when the caller signal is already aborted", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ host: "process" }); - const controller = new AbortController(); - controller.abort(); - - const result = await executor.execute("1 + 1", [], { - signal: controller.signal, - }); - - expect(result).toMatchObject({ - error: { - code: "timeout", - }, - ok: false, - }); - expect(state.child).toBeUndefined(); - }); - - it("force-kills a silent child process only once when the caller aborts", async () => { - vi.useFakeTimers(); - state.autoExitOnStart = false; - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - cancelGraceMs: 0, - timeoutMs: 1_000, - }); - const controller = new AbortController(); - - const resultPromise = executor.execute("while (true) {}", [], { - signal: controller.signal, - }); - controller.abort(); - await vi.advanceTimersByTimeAsync(0); - const result = await resultPromise; - - expect(result).toMatchObject({ - error: { - code: "timeout", - }, - ok: false, - }); - expect(state.child?.kill).toHaveBeenCalledTimes(1); - expect(state.child?.kill).toHaveBeenCalledWith("SIGKILL"); - }); -}); diff --git a/packages/quickjs/__tests__/processHostPooling.test.ts b/packages/quickjs/__tests__/processHostPooling.test.ts deleted file mode 100644 index 926fc73..0000000 --- a/packages/quickjs/__tests__/processHostPooling.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { EventEmitter } from "node:events"; - -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { ExecuteResult } from "@execbox/core"; - -class FakeChildProcess extends EventEmitter { - connected = true; - - kill = vi.fn(() => true); - send = vi.fn(); -} - -const state = vi.hoisted(() => ({ - blockExecutions: false, - blockedResolvers: [] as Array<() => void>, - children: [] as FakeChildProcess[], - results: [] as ExecuteResult[], - sessions: [] as Array<{ - code: string; - providersCount: number; - transport: unknown; - }>, -})); - -vi.mock("node:child_process", () => ({ - fork: vi.fn(() => { - const child = new FakeChildProcess(); - state.children.push(child); - return child; - }), -})); - -vi.mock("@execbox/core/protocol", async (importOriginal) => { - const actual = (await importOriginal()) as Record; - - return { - ...actual, - getNodeTransportExecArgv: vi.fn(() => []), - runHostTransportSession: vi.fn(async (options) => { - state.sessions.push({ - code: options.code, - providersCount: options.providers.length, - transport: options.transport, - }); - if (state.blockExecutions) { - await new Promise((resolve) => { - state.blockedResolvers.push(resolve); - }); - } - const result = state.results.shift() ?? { - durationMs: 0, - logs: [], - ok: true, - result: 1, - }; - await Promise.resolve(options.onSettled?.(result)); - await Promise.resolve(options.transport.dispose()); - return result; - }), - }; -}); - -describe("QuickJsExecutor process host pooling", () => { - beforeEach(() => { - state.blockExecutions = false; - state.blockedResolvers = []; - state.children = []; - state.results = []; - state.sessions = []; - }); - - it("reuses one child process for sequential executions by default", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ host: "process" }); - - await executor.execute("1 + 1", []); - await executor.execute("1 + 1", []); - - expect(state.children).toHaveLength(1); - }); - - it("uses a fresh child process per execution in ephemeral mode", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - mode: "ephemeral", - pool: { maxSize: 1 }, - }); - - await executor.execute("1 + 1", []); - await executor.execute("1 + 1", []); - - expect(state.children).toHaveLength(2); - }); - - it("runs real warmup sessions before the first pooled execution", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - pool: { maxSize: 2, prewarm: 2 }, - }); - - await executor.execute("1 + 1", []); - - expect(state.children).toHaveLength(2); - expect(state.sessions).toHaveLength(3); - expect(state.sessions[0]).toMatchObject({ - code: "undefined", - providersCount: 0, - }); - expect(state.sessions[1]).toMatchObject({ - code: "undefined", - providersCount: 0, - }); - expect(state.sessions[2]).toMatchObject({ - code: "1 + 1", - providersCount: 0, - }); - }); - - it("rejects failed explicit prewarm and evicts the broken child", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - pool: { maxSize: 1 }, - }); - state.results = [ - { - durationMs: 0, - error: { - code: "internal_error", - message: "warmup failed", - }, - logs: [], - ok: false, - }, - { - durationMs: 0, - logs: [], - ok: true, - result: 1, - }, - ]; - - await expect(executor.prewarm(1)).rejects.toThrow("warmup failed"); - await executor.execute("1 + 1", []); - - expect(state.children).toHaveLength(2); - expect(state.sessions).toHaveLength(2); - expect(state.sessions[0]?.code).toBe("undefined"); - expect(state.sessions[1]?.code).toBe("1 + 1"); - }); - - it("evicts a pooled child after a timeout result", async () => { - const { QuickJsExecutor } = await import("../src/index"); - const executor = new QuickJsExecutor({ - host: "process", - pool: { maxSize: 1 }, - }); - state.results = [ - { - durationMs: 0, - error: { - code: "timeout", - message: "Execution timed out", - }, - logs: [], - ok: false, - }, - { - durationMs: 0, - logs: [], - ok: true, - result: 1, - }, - ]; - - await executor.execute("1 + 1", []); - await executor.execute("1 + 1", []); - - expect(state.children).toHaveLength(2); - }); -}); diff --git a/packages/quickjs/__tests__/quickjsExecutor.test.ts b/packages/quickjs/__tests__/quickjsExecutor.test.ts index ca01340..4e8c16e 100644 --- a/packages/quickjs/__tests__/quickjsExecutor.test.ts +++ b/packages/quickjs/__tests__/quickjsExecutor.test.ts @@ -1,6 +1,16 @@ import { QuickJsExecutor } from "@execbox/quickjs"; import { runExecutorContractSuite } from "../../core/test-support/runExecutorContractSuite"; +describe("QuickJsExecutor host selection", () => { + it("rejects the removed process host at runtime", () => { + expect( + () => new QuickJsExecutor({ host: "process" } as never), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: QuickJsExecutor host "process" is no longer supported. Use host "worker" for local hosted execution, or @execbox/remote for process, container, or VM boundaries.]`, + ); + }); +}); + runExecutorContractSuite( "QuickJsExecutor", (options) => new QuickJsExecutor(options), @@ -15,13 +25,3 @@ runExecutorContractSuite( }), { supportsPooling: true }, ); - -runExecutorContractSuite( - "QuickJsExecutor (process host)", - (options) => - new QuickJsExecutor({ - ...options, - host: "process", - }), - { supportsPooling: true }, -); diff --git a/packages/quickjs/etc/execbox-quickjs-runner.api.md b/packages/quickjs/etc/execbox-quickjs-runner.api.md index 1859464..a36cd38 100644 --- a/packages/quickjs/etc/execbox-quickjs-runner.api.md +++ b/packages/quickjs/etc/execbox-quickjs-runner.api.md @@ -13,10 +13,10 @@ import { ToolCall } from '@execbox/core'; import { ToolCallResult } from '@execbox/core'; // @public -export type QuickJsExecutorHost = "inline" | "worker" | "process"; +export type QuickJsExecutorHost = "inline" | "worker"; // @public -export type QuickJsExecutorOptions = QuickJsInlineExecutorOptions | QuickJsWorkerExecutorOptions | QuickJsProcessExecutorOptions; +export type QuickJsExecutorOptions = QuickJsInlineExecutorOptions | QuickJsWorkerExecutorOptions; // @public export type QuickJsHostedMode = "pooled" | "ephemeral"; @@ -27,14 +27,6 @@ export interface QuickJsInlineExecutorOptions extends ExecutorRuntimeOptions { loadModule?: () => Promise | unknown; } -// @public -export interface QuickJsProcessExecutorOptions extends ExecutorRuntimeOptions { - cancelGraceMs?: number; - host: "process"; - mode?: QuickJsHostedMode; - pool?: ExecutorPoolOptions; -} - // @public export type QuickJsSessionOptions = ExecutorRuntimeOptions & Pick & { module?: QuickJSWASMModule; diff --git a/packages/quickjs/etc/execbox-quickjs.api.md b/packages/quickjs/etc/execbox-quickjs.api.md index 41fc66e..926150e 100644 --- a/packages/quickjs/etc/execbox-quickjs.api.md +++ b/packages/quickjs/etc/execbox-quickjs.api.md @@ -20,10 +20,10 @@ export class QuickJsExecutor implements Executor { } // @public -export type QuickJsExecutorHost = "inline" | "worker" | "process"; +export type QuickJsExecutorHost = "inline" | "worker"; // @public -export type QuickJsExecutorOptions = QuickJsInlineExecutorOptions | QuickJsWorkerExecutorOptions | QuickJsProcessExecutorOptions; +export type QuickJsExecutorOptions = QuickJsInlineExecutorOptions | QuickJsWorkerExecutorOptions; // @public export type QuickJsHostedMode = "pooled" | "ephemeral"; @@ -34,14 +34,6 @@ export interface QuickJsInlineExecutorOptions extends ExecutorRuntimeOptions { loadModule?: () => Promise | unknown; } -// @public -export interface QuickJsProcessExecutorOptions extends ExecutorRuntimeOptions { - cancelGraceMs?: number; - host: "process"; - mode?: QuickJsHostedMode; - pool?: ExecutorPoolOptions; -} - // @public export interface QuickJsWorkerExecutorOptions extends ExecutorRuntimeOptions { cancelGraceMs?: number; diff --git a/packages/quickjs/package.json b/packages/quickjs/package.json index 6fe5665..7294057 100644 --- a/packages/quickjs/package.json +++ b/packages/quickjs/package.json @@ -1,7 +1,7 @@ { "name": "@execbox/quickjs", "version": "0.5.0", - "description": "QuickJS executor for the execbox core package across inline, worker, and process hosts.", + "description": "QuickJS executor for the execbox core package across inline and worker hosts.", "license": "MIT", "type": "module", "engines": { diff --git a/packages/quickjs/src/hosted/processHostedExecutor.ts b/packages/quickjs/src/hosted/processHostedExecutor.ts deleted file mode 100644 index debcfc3..0000000 --- a/packages/quickjs/src/hosted/processHostedExecutor.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { fork, type ChildProcess } from "node:child_process"; -import { fileURLToPath } from "node:url"; - -import { - createResourcePool, - getNodeTransportExecArgv, - type HostTransport, - type ResourcePool, - type RunnerMessage, - type TransportCloseReason, -} from "@execbox/core/protocol"; -import { createTimeoutExecuteResult } from "@execbox/core/runtime"; -import type { - ExecutionOptions, - Executor, - ExecutorPoolOptions, - ExecuteResult, - ResolvedToolProvider, -} from "@execbox/core"; - -import type { QuickJsProcessExecutorOptions } from "../types.ts"; -import { - DEFAULT_CANCEL_GRACE_MS, - DEFAULT_POOL_OPTIONS, - createBorrowedTransport, - getPrewarmCount, - getWarmupTarget, - isReusableResult, - runHostedTransportSession, - warmHostedPool, -} from "./shared.ts"; - -interface ProcessShell { - child: ChildProcess; - transport: HostTransport; -} - -function resolveProcessEntryPath(): string { - const extension = import.meta.url.endsWith(".ts") ? ".ts" : ".js"; - return fileURLToPath(new URL(`../processEntry${extension}`, import.meta.url)); -} - -function createUnexpectedExitMessage( - code: number | null, - signal: NodeJS.Signals | null, -): string { - if (code !== null) { - return `Child process exited unexpectedly with code ${code}`; - } - - if (signal) { - return `Child process exited unexpectedly with signal ${signal}`; - } - - return "Child process exited unexpectedly"; -} - -function createChildProcess(): ChildProcess { - return fork(resolveProcessEntryPath(), [], { - execArgv: getNodeTransportExecArgv(import.meta.url), - stdio: ["ignore", "ignore", "ignore", "ipc"], - }); -} - -function createProcessTransport(child: ChildProcess): HostTransport { - let terminated = false; - let closeReason: TransportCloseReason | undefined; - const closeHandlers = new Set<(reason?: TransportCloseReason) => void>(); - const errorHandlers = new Set<(error: Error) => void>(); - const messageHandlers = new Set<(message: RunnerMessage) => void>(); - - const terminateChild = () => { - if (terminated) { - return; - } - - terminated = true; - child.kill("SIGKILL"); - }; - - const notifyClose = (reason: TransportCloseReason) => { - if (closeReason) { - return; - } - - closeReason = reason; - for (const handler of closeHandlers) { - handler(reason); - } - }; - - const onDisconnect = () => { - notifyClose({ - message: "Child process disconnected unexpectedly", - }); - }; - const onExit = (code: number | null, signal: NodeJS.Signals | null) => { - notifyClose({ - code, - message: createUnexpectedExitMessage(code, signal), - signal, - }); - }; - const onError = (error: Error) => { - for (const handler of errorHandlers) { - handler(error); - } - }; - const onMessage = (message: unknown) => { - for (const handler of messageHandlers) { - handler(message as RunnerMessage); - } - }; - - child.on("disconnect", onDisconnect); - child.on("exit", onExit); - child.on("error", onError); - child.on("message", onMessage); - - return { - dispose: () => { - child.off("disconnect", onDisconnect); - child.off("exit", onExit); - child.off("error", onError); - child.off("message", onMessage); - terminateChild(); - }, - onClose: (handler) => { - closeHandlers.add(handler); - if (closeReason) { - queueMicrotask(() => { - if (closeHandlers.has(handler)) { - handler(closeReason); - } - }); - } - return () => { - closeHandlers.delete(handler); - }; - }, - onError: (handler) => { - errorHandlers.add(handler); - return () => errorHandlers.delete(handler); - }, - onMessage: (handler) => { - messageHandlers.add(handler); - return () => messageHandlers.delete(handler); - }, - send: (message) => - new Promise((resolve, reject) => { - if (!child.connected || typeof child.send !== "function") { - reject(new Error("Child process disconnected unexpectedly")); - return; - } - - child.send(message, (error) => { - if (error) { - reject(error); - return; - } - - resolve(); - }); - }), - terminate: () => { - terminateChild(); - }, - }; -} - -function createProcessShell(): ProcessShell { - const child = createChildProcess(); - return { - child, - transport: createProcessTransport(child), - }; -} - -function resolvePoolOptions( - options: QuickJsProcessExecutorOptions, -): ExecutorPoolOptions | undefined { - if (options.mode === "ephemeral") { - return undefined; - } - - return { - ...DEFAULT_POOL_OPTIONS, - ...options.pool, - }; -} - -/** - * Child-process executor that runs guest code inside a dedicated QuickJS runtime per call. - */ -export class ProcessHostedQuickJsExecutor implements Executor { - private readonly cancelGraceMs: number; - private readonly options: QuickJsProcessExecutorOptions; - private readonly pool: ResourcePool | undefined; - private readonly poolOptions: ExecutorPoolOptions | undefined; - private readonly warmup: Promise | undefined; - - /** - * Creates a hosted QuickJS executor that launches child-process shells on demand. - */ - constructor(options: QuickJsProcessExecutorOptions) { - this.cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS; - this.options = options; - const poolOptions = resolvePoolOptions(options); - this.poolOptions = poolOptions; - - if (poolOptions) { - this.pool = createResourcePool({ - create: async () => createProcessShell(), - destroy: async (shell) => { - await shell.transport.dispose(); - }, - idleTimeoutMs: poolOptions.idleTimeoutMs, - maxSize: poolOptions.maxSize, - minSize: poolOptions.minSize, - }); - const prewarmCount = getPrewarmCount(poolOptions); - if (prewarmCount > 0) { - this.warmup = this.warmPool(prewarmCount); - } - } - } - - /** - * Disposes any pooled child-process shells owned by this executor. - */ - async dispose(): Promise { - await this.pool?.dispose(); - } - - /** - * Prewarms pooled child-process shells up to the requested count. - */ - async prewarm(count?: number): Promise { - if (!this.pool || !this.poolOptions) { - return; - } - - const target = getWarmupTarget(count, this.poolOptions); - if (target <= 0) { - return; - } - - await this.warmPool(target); - } - - private async runTransportSession( - transport: HostTransport, - code: string, - providers: ResolvedToolProvider[], - options: ExecutionOptions = {}, - onSettled?: (result: ExecuteResult) => Promise | void, - ): Promise { - return await runHostedTransportSession({ - cancelGraceMs: this.cancelGraceMs, - code, - executorOptions: this.options, - onSettled, - providers, - requestOptions: options, - transport, - }); - } - - private async warmPool(count: number): Promise { - if (!this.pool) { - return; - } - - await this.pool.prewarm(count); - - const leases: Array<{ - release: (reusable: boolean) => Promise; - value: ProcessShell; - }> = []; - try { - for (let index = 0; index < count; index += 1) { - leases.push(await this.pool.acquire()); - } - } catch (error) { - await Promise.allSettled( - leases.map(async (lease) => await lease.release(false)), - ); - throw error; - } - - await warmHostedPool({ - count, - getTransport: (lease) => lease.value.transport, - label: "child process", - onRelease: async (lease, reusable) => await lease.release(reusable), - runSession: async (transport, code, providers) => - await this.runTransportSession(transport, code, providers), - shells: leases, - }); - } - - /** - * Executes guest code in a child-process-hosted QuickJS shell. - */ - async execute( - code: string, - providers: ResolvedToolProvider[], - options: ExecutionOptions = {}, - ): Promise { - if (options.signal?.aborted) { - return createTimeoutExecuteResult(); - } - - await this.warmup; - if (this.pool) { - const lease = await this.pool.acquire(); - - return await this.runTransportSession( - createBorrowedTransport(lease.value.transport), - code, - providers, - options, - async (result) => { - await lease.release(isReusableResult(result)); - }, - ); - } - - let child: ChildProcess; - - try { - child = createChildProcess(); - } catch (error) { - return { - durationMs: 0, - error: { - code: "internal_error", - message: error instanceof Error ? error.message : String(error), - }, - logs: [], - ok: false, - }; - } - - return await this.runTransportSession( - createProcessTransport(child), - code, - providers, - options, - ); - } -} diff --git a/packages/quickjs/src/index.ts b/packages/quickjs/src/index.ts index 546445f..a3add47 100644 --- a/packages/quickjs/src/index.ts +++ b/packages/quickjs/src/index.ts @@ -8,7 +8,6 @@ export type { QuickJsExecutorOptions, QuickJsHostedMode, QuickJsInlineExecutorOptions, - QuickJsProcessExecutorOptions, QuickJsWorkerExecutorOptions, WorkerResourceLimits, } from "./types"; diff --git a/packages/quickjs/src/processEntry.ts b/packages/quickjs/src/processEntry.ts deleted file mode 100644 index eb23b58..0000000 --- a/packages/quickjs/src/processEntry.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DispatcherMessage, RunnerMessage } from "@execbox/core/protocol"; - -import { attachQuickJsProtocolEndpoint } from "./runner/protocolEndpoint.ts"; - -if (typeof process.send !== "function") { - throw new Error( - "QuickJsExecutor process host requires a child process IPC channel", - ); -} - -attachQuickJsProtocolEndpoint({ - onMessage(handler: (message: DispatcherMessage) => void): () => void { - process.on("message", handler); - return () => process.off("message", handler); - }, - send(message: RunnerMessage): void { - process.send?.(message); - }, -}); diff --git a/packages/quickjs/src/quickjsExecutor.ts b/packages/quickjs/src/quickjsExecutor.ts index d7084db..88aaab3 100644 --- a/packages/quickjs/src/quickjsExecutor.ts +++ b/packages/quickjs/src/quickjsExecutor.ts @@ -10,13 +10,11 @@ import type { ResolvedToolProvider, } from "@execbox/core"; -import { ProcessHostedQuickJsExecutor } from "./hosted/processHostedExecutor.ts"; import { WorkerHostedQuickJsExecutor } from "./hosted/workerHostedExecutor.ts"; import { runQuickJsSession } from "./runner/index.ts"; import type { QuickJsExecutorOptions, QuickJsInlineExecutorOptions, - QuickJsProcessExecutorOptions, QuickJsWorkerExecutorOptions, } from "./types"; @@ -26,14 +24,19 @@ function isWorkerOptions( return options.host === "worker"; } -function isProcessOptions( +function getUnsupportedHost( options: QuickJsExecutorOptions, -): options is QuickJsProcessExecutorOptions { - return options.host === "process"; +): string | undefined { + const host = (options as { host?: unknown }).host; + if (host === undefined || host === "inline" || host === "worker") { + return undefined; + } + + return String(host); } /** - * QuickJS-backed executor for inline, worker-backed, or process-backed JavaScript runs. + * QuickJS-backed executor for inline or worker-backed JavaScript runs. */ export class QuickJsExecutor implements Executor { private readonly hostedExecutor: Executor | undefined; @@ -41,16 +44,20 @@ export class QuickJsExecutor implements Executor { /** * Creates a QuickJS executor with inline QuickJS by default, or a hosted - * worker/process shell when `host` is explicitly set. + * worker shell when `host` is explicitly set. */ constructor(options: QuickJsExecutorOptions = {}) { - if (isWorkerOptions(options)) { - this.hostedExecutor = new WorkerHostedQuickJsExecutor(options); - return; + const unsupportedHost = getUnsupportedHost(options); + if (unsupportedHost !== undefined) { + throw new Error( + `QuickJsExecutor host "${unsupportedHost}" is no longer supported. ` + + 'Use host "worker" for local hosted execution, or @execbox/remote ' + + "for process, container, or VM boundaries.", + ); } - if (isProcessOptions(options)) { - this.hostedExecutor = new ProcessHostedQuickJsExecutor(options); + if (isWorkerOptions(options)) { + this.hostedExecutor = new WorkerHostedQuickJsExecutor(options); return; } @@ -65,8 +72,8 @@ export class QuickJsExecutor implements Executor { } /** - * Prewarms pooled hosted shells when the executor is running in worker or - * process mode. Inline mode treats this as a no-op. + * Prewarms pooled hosted shells when the executor is running in worker mode. + * Inline mode treats this as a no-op. */ async prewarm(count?: number): Promise { await this.hostedExecutor?.prewarm?.(count); diff --git a/packages/quickjs/src/runner/index.ts b/packages/quickjs/src/runner/index.ts index 4a11ec4..5a14ca2 100644 --- a/packages/quickjs/src/runner/index.ts +++ b/packages/quickjs/src/runner/index.ts @@ -48,7 +48,6 @@ export type { QuickJsExecutorOptions, QuickJsHostedMode, QuickJsInlineExecutorOptions, - QuickJsProcessExecutorOptions, QuickJsWorkerExecutorOptions, WorkerResourceLimits, } from "../types.ts"; diff --git a/packages/quickjs/src/runner/protocolEndpoint.ts b/packages/quickjs/src/runner/protocolEndpoint.ts index ca08989..fd9bbba 100644 --- a/packages/quickjs/src/runner/protocolEndpoint.ts +++ b/packages/quickjs/src/runner/protocolEndpoint.ts @@ -15,7 +15,7 @@ import type { ToolCallResult } from "@execbox/core"; import { runQuickJsSession } from "./index.ts"; /** - * Minimal worker/process-side port used by the shared QuickJS protocol endpoint. + * Minimal worker-side port used by the shared QuickJS protocol endpoint. */ export interface QuickJsProtocolPort { onMessage(handler: (message: DispatcherMessage) => void): void | (() => void); @@ -23,7 +23,7 @@ export interface QuickJsProtocolPort { } /** - * Attaches the shared QuickJS protocol loop to a worker/process messaging port. + * Attaches the shared QuickJS protocol loop to a worker messaging port. */ export function attachQuickJsProtocolEndpoint( port: QuickJsProtocolPort, diff --git a/packages/quickjs/src/types.ts b/packages/quickjs/src/types.ts index 5ce28c8..fe52215 100644 --- a/packages/quickjs/src/types.ts +++ b/packages/quickjs/src/types.ts @@ -6,7 +6,7 @@ import type { /** * Host boundary options available to the QuickJS executor. */ -export type QuickJsExecutorHost = "inline" | "worker" | "process"; +export type QuickJsExecutorHost = "inline" | "worker"; /** * Lifecycle modes for hosted QuickJS shells. @@ -56,27 +56,9 @@ export interface QuickJsWorkerExecutorOptions extends ExecutorRuntimeOptions { workerResourceLimits?: WorkerResourceLimits; } -/** - * Options for constructing a process-backed QuickJS executor. - */ -export interface QuickJsProcessExecutorOptions extends ExecutorRuntimeOptions { - /** Uses a child process to host each QuickJS runtime. */ - host: "process"; - - /** Time to wait before forcefully tearing down a hung hosted shell. */ - cancelGraceMs?: number; - - /** Whether to reuse hosted shells or spawn a fresh one per execution. */ - mode?: QuickJsHostedMode; - - /** Pool sizing and idle-eviction settings for pooled hosted shells. */ - pool?: ExecutorPoolOptions; -} - /** * Options for constructing a QuickJS executor. */ export type QuickJsExecutorOptions = | QuickJsInlineExecutorOptions - | QuickJsWorkerExecutorOptions - | QuickJsProcessExecutorOptions; + | QuickJsWorkerExecutorOptions; diff --git a/packages/quickjs/tsdown.config.ts b/packages/quickjs/tsdown.config.ts index 17686ca..7dc6c46 100644 --- a/packages/quickjs/tsdown.config.ts +++ b/packages/quickjs/tsdown.config.ts @@ -3,7 +3,6 @@ import { definePackageBuildConfig } from "../../scripts/tsdown-config.ts"; export default definePackageBuildConfig({ entry: [ "src/index.ts", - "src/processEntry.ts", "src/remoteEndpoint.ts", "src/runner/index.ts", "src/runner/protocolEndpoint.ts",