Skip to content

Commit 3c399bb

Browse files
kentonvGregBrimble
authored andcommitted
Implement support for ctx.props. (#8640)
* Implement support for ctx.props. In `wrangler.toml` you can declare a service binding with props: ``` [[services]] binding = "MY_SERVICE" service = "some-worker" props = { foo = 123, bar = 456 } ``` Unfortunately, it currently doesn't work when wrangler is running different workers in different processes, as `props` cannot be sent to a remote `workerd`. Fixing this would require `workerd` changes, but since we are moving away from the multi-process mode anyway, perhaps it's not worth it. Note: I used Claude Code to find my way around the codebase. It wrote most of the PR. It was not able to figure out that changes were needed under `packages/miniflare`, and I myself had a pretty hard time figuring out where to edit. * Add changeset for props support --------- Co-authored-by: Greg Brimble <[email protected]>
1 parent ee9521b commit 3c399bb

File tree

10 files changed

+115
-2
lines changed

10 files changed

+115
-2
lines changed

.changeset/curly-groups-scream.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
Add support for defining `props` on a Service binding.
7+
8+
In your configuration file, you can define a service binding with props:
9+
10+
```json
11+
{
12+
"services": [
13+
{
14+
"binding": "MY_SERVICE",
15+
"service": "some-worker",
16+
"props": { "foo": 123, "bar": "value" }
17+
}
18+
]
19+
}
20+
```
21+
22+
These can then be accessed by the callee:
23+
24+
```ts
25+
import { WorkerEntrypoint } from "cloudflare:workers";
26+
27+
export default class extends WorkerEntrypoint {
28+
fetch() {
29+
return new Response(JSON.stringify(this.ctx.props));
30+
}
31+
}
32+
```

packages/miniflare/src/plugins/core/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ function getCustomServiceDesignator(
255255
): ServiceDesignator {
256256
let serviceName: string;
257257
let entrypoint: string | undefined;
258+
let props: { json: string } | undefined;
258259
if (typeof service === "function") {
259260
// Custom `fetch` function
260261
serviceName = getCustomServiceName(workerIndex, kind, name);
@@ -268,6 +269,9 @@ function getCustomServiceDesignator(
268269
serviceName = getUserServiceName(service.name);
269270
}
270271
entrypoint = service.entrypoint;
272+
if (service.props) {
273+
props = { json: JSON.stringify(service.props) };
274+
}
271275
} else {
272276
// Builtin workerd service: network, external, disk
273277
serviceName = getBuiltinServiceName(workerIndex, kind, name);
@@ -282,7 +286,7 @@ function getCustomServiceDesignator(
282286
// Regular user worker
283287
serviceName = getUserServiceName(service);
284288
}
285-
return { name: serviceName, entrypoint };
289+
return { name: serviceName, entrypoint, props };
286290
}
287291

288292
function maybeGetCustomServiceService(

packages/miniflare/src/plugins/core/services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export const ServiceDesignatorSchema = z.union([
9090
z.object({
9191
name: z.union([z.string(), z.literal(kCurrentWorker)]),
9292
entrypoint: z.ostring(),
93+
props: z.record(z.unknown()).optional(),
9394
}),
9495
z.object({ network: NetworkSchema }),
9596
z.object({ external: ExternalServerSchema }),

packages/miniflare/src/runtime/config/workerd.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type Service = {
4646
export interface ServiceDesignator {
4747
name?: string;
4848
entrypoint?: string;
49+
props?: { json: string };
4950
}
5051

5152
export type Worker = (

packages/wrangler/e2e/multiworker-dev.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ describe("multiworker", () => {
6767
await counter.increment(2)
6868
await counter.increment(3)
6969
return new Response(String(await counter.value))
70+
}
71+
if (url.pathname === "/props") {
72+
const props = await env.COUNTER.getProps()
73+
return new Response(JSON.stringify(props))
7074
}
7175
return env.BEE.fetch(req);
7276
},
@@ -129,6 +133,9 @@ describe("multiworker", () => {
129133
async newCounter() {
130134
return new Counter();
131135
}
136+
async getProps() {
137+
return this.ctx.props;
138+
}
132139
}
133140
export default{
134141
async fetch(req, env) {
@@ -193,6 +200,7 @@ describe("multiworker", () => {
193200
binding = "COUNTER"
194201
service = '${workerName2}'
195202
entrypoint = 'CounterService'
203+
props = { foo = 123, bar = { baz = "hello from props" } }
196204
`,
197205
});
198206
});
@@ -232,6 +240,26 @@ describe("multiworker", () => {
232240
);
233241
});
234242

243+
it("can access service props through a binding", async () => {
244+
const workerA = helper.runLongLived(
245+
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
246+
{ cwd: a }
247+
);
248+
const { url } = await workerA.waitForReady(5_000);
249+
250+
await vi.waitFor(
251+
async () => {
252+
const response = await fetch(`${url}/props`);
253+
const props = await response.json();
254+
expect(props).toEqual({
255+
foo: 123,
256+
bar: { baz: "hello from props" },
257+
});
258+
},
259+
{ interval: 1000, timeout: 10_000 }
260+
);
261+
});
262+
235263
it("shows error on startup with non-existent service", async () => {
236264
await baseSeed(a, {
237265
"wrangler.toml": dedent`

packages/wrangler/src/__tests__/deploy.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8893,6 +8893,45 @@ addEventListener('fetch', event => {});`
88938893
expect(std.err).toMatchInlineSnapshot(`""`);
88948894
expect(std.warn).toMatchInlineSnapshot(`""`);
88958895
});
8896+
8897+
it("should support service bindings with props", async () => {
8898+
writeWranglerConfig({
8899+
services: [
8900+
{
8901+
binding: "FOO",
8902+
service: "foo-service",
8903+
props: { foo: 123, bar: { baz: "hello from props" } },
8904+
},
8905+
],
8906+
});
8907+
writeWorkerSource();
8908+
mockSubDomainRequest();
8909+
mockUploadWorkerRequest({
8910+
expectedBindings: [
8911+
{
8912+
type: "service",
8913+
name: "FOO",
8914+
service: "foo-service",
8915+
props: { foo: 123, bar: { baz: "hello from props" } },
8916+
},
8917+
],
8918+
});
8919+
8920+
await runWrangler("deploy index.js");
8921+
expect(std.out).toMatchInlineSnapshot(`
8922+
"Total Upload: xx KiB / gzip: xx KiB
8923+
Worker Startup Time: 100 ms
8924+
Your worker has access to the following bindings:
8925+
- Services:
8926+
- FOO: foo-service
8927+
Uploaded test-name (TIMINGS)
8928+
Deployed test-name triggers (TIMINGS)
8929+
https://test-name.test-sub-domain.workers.dev
8930+
Current Version ID: Galaxy-Class"
8931+
`);
8932+
expect(std.err).toMatchInlineSnapshot(`""`);
8933+
expect(std.warn).toMatchInlineSnapshot(`""`);
8934+
});
88968935
});
88978936

88988937
describe("[analytics_engine_datasets]", () => {

packages/wrangler/src/config/environment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,8 @@ export interface EnvironmentNonInheritable {
684684
environment?: string;
685685
/** Optionally, the entrypoint (named export) of the service to bind to. */
686686
entrypoint?: string;
687+
/** Optional properties that will be made available to the service via ctx.props. */
688+
props?: Record<string, unknown>;
687689
}[]
688690
| undefined;
689691

packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
386386
);
387387

388388
bindings.services?.forEach(
389-
({ binding, service, environment, entrypoint }) => {
389+
({ binding, service, environment, entrypoint, props }) => {
390390
metadataBindings.push({
391391
name: binding,
392392
type: "service",
393393
service,
394394
...(environment && { environment }),
395395
...(entrypoint && { entrypoint }),
396+
...(props && { props }),
396397
});
397398
}
398399
);

packages/wrangler/src/deployment-bundle/worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export interface CfService {
217217
service: string;
218218
environment?: string;
219219
entrypoint?: string;
220+
props?: Record<string, unknown>;
220221
}
221222

222223
export interface CfAnalyticsEngineDataset {

packages/wrangler/src/dev/miniflare.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): {
444444
serviceBindings[service.binding] = {
445445
name: service.service,
446446
entrypoint: service.entrypoint,
447+
props: service.props,
447448
};
448449
continue;
449450
}
@@ -507,6 +508,9 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): {
507508
}
508509
}
509510

511+
// BUG: We have no way to pass `props` across an external socket, so we
512+
// drop them. We are planning to move away from the multi-process model
513+
// anyway, which will solve the problem.
510514
serviceBindings[service.binding] = {
511515
external: {
512516
address,

0 commit comments

Comments
 (0)