Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const workspace = new Workspace({
loader: env.LOADER,
root: "/workspace",
access: "read-write",
defaultTimeoutMs: 10_000,
maxTimeoutMs: 30_000,
defaultTimeoutMs: 60_000,
maxTimeoutMs: 180_000,
globalOutbound: null,
modules: {
"math-kit": `export const double = value => value * 2;`,
Expand Down Expand Up @@ -81,16 +81,45 @@ Workspace parses the graph before loading the Worker, confines every durable pat

## Execution limits and retention

The backend admits one execution at a time by default. A concurrent start fails with `EEXEC_BUSY` instead of creating an unbounded number of Dynamic Workers. Set `maxConcurrentExecutions` only after measuring the Durable Object and Worker Loader limits for the deployment.
The backend admits up to twenty-four executions at a time by default. A concurrent start past that ceiling fails with `EEXEC_BUSY` instead of creating an unbounded number of Dynamic Workers. Adjust `maxConcurrentExecutions` after measuring the Durable Object and Worker Loader limits for the deployment.

Each execution also bounds log events, active event subscribers, directory entries per read, concurrent and total capability calls, and cumulative capability request and response bytes. The corresponding `maxLogEvents`, `maxExecutionSubscribers`, `maxDirectoryEntries`, and `max*Capability*` options may be lowered for public workloads. Directory reads apply their limit in SQLite before materializing rows. Requests are checked inside the isolate before Workers RPC and again by the host.

Completed execution records remain available for replay for five minutes by default. The backend also keeps at most 100 completed records. Configure these bounds with `retentionMs` and `maxRetainedExecutions`. Completed records leave the in-memory active set immediately; replay reads them from SQLite.
Completed execution records remain available for replay for sixty minutes by default. The backend also keeps at most 100 completed records. Configure these bounds with `retentionMs` and `maxRetainedExecutions`. Completed records leave the in-memory active set immediately; replay reads them from SQLite.

Cancellation stops new host capability calls, disposes the Dynamic Worker, and waits for host calls that were already accepted. Exit 130 is published only after those calls settle. Normal completion uses the same drain rule, so an unawaited capability call cannot mutate the workspace after exit 0.

Host calls have a caller-visible deadline, controlled by `maxHostCallMs` and defaulting to `maxTimeoutMs`. Missing the deadline fails the capability call and marks the execution failed, even if caller code catches that error. Execution still waits for the accepted host operation itself before publishing a terminal event because many host APIs cannot roll back an external side effect after dispatch. Trusted modules receive an optional `{ signal, deadline }` context and must stop promptly when the signal aborts. A trusted module that ignores cancellation and never settles will keep execution in its finalizing state. `compatibilityDate` and `compatibilityFlags` control the Dynamic Worker runtime and default to the package-tested settings.

## Environment, standard input, and the `process` shim

Each execution installs a small `node:process` shim so ordinary module code can read its environment and standard streams. The shim exposes only what the caller supplies for that execution; the host environment is never visible.

`process.env` is a snapshot of the `env` record passed on the exec options. Values the caller does not pass are absent, and the Durable Object's own environment is never merged in, so a module cannot read host bindings or secrets through `process.env`.

`process.stdin` is a non-interactive async-iterable over the caller-supplied `stdin` bytes. The caller passes `stdin` as a `Uint8Array` or string on the exec options; `for await` yields the bytes once and then ends, and there is no blocking read for further input because an evaluate-once execution has no session to wait on. `isTTY` is `false`. The supplied input is bounded by `maxStdinBytes`; exceeding it fails the run with a clear error.

`process.stdout` and `process.stderr` are writable streams whose writes are captured as standard output and standard error. `console.log` and `console.info` route to standard output, `console.warn` and `console.error` route to standard error, and the captured output is bounded. `process.argv`, `process.cwd()`, and `process.platform` return inert values: `cwd()` reflects the execution's working directory, while `argv` and `platform` carry fixed placeholders rather than describing the host process.

```ts
const handle = await workspace.runtime.exec(
`
export default async function main() {
let piped = "";
for await (const chunk of process.stdin) piped += new TextDecoder().decode(chunk);
console.log("received", piped.length, "bytes");
return { who: process.env.WHO, piped };
}
`,
{
backend: "worker-javascript",
env: { WHO: "demo" },
stdin: "hello",
encoding: "utf8",
},
);
```

## Configured modules

Bare imports are installed at backend construction, not passed on individual executions:
Expand Down Expand Up @@ -156,7 +185,7 @@ Each execution receives a fresh Dynamic Worker with:
- a host wall-clock deadline;
- `globalOutbound: null` by default;
- finite, acyclic JSON-compatible input and structured result validation;
- configurable source/module graph, input, result, captured-log, file/capability request, and response byte limits (`maxSourceBytes`, `maxInputBytes`, `maxResultBytes`, `maxLogBytes`, and `maxCapabilityBytes`);
- configurable source/module graph, input, result, stdin, captured-log, file/capability request, and response byte limits (`maxSourceBytes`, `maxInputBytes`, `maxResultBytes`, `maxStdinBytes`, `maxLogBytes`, and `maxCapabilityBytes`);
- explicit entrypoint and Worker disposal;
- host-owned cancellation;
- retained events and result rows in the Workspace database.
Expand Down
12 changes: 11 additions & 1 deletion examples/worker-javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ npm run seed:r2 --workspace @example/computer-worker-javascript
PUT /c/<name>/file/workspace/<path> raw body → writeFile at /workspace/<path>
GET /c/<name>/file/workspace/<path> octet-stream of /workspace/<path>
(any path outside /workspace returns 400)
POST /c/<name>/exec { source, input?, cwd? }
POST /c/<name>/exec { source, input?, cwd?, env?, stdin? }
cwd defaults to /workspace
→ JSON { status, exitCode, stdout, stderr, value }
```
Expand Down Expand Up @@ -120,8 +120,18 @@ curl -X POST http://127.0.0.1:8787/c/demo/exec \
curl -X POST http://127.0.0.1:8787/c/demo/exec \
-H 'content-type: application/json' \
-d '{"source":"export default (input) => input.n * 2;","input":{"n":21}}'

curl -X POST http://127.0.0.1:8787/c/demo/exec \
-H 'content-type: application/json' \
-d '{"source":"export default async () => { let s = \"\"; for await (const c of process.stdin) s += new TextDecoder().decode(c); return process.env.WHO + \":\" + s; };","env":{"WHO":"demo"},"stdin":"piped"}'
```

`env` populates `process.env` (only the values you pass; the host
environment is never exposed), and `stdin` is readable through
`process.stdin`. `console.log` / `console.error` and
`process.stdout` / `process.stderr` writes come back as the result's
`stdout` and `stderr`.

## Layout

```
Expand Down
4 changes: 4 additions & 0 deletions examples/worker-javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ interface ExecRequest {
source?: string;
input?: WorkspaceRuntimeValue;
cwd?: string;
env?: Record<string, string>;
stdin?: string;
}

const MOUNT_ROOT = "/workspace";
Expand Down Expand Up @@ -130,6 +132,8 @@ async function handleExec(request: Request, env: Env, name: string): Promise<Res
backend: "worker-javascript",
cwd: body.cwd,
input: body.input,
env: body.env,
stdin: body.stdin,
encoding: "utf8",
});
const result = await handle.result();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,47 @@ describe("WorkerJavaScriptBackend", () => {
await expect(execution).rejects.toMatchObject({ code: "ECLOSED" });
});

it("rejects stdin larger than the configured ceiling", async () => {
const load = vi.fn();
const workspace = new Workspace({
storage: new SQLiteTestStorage(),
backends: [new WorkerJavaScriptBackend({ loader: { load }, maxStdinBytes: 8 })],
});
await workspace.fs.mkdir("/workspace", { recursive: true });
await expect(
workspace.runtime.exec("export default 1", { stdin: "x".repeat(64) }),
).rejects.toThrow(/stdin exceeds 8 bytes/);
expect(load).not.toHaveBeenCalled();
});

it("rejects env larger than the configured ceiling", async () => {
const load = vi.fn();
const workspace = new Workspace({
storage: new SQLiteTestStorage(),
backends: [new WorkerJavaScriptBackend({ loader: { load }, maxEnvBytes: 8 })],
});
await workspace.fs.mkdir("/workspace", { recursive: true });
await expect(
workspace.runtime.exec("export default 1", { env: { KEY: "x".repeat(64) } }),
).rejects.toThrow(/env exceeds 8 bytes/);
expect(load).not.toHaveBeenCalled();
});

it("rejects non-string env values", async () => {
const load = vi.fn();
const workspace = new Workspace({
storage: new SQLiteTestStorage(),
backends: [new WorkerJavaScriptBackend({ loader: { load } })],
});
await workspace.fs.mkdir("/workspace", { recursive: true });
await expect(
workspace.runtime.exec("export default 1", {
env: { KEY: 42 as unknown as string },
}),
).rejects.toThrow(/env value for "KEY" must be a string/);
expect(load).not.toHaveBeenCalled();
});

it("checks limits against the complete loader map including the runtime runner", async () => {
const load = vi.fn();
const workspace = new Workspace({
Expand Down Expand Up @@ -343,6 +384,7 @@ describe("WorkerJavaScriptBackend", () => {
waitUntil,
backends: [
new WorkerJavaScriptBackend({
maxConcurrentExecutions: 1,
loader: {
load() {
return {
Expand Down
Loading
Loading