| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Option from "effect/Option"; |
| 3 | import { SingleShotGen } from "effect/Utils"; |
| 4 | import * as CoreBinding from "../../Binding.ts"; |
| 5 | import { ProviderModePolicy } from "../../ProviderMode.ts"; |
| 6 | import { taggedFunction } from "../../Util/effect.ts"; |
| 7 | import 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 | */ |
| 24 | export 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 | */ |
| 81 | export 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`. */ |
| 93 | export 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 | */ |
| 119 | export 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 | |
| 129 | type 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 | */ |
| 139 | export 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}. */ |
| 208 | export 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"; |