-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Document ctx.props, ctx.exports, and worker-loader. #25303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
10dde39
c41ff24
ed54d06
dbc1630
18e8d6d
72fd4f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| title: Automatic loopback bindings via ctx.exports | ||
| description: You no longer have to configure bindings explicitly when they point back to your own Worker's top-level exports | ||
| date: 2025-09-26 | ||
| --- | ||
|
|
||
| The [`ctx.exports` API](/workers/runtime-apis/context/#exports) contains automatically-configured bindings corresponding to your Worker's top-level exports. For each top-level export extending `WorkerEntrypoint`, `ctx.exports` will contain a [Service Binding](/workers/runtime-apis/bindings/service-bindings) by the same name, and for each export extending `DurableObject` (and for which storage has been configured via a [migration](/durable-objects/reference/durable-objects-migrations/)), `ctx.exports` will contain a [Durable Object namespace binding](/durable-objects/api/namespace/). This means you no longer have to configure these bindings explicitly in `wrangler.jsonc`/`wrangler.toml`. | ||
|
|
||
| At present, you must use [the `enable_ctx_exports` compatibility flag](/workers/configuration/compatibility-flags#enable-ctxexports) to enable this API, though it will be on by default in the future. | ||
|
|
||
| [See the API reference for more information.](/workers/runtime-apis/context/#exports) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| _build: | ||
| publishResources: false | ||
| render: never | ||
| list: never | ||
|
|
||
| name: "Enable ctx.exports" | ||
| sort_date: "2025-09-24" | ||
| enable_flag: "enable_ctx_exports" | ||
| --- | ||
|
|
||
| This flag enables [the `ctx.exports` API](/workers/runtime-apis/context/#exports), which contains automatically-configured loopback bindings for your Worker's top-level exports. This allows you to skip configuring explicit bindings for your `WorkerEntrypoint`s and Durable Object namespaces defined in the same Worker. | ||
|
|
||
| We may change this API to be enabled by default in the future (regardless of compat date or flags). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,227 @@ | ||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
| pcx_content_type: configuration | ||||||||||||||||||||||||
| title: Dynamic Worker Loaders | ||||||||||||||||||||||||
| head: [] | ||||||||||||||||||||||||
| description: The Dynamic Worker Loader API, which allows dynamically spawning isolates that run arbitrary code. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
| Type, | ||||||||||||||||||||||||
| MetaInfo, | ||||||||||||||||||||||||
| WranglerConfig | ||||||||||||||||||||||||
| } from "~/components"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| :::note[Dynamic Worker Loading is in closed beta] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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](https://forms.gle/MoeDxE9wNiqdf8ri9). | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ::: | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| A Worker Loader binding allows you to load additional Workers containing arbitrary code at runtime. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Worker Loaders also enable **sandboxing** of code, meaning that you can strictly limit what the code is allowed to do. In particular: | ||||||||||||||||||||||||
| * You can arrange to intercept or simply block all network requests made by the Worker within. | ||||||||||||||||||||||||
| * You can supply the sandboxed Worker with custom bindings to represent specific resources which it should be allowed to access. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| With proper sandboxing configured, you can safely run code you do not trust in a dynamic isolate. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| A Worker Loader is a binding with just one method, `get()`, which loads an isolate. Example usage: | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```js | ||||||||||||||||||||||||
| let id = "foo"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Get the isolate with the given ID, creating it if no such isolate exists yet. | ||||||||||||||||||||||||
| let worker = env.LOADER.get(id, async () => { | ||||||||||||||||||||||||
| // If the isolate does not already exist, this callback is invoked to fetch | ||||||||||||||||||||||||
| // the isolate's Worker code. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| compatibilityDate: "2025-06-01", | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Specify the worker's code (module files). | ||||||||||||||||||||||||
| mainModule: "foo.js", | ||||||||||||||||||||||||
| modules: { | ||||||||||||||||||||||||
| "foo.js": | ||||||||||||||||||||||||
| "export default {\n" + | ||||||||||||||||||||||||
| " fetch(req, env, ctx) { return new Response('Hello'); }\n" + | ||||||||||||||||||||||||
| "}\n", | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Specify the dynamic Worker's environment (`env`). This is specified | ||||||||||||||||||||||||
| // as a JavaScript object, exactly as you want it to appear to the | ||||||||||||||||||||||||
| // child Worker. It can contain basic serializable types as well as | ||||||||||||||||||||||||
| // Service Bindings (see below). | ||||||||||||||||||||||||
| env: { | ||||||||||||||||||||||||
| SOME_ENV_VAR: 123 | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // To block the worker from talking to the internet using `fetch()` or | ||||||||||||||||||||||||
| // `connect()`, set `globalOutbound` to `null`. You can also set this | ||||||||||||||||||||||||
| // to any service binding, to have calls be intercepted and redirected | ||||||||||||||||||||||||
| // to that binding. | ||||||||||||||||||||||||
| globalOutbound: null, | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Now you can get the Worker's entrypoint and send requests to it. | ||||||||||||||||||||||||
| let defaultEntrypoint = worker.getEntrypoint(); | ||||||||||||||||||||||||
| await defaultEntrypoint.fetch("http://example.com"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // You can get non-default entrypoints as well, and specify the | ||||||||||||||||||||||||
| // `ctx.props` value to be delivered to the entrypoint. | ||||||||||||||||||||||||
| let someEntrypoint = worker.getEntrypoint("SomeEntrypointClass", { | ||||||||||||||||||||||||
| props: {someProp: 123} | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Configuration | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| To add a dynamic worker loader binding to your worker, add it to your Wrangler config like so: | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <WranglerConfig> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```toml | ||||||||||||||||||||||||
| [[worker_loaders]] | ||||||||||||||||||||||||
| binding = "LOADER" | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| </WranglerConfig> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## API Reference | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### `get` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <code>get(id <Type text="string" />, getCodeCallback <Type text="() => Promise<WorkerCode>" />): <Type text="WorkerStub" /></code> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Loads a Worker with the given ID. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| 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. | |
| 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. | |
| This means that anytime you update any value returned by the callback (the isolate's code and its configuration), you must use a different ID if you need to guarantee that the updated values will be used. | |
| Best practices: | |
| - If you frequently run the same code with the same configuration provided in the callback, you should use the same ID in order to get automatic caching. | |
| - If the code or configuration you load is different every time, you should 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. |
Thinking about ways to be explicit to people that this means not just that if the code changes you need new ID, but that if the config provided via the callback changes, you also need new ID...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
riffing on how to be super clear about this...can take or leave language
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the same time as you were suggesting this I was tweaking my own text a bit, but now I gotta run, so I'll come back and see if I can merge yours and mine later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I rewrote this section again, I think it's clearer now, hopefully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy over this code snippet?
https://github.com/cloudflare/cloudflare-docs/pull/25303/files#diff-7157aec1a3a350afe6de7fce2cef85501751923d7af1509ee0fee060811039e4R80-R95
so that the reader can read the code snippet and "get it" faster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do before/after
Before:
example codeAfter:
<less config / zero config>
example code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied the example, not really feeling how to do before/after.