Skip to content

Commit

Permalink
feat: add support for wrapped bindings (#4348)
Browse files Browse the repository at this point in the history
This change adds a new `wrappedBindings` worker option for configuring
`workerd`'s wrapped bindings. These allow custom bindings to be
written as JavaScript functions accepting an `env` parameter of
"inner bindings" and returning the value to bind.

The `wrappedBindings` option maps binding names to designators.
The worker defined by the designator is used for the wrapped binding's
source and inner bindings. The worker becomes un-routable, and cannot
be used for service or Durable Object bindings. Using a worker allows
us to piggyback on all of Miniflare's existing bindings. For example,
the wrapped binding worker can declare a function-valued service
binding for accessing Node.js state. JSON bindings can be specified
with the designator to override specific bindings.
  • Loading branch information
mrbbot authored Nov 21, 2023
1 parent 805d524 commit be2b9cf
Show file tree
Hide file tree
Showing 10 changed files with 925 additions and 82 deletions.
11 changes: 11 additions & 0 deletions .changeset/fair-emus-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"miniflare": minor
---

feat: add support for wrapped bindings

This change adds a new `wrappedBindings` worker option for configuring
`workerd`'s [wrapped bindings](https://github.com/cloudflare/workerd/blob/bfcef2d850514c569c039cb84c43bc046af4ffb9/src/workerd/server/workerd.capnp#L469-L487).
These allow custom bindings to be written as JavaScript functions accepting an
`env` parameter of "inner bindings" and returning the value to bind. For more
details, refer to the [API docs](https://github.com/cloudflare/workers-sdk/blob/main/packages/miniflare/README.md#core).
108 changes: 108 additions & 0 deletions packages/miniflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,114 @@ parameter in module format Workers.
handler. This allows you to access data and functions defined in Node.js
from your Worker.

<!--prettier-ignore-start-->

- `wrappedBindings?: Record<string, string | { scriptName: string, entrypoint?: string, bindings?: Record<string, Json> }>`

Record mapping binding name to designators to inject as
[wrapped bindings](https://github.com/cloudflare/workerd/blob/bfcef2d850514c569c039cb84c43bc046af4ffb9/src/workerd/server/workerd.capnp#L469-L487) into this Worker.
Wrapped bindings allow custom bindings to be written as JavaScript functions
accepting an `env` parameter of "inner bindings" and returning the value to
bind. A `string` designator is equivalent to `{ scriptName: <string> }`.
`scriptName`'s bindings will be used as "inner bindings". JSON `bindings` in
the `designator` also become "inner bindings" and will override any of
`scriptName` bindings with the same name. The Worker named `scriptName`...

- Must define a single `ESModule` as its source, using
`{ modules: true, script: "..." }`, `{ modules: true, scriptPath: "..." }`,
or `{ modules: [...] }`
- Must provide the function to use for the wrapped binding as an `entrypoint`
named export or a default export if `entrypoint` is omitted
- Must not be the first/entrypoint worker
- Must not be bound to with service or Durable Object bindings
- Must not define `compatibilityDate` or `compatibilityFlags`
- Must not define `outboundService`
- Must not directly or indirectly have a wrapped binding to itself
- Must not be used as an argument to `Miniflare#getWorker()`

<details>
<summary><b>Wrapped Bindings Example</b></summary>

```ts
import { Miniflare } from "miniflare";
const store = new Map<string, string>();
const mf = new Miniflare({
workers: [
{
wrappedBindings: {
MINI_KV: {
scriptName: "mini-kv", // Use Worker named `mini-kv` for implementation
bindings: { NAMESPACE: "ns" }, // Override `NAMESPACE` inner binding
},
},
modules: true,
script: `export default {
async fetch(request, env, ctx) {
// Example usage of wrapped binding
await env.MINI_KV.set("key", "value");
return new Response(await env.MINI_KV.get("key"));
}
}`,
},
{
name: "mini-kv",
serviceBindings: {
// Function-valued service binding for accessing Node.js state
async STORE(request) {
const { pathname } = new URL(request.url);
const key = pathname.substring(1);
if (request.method === "GET") {
const value = store.get(key);
const status = value === undefined ? 404 : 200;
return new Response(value ?? null, { status });
} else if (request.method === "PUT") {
const value = await request.text();
store.set(key, value);
return new Response(null, { status: 204 });
} else if (request.method === "DELETE") {
store.delete(key);
return new Response(null, { status: 204 });
} else {
return new Response(null, { status: 405 });
}
},
},
modules: true,
script: `
// Implementation of binding
class MiniKV {
constructor(env) {
this.STORE = env.STORE;
this.baseURL = "http://x/" + (env.NAMESPACE ?? "") + ":";
}
async get(key) {
const res = await this.STORE.fetch(this.baseURL + key);
return res.status === 404 ? null : await res.text();
}
async set(key, body) {
await this.STORE.fetch(this.baseURL + key, { method: "PUT", body });
}
async delete(key) {
await this.STORE.fetch(this.baseURL + key, { method: "DELETE" });
}
}
// env has the type { STORE: Fetcher, NAMESPACE?: string }
export default function (env) {
return new MiniKV(env);
}
`,
},
],
});
```

</details>

> :warning: `wrappedBindings` are only supported in modules format Workers.

<!--prettier-ignore-end-->

- `outboundService?: string | { network: Network } | { external: ExternalServer } | { disk: DiskDirectory } | (request: Request) => Awaitable<Response>`

Dispatch this Worker's global `fetch()` and `connect()` requests to the
Expand Down
Loading

0 comments on commit be2b9cf

Please sign in to comment.