1import type { Rpc } from "@cloudflare/workers-types";
2import type { Input } from "../../Input.ts";
3import type { Worker } from "./Worker.ts";
4
5type WorkerEntrypointTypeId = "Cloudflare.WorkerEntrypoint";
6const WorkerEntrypointTypeId: WorkerEntrypointTypeId =
7 "Cloudflare.WorkerEntrypoint";
8
9export interface WorkerEntrypointOptions {
10 /**
11 * Named entrypoint on the target Worker — the exported
12 * `WorkerEntrypoint` class the binding calls. Omitted → the default
13 * entrypoint (equivalent to binding the Worker directly, but with
14 * {@link props} support).
15 */
16 entrypoint?: string;
17 /**
18 * Properties exposed to the target entrypoint via workerd's
19 * `ctx.props`. Values accept `Output` references and resolve at deploy
20 * time.
21 *
22 * Local dev (`alchemy dev`) delivers these today. On deployed Workers
23 * the Cloudflare API's binding schema does not carry `props` yet, so
24 * they are typed and plumbed but dropped at upload until the distilled
25 * `workers` service adds the field.
26 */
27 props?: Record<string, Input<unknown>>;
28}
29
30/**
31 * A service binding to a specific entrypoint of another Worker — the value
32 * form accepted in an async Worker's `env`. See {@link WorkerEntrypoint}.
33 */
34export interface WorkerEntrypointBinding<
35 Entrypoint extends Rpc.WorkerEntrypointBranded | undefined = undefined,
36> {
37 /** Brand discriminating entrypoint bindings in `env` classification. */
38 readonly kind: WorkerEntrypointTypeId;
39 /** The target Worker resource. */
40 readonly worker: Worker;
41 /** Named entrypoint on the target, or `undefined` for the default. */
42 readonly entrypoint: string | undefined;
43 /** `ctx.props` delivered to the target entrypoint. */
44 readonly props: Record<string, Input<unknown>> | undefined;
45 /** Entry-point instance type used by `InferEnv`; absent at runtime. */
46 readonly "~alchemy/entrypoint"?: Entrypoint;
47}
48
49/**
50 * Bind a specific `WorkerEntrypoint` class exported by another Worker.
51 *
52 * Binding a Worker directly in `env` (`env: { TARGET: worker }`) targets
53 * its *default* entrypoint. A Worker that exposes additional
54 * `WorkerEntrypoint` classes — workerd treats every named class export of
55 * an entry module as an entrypoint — is bound with `WorkerEntrypoint`,
56 * which selects the class by name and can deliver `ctx.props` to it.
57 *
58 * ### Defining the Target Entrypoint
59 * Export a class extending Cloudflare's native `WorkerEntrypoint` from
60 * the target Worker's module. This `Api` class defines the RPC methods
61 * that callers can invoke through a named service binding.
62 *
63 * **Example:** Export the Api class
64 * ```typescript
65 * // target/src/worker.ts
66 * import { WorkerEntrypoint } from "cloudflare:workers";
67 *
68 * export class Api extends WorkerEntrypoint {
69 * async greet(name: string): Promise<string> {
70 * return `hello ${name}`;
71 * }
72 * }
73 *
74 * export default {
75 * async fetch() {
76 * return new Response("ok");
77 * },
78 * };
79 * ```
80 *
81 * ### Binding a Named Entrypoint
82 * Import the exported `Api` class as a type and select its named export
83 * with `"Api"`. Pass its instance type (`Api`, not `typeof Api`) to get
84 * Cloudflare's native `Service<Api>` RPC client. Without a type argument,
85 * the binding is a bare `Fetcher`; the entrypoint name alone cannot
86 * identify the class's type.
87 *
88 * **Example:** Bind and call the exported Api
89 * ```typescript
90 * // alchemy.run.ts
91 * import * as Cloudflare from "alchemy/Cloudflare";
92 * import type { Api } from "./target/src/worker.ts";
93 *
94 * const target = yield* Cloudflare.Worker("Target", {
95 * main: "./target/src/worker.ts",
96 * });
97 *
98 * const caller = yield* Cloudflare.Worker("Caller", {
99 * main: "./caller/src/worker.ts",
100 * env: {
101 * API: Cloudflare.WorkerEntrypoint<Api>(target, "Api"),
102 * },
103 * });
104 * ```
105 *
106 * ```typescript
107 * // caller/src/worker.ts
108 * import type { CallerEnv } from "../../alchemy.run.ts";
109 *
110 * export default {
111 * async fetch(request: Request, env: CallerEnv) {
112 * return new Response(await env.API.greet("alice"));
113 * },
114 * };
115 * ```
116 *
117 * ### Delivering ctx.props
118 * The options form attaches properties the target reads from
119 * `this.ctx.props` — workerd's per-binding configuration channel. `Output`
120 * values resolve at deploy time.
121 *
122 * **Example:** Entrypoint binding with props
123 * ```typescript
124 * env: {
125 * VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, {
126 * entrypoint: "Vendor",
127 * props: { baseUrl: site.url },
128 * }),
129 * }
130 * ```
131 *
132 * @resource
133 * @product Workers
134 * @category Workers & Compute
135 */
136export const WorkerEntrypoint = <
137 Entrypoint extends Rpc.WorkerEntrypointBranded | undefined = undefined,
138>(
139 worker: Worker,
140 entrypointOrOptions?: string | WorkerEntrypointOptions,
141): WorkerEntrypointBinding<NoInfer<Entrypoint>> => {
142 const options =
143 typeof entrypointOrOptions === "string"
144 ? { entrypoint: entrypointOrOptions }
145 : (entrypointOrOptions ?? {});
146 return {
147 kind: WorkerEntrypointTypeId,
148 worker,
149 entrypoint: options.entrypoint,
150 props: options.props,
151 };
152};
153
154/** Structural guard for {@link WorkerEntrypointBinding} `env` values. */
155export const isWorkerEntrypoint = (
156 value: unknown,
157): value is WorkerEntrypointBinding<any> =>
158 typeof value === "object" &&
159 value !== null &&
160 (value as { kind?: unknown }).kind === WorkerEntrypointTypeId;