Skip to content

Commit 0a11df2

Browse files
committed
Document ctx.props, ctx.exports, and worker-loader.
1 parent 03d811a commit 0a11df2

File tree

3 files changed

+379
-3
lines changed

3 files changed

+379
-3
lines changed

src/content/docs/durable-objects/api/state.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ export class MyDurableObject extends DurableObject {
4949

5050
</TabItem> </Tabs>
5151

52-
## Methods
52+
## Methods and Properties
53+
54+
### `exports`
55+
56+
Contains loopback bindings to the Worker's own top-level exports. This has exactly the same meaning as [`ExecutionContext`'s `ctx.exports`](/workers/runtime-apis/context/#exports).
5357

5458
### `waitUntil`
5559

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
---
2+
pcx_content_type: configuration
3+
title: Dynamic Worker Loaders
4+
head: []
5+
description: The Dynamic Worker Loader API, which allows dynamically spawning isolates that run arbitrary code.
6+
7+
---
8+
9+
import {
10+
Type,
11+
MetaInfo,
12+
WranglerConfig
13+
} from "~/components";
14+
15+
:::note[Dynamic Worker Loading is in closed beta]
16+
17+
The Worker Loader API is available in local development with Wrangler and workerd. But, to use it in production, you must [sign up for the closed beta](TODO: Link to sign-up form).
18+
19+
:::
20+
21+
A Worker Loader binding allows you to load additional Workers containing arbitrary code at runtime.
22+
23+
An isolate is like a lightweight container. [The Workers platform uses isolates instead of containers or VMs](/workers/reference/how-workers-works/), so every Worker runs in an isolate already. But, a Worker Loader binding allows your Worker to create additional isolates that load arbitrary code on-demand.
24+
25+
Isolates are much cheaper than containers. You can start an isolate in milliseconds, and it's fine to start one just to run a snippet of code and immediately throw away. There's no need to worry about pooling isolates or trying to reuse already-warm isolates, as you would need to do with containers.
26+
27+
Worker Loaders also enable **sandboxing** of code, meaning that you can strictly limit what the code is allowed to do. In particular:
28+
* You can arrange to intercept or simply block all network requests made by the Worker within.
29+
* You can supply the sandboxed Worker with custom bindings to represent specific resources which it should be allowed to access.
30+
31+
With proper sandboxing configured, you can safely run code you do not trust in a dynamic isolate.
32+
33+
A Worker Loader is a binding with just one method, `get()`, which loads an isolate. Example usage:
34+
35+
```js
36+
let id = "foo";
37+
38+
// Get the isolate with the given ID, creating it if no such isolate exists yet.
39+
let worker = env.LOADER.get(id, async () => {
40+
// If the isolate does not already exist, this callback is invoked to fetch
41+
// the isolate's Worker code.
42+
43+
return {
44+
compatibilityDate: "2025-06-01",
45+
46+
// Specify the worker's code (module files).
47+
mainModule: "foo.js",
48+
modules: {
49+
"foo.js":
50+
"export default {\n" +
51+
" fetch(req, env, ctx) { return new Response('Hello'); }\n" +
52+
"}\n",
53+
},
54+
55+
// Specify the dynamic Worker's environment (`env`). This is specified
56+
// as a JavaScript object, exactly as you want it to appear to the
57+
// child Worker. It can contain basic serializable types as well as
58+
// Service Bindings (see below).
59+
env: {
60+
SOME_ENV_VAR: 123
61+
},
62+
63+
// To block the worker from talking to the internet using `fetch()` or
64+
// `connect()`, set `globalOutbound` to `null`. You can also set this
65+
// to any service binding, to have calls be intercepted and redirected
66+
// to that binding.
67+
globalOutbound: null,
68+
};
69+
});
70+
71+
// Now you can get the Worker's entrypoint and send requests to it.
72+
let defaultEntrypoint = worker.getEntrypoint();
73+
await defaultEntrypoint.fetch("http://example.com");
74+
75+
// You can get non-default entrypoints as well, and specify the
76+
// `ctx.props` value to be delivered to the entrypoint.
77+
let someEntrypoint = worker.getEntrypoint("SomeEntrypointClass", {
78+
props: {someProp: 123}
79+
});
80+
```
81+
82+
## Configuration
83+
84+
To add a dynamic worker loader binding to your worker, add it to your Wrangler config like so:
85+
86+
<WranglerConfig>
87+
88+
```toml
89+
[[unsafe.bindings]]
90+
name = "LOADER"
91+
type = "worker-loader"
92+
```
93+
94+
</WranglerConfig>
95+
96+
## API Reference
97+
98+
### `get`
99+
100+
<code>get(id <Type text="string" />, getCodeCallback <Type text="() => Promise<WorkerCode>" />): <Type text="WorkerStub" /></code>
101+
102+
Loads a Worker with the given ID.
103+
104+
As a convenience, the loader implements basic caching of isolates: If this loader has already been used to load a Worker with the same ID in the past, and that Worker's isolate is still resident in memory, then the existing Worker will be returned, and the callback will not be called. When an isolate has not been used in a while, the system will discard it automatically, and then the next attempt to get the same ID will have to load it again. If you frequently run the same code, you should use the same ID in order to get automatic caching. On the other hand, if the code you load is different every time, you can provide a random ID. Note that if your code has many versions, each version will need a unique ID, as there is no way to explicitly evict a previous version.
105+
106+
If the system opts not to reuse an existing Worker, then it invokes `codeCallback` to get the Worker's code. This is an async callback, so the application can load the code from remote storage if desired. The callback returns a `WorkerCode` object (described below).
107+
108+
`get()` returns a `WorkerStub`, which can be used to send requests to the loaded Worker. Note that the stub is returned synchronously&mdash;you do not have to await it. If the Worker is not loaded yet, requests made to the stub will wait for the Worker to load before being delivered. If loading fails, the request will throw an exception.
109+
110+
### `WorkerCode`
111+
112+
This is the structure returned by `getCodeCallback` to represent a worker.
113+
114+
#### <code>compatibilityDate <Type text="string" /></code>
115+
116+
The [compatibility date](/workers/configuration/compatibility-dates/) for the Worker. This has the same meaning as the `compatibility_date` setting in a Wrangler config file.
117+
118+
#### <code>compatibilityFlags <Type text="string[]" /> <MetaInfo text='Optional' /></code>
119+
120+
An optional list of [compatibility flags](/workers/configuration/compatibility-flags) augmenting the compatibility date. This has the same meaning as the `compatibility_flags` setting in a Wrangler config file.
121+
122+
#### <code>allowExperimental <Type text="boolean" /> <MetaInfo text='Optional' /></code>
123+
124+
If true, then experimental compatibility flags will be permitted in `compatibilityFlags`. In order to set this, the worker calling the loader must itself have the compatibility flag `"experimental"` set. Experimental flags cannot be enabled in production.
125+
126+
#### <code>mainModule <Type text="string" /></code>
127+
128+
The name of the Worker's main module. This must be one of the modules listed in `modules`.
129+
130+
#### <code>modules <Type text="Record<string, string | Module>"/></code>
131+
132+
A dictionary object mapping module names to their string contents. If the module content is a plain string, then the module name must have a file extension indicating its type: either `.js` or `.py`.
133+
134+
A module's content can also be specified as an object, in order to specify its type independent from the name. The allowed objects are:
135+
136+
* `{js: string}`: A JavaScript module, using ES modules syntax for imports and exports.
137+
* `{cjs: string}`: A CommonJS module, using `require()` syntax for imports.
138+
* `{py: string}`: A [Python module](/workers/languages/python/), but see the warning below.
139+
* `{text: string}`: An importable string value.
140+
* `{data: ArrayBuffer}`: An importable `ArrayBuffer` value.
141+
* `{json: object}`: An importable object. The value must be JSON-serializable. However, note that value is provided as a parsed object, and is delivered as a parsed object; neither side actually sees the JSON serialization.
142+
143+
:::caution[Warning]
144+
145+
While Dynamic Isolates support Python, please note that at this time, Python Workers are much slower to start than JavaScript Workers, which may defeat some of the benefits of dynamic isolate loading. They may also be priced differently, when Worker Loaders become generally available.
146+
147+
:::
148+
149+
#### <code>globalOutbound <Type text="ServiceStub | null" /> Optional</code>
150+
151+
Controls whether the dynamic Worker has access to the network. The global `fetch()` and `connect()` functions (for making HTTP requests and TCP connections, respectively) can be blocked or redirected to isolate the Worker.
152+
153+
If `globalOutbound` is not specified, the default is to inherit the parent's network access, which usually means the dynamic Worker will have full access to the public Internet.
154+
155+
If `globalOutbound` is `null`, then the dynamic Worker will be totally cut off from the network. Both `fetch()` and `connect()` will throw exceptions.
156+
157+
`globalOutbound` can also be set to any service binding, including service bindings in the parent worker's `env` as well as [loopback bindings from `ctx.exports`](/workers/runtime-apis/context/#exports).
158+
159+
Using `ctx.exports` is particularly useful as it allows you to customize the binding further for the specific sandbox, by setting the value of `ctx.props` that should be passed back to it. The `props` can contain information to identify the specific dynamic Worker that made the request.
160+
161+
For example:
162+
163+
```js
164+
import { WorkerEntrypoint } from "cloudflare:workers";
165+
166+
export class Greeter extends WorkerEntrypoint {
167+
fetch(request) {
168+
return new Response(`Hello, ${this.ctx.props.name}!`);
169+
}
170+
}
171+
172+
export default {
173+
async fetch(request, env, ctx) {
174+
let worker = env.LOADER.get("alice", () => {
175+
return {
176+
// Redirect the worker's global outbound to send all requests
177+
// to the `Greeter` class, filling in `ctx.props.name` with
178+
// the name "Alice", so that it always responds "Hello, Alice!".
179+
globalOutbound: ctx.exports.Greeter({props: {name: "Alice"}})
180+
181+
// ... code ...
182+
}
183+
});
184+
}
185+
}
186+
```
187+
188+
#### <code>env <Type text="object" /></code>
189+
190+
The environment object to provide to the dynamic Worker.
191+
192+
Using this, you can provide custom bindings to the Worker.
193+
194+
`env` is serialized and transferred into the dynamic Worker, where it is used directly as the value of `env` there. It may contain:
195+
196+
* [Structured clonable types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
197+
* [Service Bindings](/workers/runtime-apis/bindings/service-bindings), including [loopback bindings from `ctx.exports`](/workers/runtime-apis/context/#exports).
198+
199+
The second point is the key to creating custom bindings: you can define a binding with any arbitrary API, by defining a [`WorkerEntrypoint` class](/workers/runtime-apis/bindings/service-bindings/rpc) implementing an RPC API, and then giving it to the dynamic Worker as a Service Binding.
200+
201+
Moreover, by using `ctx.exports` loopback bindings, you can further customize the bindings for the specific dynamic Worker by setting `ctx.props`, just as described for `globalOutbound`, above.
202+
203+
```js
204+
import { WorkerEntrypoint } from "cloudflare:workers";
205+
206+
// Implement a binding which can be called by the dynamic Worker.
207+
export class Greeter extends WorkerEntrypoint {
208+
greet() {
209+
return `Hello, ${this.ctx.props.name}!`;
210+
}
211+
}
212+
213+
export default {
214+
async fetch(request, env, ctx) {
215+
let worker = env.LOADER.get("alice", () => {
216+
return {
217+
env: {
218+
// Provide a binding which has a method greet() which can be called
219+
// to receive a greeting. The binding knows the Worker's name.
220+
GREETER: ctx.exports.Greeter({props: {name: "Alice"}})
221+
}
222+
223+
// ... code ...
224+
}
225+
});
226+
}
227+
}
228+
```

0 commit comments

Comments
 (0)