1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3import { SingleShotGen } from "effect/Utils";
4import * as CoreBinding from "../../Binding.ts";
5import { ProviderModePolicy } from "../../ProviderMode.ts";
6import { taggedFunction } from "../../Util/effect.ts";
7import type { WorkerBinding } from "./WorkerBinding.ts";
8
9/**
10 * A Cloudflare **Worker-only binding** — the plain data value produced by
11 * calling a {@link Service} (e.g. `Cloudflare.Images.Images(name)`).
12 *
13 * These bindings (`Browser`, `Images`, `VersionMetadata`, `RateLimit`,
14 * `Artifacts.Namespace`, …) have **no backing cloud resource**; they are only
15 * configuration on a Worker's script metadata. A binding value is therefore
16 * both:
17 *
18 * - **declarable on a Worker's `env`** — `InferEnv` maps the per-binding type to
19 * its native runtime type, and {@link bindWorkerAsyncBindings} emits its wire
20 * spec via {@link toWorkerBinding}, and
21 * - **`yield*`-able inside an Effect-native Worker** — its iterator attaches the
22 * binding to the surrounding Worker and resolves to the runtime client.
23 */
24export interface Binding<
25 Kind extends string = string,
26 Client = unknown,
27 Service = unknown,
28> {
29 readonly kind: Kind;
30 /** Binding name; the object key when declared on `env`. */
31 readonly name: string;
32 /**
33 * Alchemy-internal: opt this binding out of local emulation in
34 * `alchemy dev` — the `Alchemy.remote()` decoration captured by
35 * {@link pipe} on capabilities with a local simulator (Browser, Images,
36 * Stream). Registered on the host Worker via the binding-data `devRemote`
37 * channel, never on the wire binding itself.
38 */
39 readonly devRemote?: boolean;
40 /** Attach the binding to the surrounding Worker and resolve to the client. */
41 asEffect(): Effect.Effect<Client, never, Service>;
42 [Symbol.iterator](): Generator<Effect.Effect<Client, never, Service>, Client>;
43 /** Wire metadata emitted into the Worker script's `metadata.bindings`. */
44 toWorkerBinding(): WorkerBinding;
45 /**
46 * Lift the binding into an Effect ({@link BindingEffect}) so registration
47 * aspects can decorate it — most notably `Alchemy.remote()`, which opts
48 * the binding out of local emulation in `alchemy dev`:
49 *
50 * ```ts
51 * // Effect-native Worker — resolves to the runtime client:
52 * const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
53 *
54 * // async Worker env — declares the decorated binding:
55 * env: { IMAGES: Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote()) }
56 * ```
57 */
58 pipe(): BindingEffect<this>;
59 pipe<A>(ab: (self: BindingEffect<this>) => A): A;
60 pipe<A, B>(ab: (self: BindingEffect<this>) => A, bc: (a: A) => B): B;
61 pipe<A, B, C>(
62 ab: (self: BindingEffect<this>) => A,
63 bc: (a: A) => B,
64 cd: (b: B) => C,
65 ): C;
66}
67
68/**
69 * A Worker-only {@link Binding} lifted into an Effect by {@link Binding.pipe},
70 * with the ambient {@link ProviderModePolicy} (`Alchemy.remote()`) captured
71 * when it runs.
72 *
73 * Context-adaptive resolution: inside an Effect-native Worker (where the
74 * binding's implementation layer is provided) it registers the binding on the
75 * host and resolves to the runtime `Client` — same one-`yield*` shape as the
76 * bare binding value. Anywhere else (an async Worker's `env`, resolved by the
77 * engine) it resolves to the decorated binding *value*; the
78 * `"~alchemy/Binding"` brand lets `InferEnv` map it to the native runtime
79 * type.
80 */
81export interface BindingEffect<
82 B extends Binding<any, any, any>,
83> extends Effect.Effect<
84 B extends Binding<any, infer Client, any> ? Client : never,
85 never,
86 B extends Binding<any, any, infer Service> ? Service : never
87> {
88 readonly "~alchemy/Kind": "Cloudflare.Workers.BindingEffect";
89 readonly "~alchemy/Binding": B;
90}
91
92/** Any {@link BindingEffect} — the arm admitted by a Worker's `env`. */
93export type AnyBindingEffect = BindingEffect<Binding<any, any, any>>;
94
95/**
96 * The fused tag + callable + type for a Worker-only binding — the same
97 * single-identifier shape as core {@link CoreBinding.Service}, specialized so
98 * the callable produces a {@link Binding} value instead of binding a resource.
99 *
100 * `interface X extends Binding.Service<X, Id, Client>` declares the type;
101 * `const X = Binding.Service<X>({ … })` produces the value that is at once the
102 * Context tag (usable in `Layer.effect(X, …)` / `Effect.provide`), the callable
103 * (`X(props)` → a {@link Binding}), and carries the type.
104 *
105 * ```ts
106 * import * as Binding from "../Workers/Binding.ts";
107 *
108 * export interface Images extends Binding.Service<Images, typeof Id, Client> {
109 * (props?: ImagesProps): Binding.Binding<typeof Id, Client, Images>;
110 * }
111 *
112 * export const Images = Binding.Service<Images>({
113 * id: Id,
114 * defaultName: "IMAGES",
115 * toWorkerBinding: (b) => ({ type: "images", name: b.name }),
116 * });
117 * ```
118 */
119export interface Service<
120 Self,
121 Id extends string,
122 Client,
123> extends CoreBinding.Service<
124 Self,
125 Id,
126 (binding: Binding<Id, Client, Self>) => Effect.Effect<Client>
127> {}
128
129type AnyService = { readonly key: string } & ((...args: any[]) => any);
130
131/**
132 * Build the fused tag + callable value for a Worker-only binding.
133 *
134 * `Self` (the per-binding interface) supplies the tag `id` (`Self["key"]`); the
135 * binding's `Payload` (extra fields beyond `name`, e.g. RateLimit's
136 * `namespaceId`/`simple`) is inferred from `parse`. Omit `parse` for name-only
137 * bindings — `name` is read from the first arg's `.name` property.
138 */
139export const Service = <
140 Self extends AnyService,
141 Payload extends object = Record<never, never>,
142>(config: {
143 /** Tag key + binding `kind` (one identifier for both). */
144 readonly id: Self["key"] & string;
145 /** Default binding name when `props` omits one. */
146 readonly defaultName: string;
147 /**
148 * Derive `name` + payload from the constructor args. Omit for name-only
149 * bindings — `name` is then the first (string) arg, the binding's logical id.
150 */
151 readonly parse?: (...args: any[]) => { name?: string } & Payload;
152 /** Build the wire binding spec from the resolved binding value. */
153 readonly toWorkerBinding: (
154 binding: { readonly name: string } & Payload,
155 ) => WorkerBinding;
156}): Self => {
157 const tag = CoreBinding.Service<Self & { kind: "Service"; key: string }>(
158 config.id as never,
159 );
160 const bind = tag as unknown as (binding: unknown) => Effect.Effect<unknown>;
161
162 const make = (data: Record<PropertyKey, unknown>) => {
163 const self: Record<PropertyKey, unknown> = { ...data };
164 self.toWorkerBinding = () =>
165 config.toWorkerBinding(self as never as { name: string } & Payload);
166 self.asEffect = () => bind(self);
167 self[Symbol.iterator] = () => new SingleShotGen(bind(self));
168 // The lifted form (`BindingEffect`): capture the ambient
169 // `ProviderModePolicy` (`Alchemy.remote()` decoration), then resolve
170 // context-adaptively — to the runtime client when the binding's layer is
171 // in context (inside an Effect-native Worker), to the decorated binding
172 // value otherwise (an async Worker's `env`, resolved by the engine).
173 const lifted = Effect.gen(function* () {
174 const policy = yield* ProviderModePolicy;
175 const decorated =
176 policy === undefined || policy === !!data.devRemote
177 ? self
178 : make({ ...data, devRemote: policy || undefined });
179 const impl = yield* Effect.serviceOption(tag as never);
180 return Option.isSome(impl)
181 ? yield* (impl.value as (b: unknown) => Effect.Effect<unknown>)(
182 decorated,
183 )
184 : decorated;
185 });
186 self.pipe = (...fns: Array<(value: unknown) => unknown>) =>
187 fns.reduce((acc, fn) => fn(acc), lifted as unknown);
188 return self;
189 };
190
191 const construct = (...args: unknown[]) => {
192 const { name, ...payload } =
193 config.parse?.(...args) ??
194 ({ name: args[0] as string | undefined } as {
195 name?: string;
196 } & Payload);
197 return make({
198 kind: config.id,
199 name: name ?? config.defaultName,
200 ...payload,
201 });
202 };
203
204 return taggedFunction(tag as never, construct as never) as unknown as Self;
205};
206
207/** Structural guard for any Worker-only {@link Binding}. */
208export const isBinding = (value: unknown): value is Binding =>
209 typeof value === "object" &&
210 value !== null &&
211 "kind" in value &&
212 typeof (value as { toWorkerBinding?: unknown }).toWorkerBinding ===
213 "function" &&
214 typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
215 "function";