Skip to content

Commit 875197a

Browse files
authored
Support JSRPC for remote bindings (#10249)
* Support RPC bindings Create yellow-kangaroos-bathe.md fix checks fix checks Increase timeouts more logging static workers static workers pre-compile proxy server worker * Mark cloudflare:* as external in CI tests * address comments
1 parent ddadb93 commit 875197a

File tree

35 files changed

+887
-233
lines changed

35 files changed

+887
-233
lines changed

.changeset/yellow-kangaroos-bathe.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"miniflare": patch
3+
"wrangler": patch
4+
---
5+
6+
Support JSRPC for remote bindings. This unlocks:
7+
- JSRPC over Service Bindings
8+
- JSRPC over Dispatch Namespace Bindings
9+
- Email
10+
- Pipelines

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,4 @@ dist/**
230230
.env*
231231
!.env.example
232232
.node-cache/
233+
!vendor/jsrpc/dist

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ fixtures/redirected-config-worker/build/*
4949
fixtures/redirected-config-worker-with-environments/build/*
5050

5151
packages/vite-plugin-cloudflare/playground/**/*.d.ts
52+
53+
vendor/jsrpc

packages/miniflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@ava/typescript": "^4.1.0",
6161
"@cloudflare/cli": "workspace:*",
6262
"@cloudflare/containers-shared": "workspace:*",
63+
"@cloudflare/jsrpc": "link:../../vendor/jsrpc",
6364
"@cloudflare/kv-asset-handler": "workspace:*",
6465
"@cloudflare/workers-shared": "workspace:*",
6566
"@cloudflare/workers-types": "catalog:default",

packages/miniflare/src/plugins/dispatch-namespace/index.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import LOCAL_DISPATCH_NAMESPACE from "worker:dispatch-namespace/dispatch-namespa
33
import { z } from "zod";
44
import { Worker_Binding } from "../../runtime";
55
import {
6-
getUserBindingServiceName,
76
Plugin,
87
ProxyNodeBinding,
9-
remoteProxyClientWorker,
108
RemoteProxyConnectionString,
119
} from "../shared";
1210

@@ -35,20 +33,23 @@ export const DISPATCH_NAMESPACE_PLUGIN: Plugin<
3533
const bindings = Object.entries(
3634
options.dispatchNamespaces
3735
).map<Worker_Binding>(([name, config]) => {
36+
assert(
37+
config.remoteProxyConnectionString,
38+
"Dispatch Namespace bindings only support running remotely"
39+
);
40+
3841
return {
3942
name,
4043
wrapped: {
4144
moduleName: `${DISPATCH_NAMESPACE_PLUGIN_NAME}:local-dispatch-namespace`,
4245
innerBindings: [
4346
{
44-
name: "fetcher",
45-
service: {
46-
name: getUserBindingServiceName(
47-
DISPATCH_NAMESPACE_PLUGIN_NAME,
48-
config.namespace,
49-
config.remoteProxyConnectionString
50-
),
51-
},
47+
name: "remoteProxyConnectionString",
48+
text: config.remoteProxyConnectionString.href,
49+
},
50+
{
51+
name: "binding",
52+
text: name,
5253
},
5354
],
5455
},
@@ -67,28 +68,8 @@ export const DISPATCH_NAMESPACE_PLUGIN: Plugin<
6768
])
6869
);
6970
},
70-
async getServices({ options }) {
71-
if (!options.dispatchNamespaces) {
72-
return [];
73-
}
74-
75-
return Object.entries(options.dispatchNamespaces).map(([name, config]) => {
76-
assert(
77-
config.remoteProxyConnectionString,
78-
"Dispatch Namespace bindings only support running remotely"
79-
);
80-
return {
81-
name: getUserBindingServiceName(
82-
DISPATCH_NAMESPACE_PLUGIN_NAME,
83-
config.namespace,
84-
config.remoteProxyConnectionString
85-
),
86-
worker: remoteProxyClientWorker(
87-
config.remoteProxyConnectionString,
88-
name
89-
),
90-
};
91-
});
71+
async getServices() {
72+
return [];
9273
},
9374
getExtensions({ options }) {
9475
if (!options.some((o) => o.dispatchNamespaces)) {

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

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import { Service, Worker_Binding } from "../../runtime";
55
import {
66
getUserBindingServiceName,
77
Plugin,
8+
remoteProxyClientWorker,
9+
RemoteProxyConnectionString,
810
WORKER_BINDING_SERVICE_LOOPBACK,
911
} from "../shared";
1012

1113
// Define the mutually exclusive schema
1214
const EmailBindingOptionsSchema = z
1315
.object({
1416
name: z.string(),
17+
remoteProxyConnectionString: z
18+
.custom<RemoteProxyConnectionString>()
19+
.optional(),
1520
})
1621
.and(
1722
z.union([
@@ -53,10 +58,12 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
5358

5459
const sendEmailBindings = options.email.send_email;
5560

56-
return sendEmailBindings.map(({ name }) => ({
61+
return sendEmailBindings.map(({ name, remoteProxyConnectionString }) => ({
5762
name,
5863
service: {
59-
entrypoint: "SendEmailBinding",
64+
entrypoint: remoteProxyConnectionString
65+
? undefined
66+
: "SendEmailBinding",
6067
name: getUserBindingServiceName(SERVICE_SEND_EMAIL_WORKER_PREFIX, name),
6168
},
6269
}));
@@ -67,22 +74,25 @@ export const EMAIL_PLUGIN: Plugin<typeof EmailOptionsSchema> = {
6774
async getServices(args) {
6875
const services: Service[] = [];
6976

70-
for (const { name, ...config } of args.options.email?.send_email ?? []) {
77+
for (const { name, remoteProxyConnectionString, ...config } of args.options
78+
.email?.send_email ?? []) {
7179
services.push({
7280
name: getUserBindingServiceName(SERVICE_SEND_EMAIL_WORKER_PREFIX, name),
73-
worker: {
74-
compatibilityDate: "2025-03-17",
75-
modules: [
76-
{
77-
name: "send_email.mjs",
78-
esModule: SEND_EMAIL_BINDING(),
81+
worker: remoteProxyConnectionString
82+
? remoteProxyClientWorker(remoteProxyConnectionString, name)
83+
: {
84+
compatibilityDate: "2025-03-17",
85+
modules: [
86+
{
87+
name: "send_email.mjs",
88+
esModule: SEND_EMAIL_BINDING(),
89+
},
90+
],
91+
bindings: [
92+
...buildJsonBindings(config),
93+
WORKER_BINDING_SERVICE_LOOPBACK,
94+
],
7995
},
80-
],
81-
bindings: [
82-
...buildJsonBindings(config),
83-
WORKER_BINDING_SERVICE_LOOPBACK,
84-
],
85-
},
8696
});
8797
}
8898

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

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
import SCRIPT_PIPELINE_OBJECT from "worker:pipelines/pipeline";
22
import { z } from "zod";
33
import { Service } from "../../runtime";
4-
import { namespaceKeys, Plugin, ProxyNodeBinding } from "../shared";
4+
import {
5+
namespaceKeys,
6+
Plugin,
7+
ProxyNodeBinding,
8+
remoteProxyClientWorker,
9+
RemoteProxyConnectionString,
10+
} from "../shared";
511

612
export const PipelineOptionsSchema = z.object({
7-
pipelines: z.union([z.record(z.string()), z.string().array()]).optional(),
13+
pipelines: z
14+
.union([
15+
z.record(z.string()),
16+
z.string().array(),
17+
z.record(
18+
z.object({
19+
pipeline: z.string(),
20+
remoteProxyConnectionString: z
21+
.custom<RemoteProxyConnectionString>()
22+
.optional(),
23+
})
24+
),
25+
])
26+
.optional(),
827
});
928

1029
export const PIPELINES_PLUGIN_NAME = "pipelines";
@@ -14,7 +33,7 @@ export const PIPELINE_PLUGIN: Plugin<typeof PipelineOptionsSchema> = {
1433
options: PipelineOptionsSchema,
1534
getBindings(options) {
1635
const pipelines = bindingEntries(options.pipelines);
17-
return pipelines.map<Service>(([name, id]) => ({
36+
return pipelines.map<Service>(([name, { id }]) => ({
1837
name,
1938
service: { name: `${SERVICE_PIPELINE_PREFIX}:${id}` },
2039
}));
@@ -29,18 +48,23 @@ export const PIPELINE_PLUGIN: Plugin<typeof PipelineOptionsSchema> = {
2948
const pipelines = bindingEntries(options.pipelines);
3049

3150
const services = [];
32-
for (const pipeline of pipelines) {
51+
for (const [bindingName, pipeline] of pipelines) {
3352
services.push({
34-
name: `${SERVICE_PIPELINE_PREFIX}:${pipeline[1]}`,
35-
worker: {
36-
compatibilityDate: "2024-12-30",
37-
modules: [
38-
{
39-
name: "pipeline.worker.js",
40-
esModule: SCRIPT_PIPELINE_OBJECT(),
53+
name: `${SERVICE_PIPELINE_PREFIX}:${pipeline.id}`,
54+
worker: pipeline.remoteProxyConnectionString
55+
? remoteProxyClientWorker(
56+
pipeline.remoteProxyConnectionString,
57+
bindingName
58+
)
59+
: {
60+
compatibilityDate: "2024-12-30",
61+
modules: [
62+
{
63+
name: "pipeline.worker.js",
64+
esModule: SCRIPT_PIPELINE_OBJECT(),
65+
},
66+
],
4167
},
42-
],
43-
},
4468
});
4569
}
4670

@@ -50,16 +74,41 @@ export const PIPELINE_PLUGIN: Plugin<typeof PipelineOptionsSchema> = {
5074

5175
function bindingEntries(
5276
namespaces?:
53-
| Record<string, { pipelineName: string }>
77+
| Record<
78+
string,
79+
{
80+
pipeline: string;
81+
remoteProxyConnectionString?: RemoteProxyConnectionString;
82+
}
83+
>
5484
| string[]
5585
| Record<string, string>
56-
): [bindingName: string, id: string][] {
86+
): [
87+
bindingName: string,
88+
{ id: string; remoteProxyConnectionString?: RemoteProxyConnectionString },
89+
][] {
5790
if (Array.isArray(namespaces)) {
58-
return namespaces.map((bindingName) => [bindingName, bindingName]);
91+
return namespaces.map((bindingName) => [bindingName, { id: bindingName }]);
5992
} else if (namespaces !== undefined) {
60-
return Object.entries(namespaces).map(([name, opts]) => [
93+
return (
94+
Object.entries(namespaces) as [
95+
string,
96+
(
97+
| string
98+
| {
99+
pipeline: string;
100+
remoteProxyConnectionString?: RemoteProxyConnectionString;
101+
}
102+
),
103+
][]
104+
).map(([name, opts]) => [
61105
name,
62-
typeof opts === "string" ? opts : opts.pipelineName,
106+
typeof opts === "string"
107+
? { id: opts }
108+
: {
109+
id: opts.pipeline,
110+
remoteProxyConnectionString: opts.remoteProxyConnectionString,
111+
},
63112
]);
64113
} else {
65114
return [];

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export class ProxyNodeBinding {
131131
}
132132

133133
export function namespaceKeys(
134-
namespaces?: Record<string, string | { id: string }> | string[]
134+
namespaces?: Record<string, unknown> | string[]
135135
): string[] {
136136
if (Array.isArray(namespaces)) {
137137
return namespaces;
Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,48 @@
1-
interface Env {
2-
fetcher: Fetcher;
3-
}
1+
import { newWebSocketRpcSession } from "@cloudflare/jsrpc";
2+
import { makeFetch } from "../shared/remote-bindings-utils";
43

5-
class LocalDispatchNamespace implements DispatchNamespace {
6-
constructor(private env: Env) {}
7-
get(
8-
name: string,
9-
args?: { [key: string]: any },
10-
options?: DynamicDispatchOptions
11-
): Fetcher {
12-
return {
13-
...this.env.fetcher,
14-
fetch: (
15-
input: RequestInfo | URL,
16-
init?: RequestInit
17-
): Promise<Response> => {
18-
const request = new Request(input, init);
19-
request.headers.set(
20-
"MF-Dispatch-Namespace-Options",
21-
JSON.stringify({ name, args, options })
22-
);
23-
return this.env.fetcher.fetch(request);
24-
},
25-
};
26-
}
4+
interface Env {
5+
remoteProxyConnectionString: string;
6+
binding: string;
277
}
288

299
export default function (env: Env) {
30-
return new LocalDispatchNamespace(env);
10+
return {
11+
get(
12+
name: string,
13+
args?: { [key: string]: any },
14+
options?: DynamicDispatchOptions
15+
): Fetcher {
16+
const url = new URL(env.remoteProxyConnectionString);
17+
url.protocol = "ws:";
18+
url.searchParams.set("MF-Binding", env.binding);
19+
url.searchParams.set(
20+
"MF-Dispatch-Namespace-Options",
21+
JSON.stringify({ name, args, options })
22+
);
23+
const stub = newWebSocketRpcSession(url.href);
24+
25+
return new Proxy(stub, {
26+
get(_, p) {
27+
// We don't want to wrap direct .fetch() calls on a customer worker in a JSRPC layer
28+
// Instead, intercept accesses to the specific `fetch` key, and send them directly
29+
if (p === "fetch") {
30+
return makeFetch(
31+
env.remoteProxyConnectionString,
32+
env.binding,
33+
new Headers({
34+
"MF-Dispatch-Namespace-Options": JSON.stringify({
35+
name,
36+
args,
37+
options,
38+
}),
39+
})
40+
);
41+
}
42+
43+
return Reflect.get(stub, p);
44+
},
45+
}) as Service;
46+
},
47+
} satisfies DispatchNamespace;
3148
}

0 commit comments

Comments
 (0)