Skip to content
Open
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
5 changes: 3 additions & 2 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ packages/computer/
│ ├── backend.ts # Command backend interface
│ ├── backends/
│ │ ├── container/ # Cloudflare Container + computerd backend
│ │ ├── worker/ # Dynamic Worker + just-bash backend
│ │ ├── javascript/ # Dynamic Worker ECMAScript backend
│ │ ├── worker-shell/ # Dynamic Worker + just-bash shell backend
│ │ ├── worker-javascript/ # Dynamic Worker ECMAScript module backend
│ │ └── test.ts # In-process test backend
│ ├── proxy.ts # WorkspaceProxy
│ ├── proxy-stub.ts # Client-side stub plumbing
Expand Down Expand Up @@ -200,6 +200,7 @@ Runnable examples live at the repo root, not inside any package:
examples/
├── container/ # Reference container image for computerd
├── worker-shell/ # WorkerShellBackend example
├── worker-javascript/ # WorkerJavaScriptBackend example
├── code/ # workspace.runtime with Worker and Container shells
└── think/ # @cloudflare/think integration
```
Expand Down
4 changes: 2 additions & 2 deletions docs/12_worker_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> [!NOTE]
> This doc reflects shipped code in
> `packages/computer/src/backends/worker-shell/`. The example deployment
> lives at `examples/worker/`.
> lives at `examples/worker-shell/`.

The worker backend is the second `WorkspaceBackend` shape the
package ships. It pairs a Workspace with a
Expand Down Expand Up @@ -241,7 +241,7 @@ network-bound `git` subcommands do. See

## Example

`examples/worker/` is a single wrangler project that mirrors
`examples/worker-shell/` is a single wrangler project that mirrors
`examples/container/` beat for beat:

- One `wrangler.jsonc` with the Durable Object, an R2 mount at
Expand Down
31 changes: 30 additions & 1 deletion docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ Cancellation stops new host capability calls, disposes the Dynamic Worker, and w

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
3 changes: 3 additions & 0 deletions examples/worker-javascript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
.wrangler/
152 changes: 152 additions & 0 deletions examples/worker-javascript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# worker-javascript example

> [!IMPORTANT]
> **PREVIEW ONLY** This package is provided as a preview for feedback only.
> APIs are unstable and the design is subject to change.

A Cloudflare Worker + Durable Object that runs a Workspace whose
runtime evaluates **ECMAScript modules** in a **Dynamic Worker**
loaded through `env.LOADER`. It mirrors
[`examples/worker-shell`](../worker-shell) beat for beat — same DO
class, same routes, same R2 mount — but `exec` runs a JavaScript
module instead of a just-bash command.

## Architecture

```
client ─► Worker /c/<name>/{file,exec}
│ (DO RPC calls)
DO (ContainerExample) ──► Workspace ──► WorkerJavaScriptBackend
│ env.LOADER.load(...)
Dynamic Worker
(module runner)
│ evaluate(input, bridge)
host capability bridge
(fs, git, artifacts)
```

1. The DO constructs a `WorkerJavaScriptBackend` from
`@cloudflare/computer/backends/worker-javascript`, passing only
the Loader binding. Unlike the shell backend there is no
`WorkspaceServiceProxy` loopback: the backend is self-contained
and reaches the host through the `WorkspaceRuntimeBridge` it
builds internally.
2. Each `exec(source, { backend: "worker-javascript", input })`
builds a module graph from the source, mints a fresh Dynamic
Worker through `env.LOADER.load(...)`, and calls the runner's
`evaluate(input, bridge)`. The module's default export receives
the structured `input` and its return value becomes the
result's `value`.
3. Filesystem, Git, and artifacts operations from inside the module
round-trip through the bridge to the host DO's own store, so
storage handles stay valid and one workspace per DO is the
natural boundary.
4. `BackendHandle.sync` is `"none"`. There's a single authoritative
store (the DO's SQLite); push and pull short-circuit.
`pushed` / `pulled` are always zero.

The backend requires `waitUntil`, so the DO passes
`ctx.waitUntil.bind(ctx)` into the Workspace options. The DO is a
thin host; the Dynamic Worker lifecycle is the loader's problem.

## Paths

`PUT /c/<name>/file/workspace/hello.txt` writes
`/workspace/hello.txt`, and
`GET /c/<name>/file/workspace/r2/x` reads `/workspace/r2/x` —
the URL and the on-disk path always match. Any URL outside
`/workspace` returns 400.

`exec` runs with `cwd` defaulting to `/workspace`. The source is
an ECMAScript module; its default export may be a function that
receives the request's `input` value and returns a
JSON-compatible result. The Dynamic Worker has
`globalOutbound: null`, so the module can't reach the public
internet on its own.

## R2 mount

Identical to the worker-shell example. Seed once with:

```sh
npm run seed:r2:local --workspace @example/computer-worker-javascript

# or after deploy
npm run seed:r2 --workspace @example/computer-worker-javascript
```

## HTTP surface

```
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?, env?, stdin? }
cwd defaults to /workspace
→ JSON { status, exitCode, stdout, stderr, value }
```

## Run it locally

No Docker, no extra build step. The module runner ships inside
`@cloudflare/computer/backends/worker-javascript`;
`WorkerJavaScriptBackend` mints the Dynamic Worker through the
Loader binding internally so the DO constructor stays a one-line
backend invocation.

```sh
npm run dev --workspace @example/computer-worker-javascript
```

Smoke test:

```sh
curl http://127.0.0.1:8787/

echo 'hello' | curl -X PUT --data-binary @- \
http://127.0.0.1:8787/c/demo/file/workspace/hello.txt

curl http://127.0.0.1:8787/c/demo/file/workspace/hello.txt

curl -X POST http://127.0.0.1:8787/c/demo/exec \
-H 'content-type: application/json' \
-d '{"source":"import { readFile } from \"node:fs/promises\";\nexport default async () => (await readFile(\"/workspace/hello.txt\", \"utf8\")).trim();"}'

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

```
examples/worker-javascript/
wrangler.jsonc Worker + DO + worker_loaders binding
src/index.ts Worker handler + DO (ContainerExample)
```

Nothing else. The Dynamic Worker module runner ships from
`@cloudflare/computer/backends/worker-javascript`.

## Known limitations

- **Exec is run-and-collect.** The handler awaits
`handle.result()` and emits one JSON response.
- **One execution at a time by default.** The backend admits a
single execution per workspace unless configured otherwise, and
bounds completed-execution retention by time and count.
22 changes: 22 additions & 0 deletions examples/worker-javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@example/computer-worker-javascript",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Example Worker + Durable Object that evaluates ECMAScript modules inside a Dynamic Worker via @cloudflare/computer's WorkerJavaScriptBackend.",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"seed:r2": "wrangler r2 object put computer-worker-javascript-hello/hello.txt --file ./seed/data/hello.txt --remote",
"seed:r2:local": "wrangler r2 object put computer-worker-javascript-hello/hello.txt --file ./seed/data/hello.txt --local"
},
"dependencies": {
"@cloudflare/computer": "*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260616.1",
"typescript": "^6.0.3",
"wrangler": "^4.107.1"
}
}
1 change: 1 addition & 0 deletions examples/worker-javascript/seed/data/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
Loading