1import * as Effect from "effect/Effect";
2import { SERVICE_USER_WORKER } from "../internal/constants.ts";
3import * as Plugin from "../Plugin.ts";
4import type { BindingHook } from "../PluginContext.ts";
5import { RegistryProxy } from "../registry/RegistryProxy.ts";
6import { makeRemoteBinding } from "../remote-bindings/RemoteBindings.ts";
7
8export interface LocalServiceProps {
9 /**
10 * Name of the binding.
11 */
12 readonly binding: string;
13 /**
14 * Name of the worker (as registered in the dev registry by another
15 * `cloudflare-runtime` or `wrangler dev` process) to bind to.
16 */
17 readonly scriptName: string;
18 /**
19 * Named entrypoint on the target worker. Defaults to the default entrypoint.
20 */
21 readonly entrypoint?: string;
22 /**
23 * Optional `ctx.props` forwarded to the remote entrypoint.
24 */
25 readonly props?: Record<string, unknown>;
26}
27
28/**
29 * Bind to another locally-running worker by name. The connection is
30 * established cross-process via the workerd debug port and the on-disk
31 * dev registry, so the target worker can be running in a separate
32 * `cloudflare-runtime` or `wrangler dev` process.
33 */
34export const local = ({
35 binding,
36 scriptName,
37 entrypoint,
38 props,
39}: LocalServiceProps): BindingHook<RegistryProxy> =>
40 Plugin.use(RegistryProxy, (proxy) =>
41 proxy.api
42 .subscribe({
43 kind: "worker",
44 scriptName,
45 entrypoint,
46 props,
47 })
48 .pipe(
49 Effect.map((service) => ({
50 name: binding,
51 service,
52 })),
53 ),
54 );
55
56export interface SelfServiceProps {
57 /**
58 * Named entrypoint on the worker. Defaults to the default entrypoint.
59 */
60 readonly entrypoint?: string;
61}
62
63/**
64 * Bind the worker to itself (a same-script service binding, e.g. OpenNext's
65 * `WORKER_SELF_REFERENCE`). Unlike {@link local}, this resolves in-process with
66 * no dev-registry round trip, so it works while the worker is still starting.
67 *
68 * The binding targets the user worker service directly, **bypassing the
69 * middleware chain** (assets router, etc.) that fronts the worker's entry
70 * socket. Requests sent through this binding always reach the worker's own
71 * handlers, even for paths that static assets would otherwise serve. This is
72 * the correct semantic for self-invocation patterns like ISR revalidation,
73 * where the request must hit the worker's `fetch` handler rather than the
74 * assets middleware.
75 */
76export const self = (
77 binding: string,
78 { entrypoint }: SelfServiceProps = {},
79): BindingHook =>
80 Effect.succeed({
81 name: binding,
82 service: {
83 name: SERVICE_USER_WORKER,
84 ...(entrypoint !== undefined ? { entrypoint } : undefined),
85 },
86 });
87
88/**
89 * Bind to a deployed Cloudflare Worker via the remote bindings proxy.
90 */
91export const remote = (name: string, service: string) =>
92 makeRemoteBinding({ name, type: "service", service }, (service) => ({
93 name,
94 service,
95 }));