| 1 | import type * as workers from "@distilled.cloud/cloudflare/workers"; |
| 2 | import * as Config from "effect/Config"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Redacted from "effect/Redacted"; |
| 5 | import type { Json } from "effect/Schema"; |
| 6 | import type * as Output from "../../Output.ts"; |
| 7 | import type { Rpc } from "../../Rpc.ts"; |
| 8 | import { isYieldableEffectLike } from "../../Util/effect.ts"; |
| 9 | import type { Gateway as AiGateway } from "../AI/Gateway.ts"; |
| 10 | import type { SearchInstance } from "../AI/SearchInstance.ts"; |
| 11 | import type { SearchNamespace } from "../AI/SearchNamespace.ts"; |
| 12 | import { Dataset } from "../AnalyticsEngine/Dataset.ts"; |
| 13 | import type { Namespace as ArtifactsNamespace } from "../Artifacts/Namespace.ts"; |
| 14 | import type { Container } from "../Containers/Container.ts"; |
| 15 | import type { Database as D1Database } from "../D1/Database.ts"; |
| 16 | import { SendEmail } from "../Email/SendEmail.ts"; |
| 17 | import type { App as FlagshipApp } from "../Flagship/App.ts"; |
| 18 | import type { Connection as Hyperdrive } from "../Hyperdrive/Connection.ts"; |
| 19 | import type { ImagesBinding } from "../Images/ImagesBinding.ts"; |
| 20 | import type { Namespace } from "../KV/Namespace.ts"; |
| 21 | import type { LegacyPipeline } from "../Pipelines/LegacyPipeline.ts"; |
| 22 | import type { Stream as PipelinesStream } from "../Pipelines/Stream.ts"; |
| 23 | import type { Queue } from "../Queues/Queue.ts"; |
| 24 | import type { Bucket } from "../R2/Bucket.ts"; |
| 25 | import type { S3Credentials } from "../R2/S3Credentials.ts"; |
| 26 | import type { Secret } from "../SecretsStore/Secret.ts"; |
| 27 | import type { StreamBinding } from "../Stream/StreamBinding.ts"; |
| 28 | import type { Index as VectorizeIndex } from "../Vectorize/VectorizeIndex.ts"; |
| 29 | import type { VpcService } from "../VpcService/VpcService.ts"; |
| 30 | import type { VpcServiceLookup } from "../VpcService/VpcServiceLookup.ts"; |
| 31 | import type { DispatchNamespace } from "../WorkersForPlatforms/DispatchNamespace.ts"; |
| 32 | import type { WorkflowLike } from "../Workflows/Workflow.ts"; |
| 33 | import type { AIBinding } from "./AIBinding.ts"; |
| 34 | import type { AnyBindingEffect } from "./Binding.ts"; |
| 35 | import type { Assets } from "./Assets.ts"; |
| 36 | import type { URLEffect } from "./Worker.ts"; |
| 37 | import type { BrowserBinding } from "./BrowserBinding.ts"; |
| 38 | import type { DurableObjectLike } from "./DurableObject.ts"; |
| 39 | import type { RateLimitBinding } from "./RateLimitBinding.ts"; |
| 40 | import { makeRpcStub } from "./Rpc.ts"; |
| 41 | import type { SecretKeyBinding } from "./SecretKeyBinding.ts"; |
| 42 | import type { VersionMetadataBinding } from "./VersionMetadataBinding.ts"; |
| 43 | import { Worker, WorkerEnvironment } from "./Worker.ts"; |
| 44 | import type { WorkerEntrypointBinding } from "./WorkerEntrypoint.ts"; |
| 45 | import type { WorkerLoader } from "./WorkerLoader.ts"; |
| 46 | |
| 47 | type DistilledWorkerBinding = Exclude< |
| 48 | workers.PutScriptRequest["metadata"]["bindings"], |
| 49 | undefined |
| 50 | >[number]; |
| 51 | |
| 52 | /** |
| 53 | * The `durable_object_namespace` metadata binding extended with alchemy-only |
| 54 | * transfer metadata. `transferredFrom` names the Worker(s) — by logical id in |
| 55 | * this stack + stage, or by physical script name — that previously hosted the |
| 56 | * class, so the provider can drive Cloudflare's data-preserving |
| 57 | * `transferred_classes` migration. It is stripped from the binding before the |
| 58 | * script upload — Cloudflare never sees it. |
| 59 | */ |
| 60 | export type DurableObjectNamespaceWorkerBinding = Extract< |
| 61 | DistilledWorkerBinding, |
| 62 | { type: "durable_object_namespace" } |
| 63 | > & { |
| 64 | transferredFrom?: string | string[]; |
| 65 | }; |
| 66 | |
| 67 | /** |
| 68 | * Alchemy-only binding: the host Worker's own public URL (`Worker.URL`). The |
| 69 | * provider resolves the URL the Worker will be served at (first custom domain, |
| 70 | * else its `workers.dev` URL) and lowers this into a `plain_text` binding |
| 71 | * before the script upload — Cloudflare never sees this type. |
| 72 | */ |
| 73 | export interface SelfUrlWorkerBinding { |
| 74 | type: "self_url"; |
| 75 | name: string; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Alchemy-only binding: a service binding that points at the host Worker |
| 80 | * ITSELF (`Worker.Self`). The provider lowers this into a |
| 81 | * `service` binding targeting the Worker's own physical name just before |
| 82 | * the script upload — Cloudflare never sees this type. In local dev it |
| 83 | * lowers to the runtime's in-process self service (bypassing the assets |
| 84 | * middleware), matching production semantics. |
| 85 | * |
| 86 | * The canonical consumer is OpenNext's `WORKER_SELF_REFERENCE` (the ISR |
| 87 | * revalidation queue re-fetches the worker through it). |
| 88 | */ |
| 89 | export interface SelfServiceWorkerBinding { |
| 90 | type: "self_service"; |
| 91 | name: string; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Alchemy-only, dev-only binding: R2 S3 credentials for a locally-emulated |
| 96 | * (`dev:`) bucket (`Cloudflare.R2.S3Credentials`). The local runtime lowers |
| 97 | * it into a text binding holding the credentials JSON for the Worker's local |
| 98 | * S3 endpoint (`{worker url}/cdn-cgi/local/r2/s3`) and serves the bucket on |
| 99 | * that endpoint. Deployed Workers receive a `secret_text` binding instead — |
| 100 | * Cloudflare never sees this type. |
| 101 | */ |
| 102 | export interface R2S3CredentialsWorkerBinding { |
| 103 | type: "r2_s3_credentials"; |
| 104 | name: string; |
| 105 | bucketName: string; |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * The `queue` metadata binding extended with the alchemy-only `queueId`. |
| 110 | * The local worker provider uses it to discriminate a locally-emulated |
| 111 | * queue (`dev:` id → local broker) from an `Alchemy.remote()` queue in dev |
| 112 | * (real id → remote-proxied producer). Stripped from the binding before |
| 113 | * the script upload — Cloudflare never sees it. |
| 114 | */ |
| 115 | export type QueueWorkerBinding = Extract< |
| 116 | DistilledWorkerBinding, |
| 117 | { type: "queue" } |
| 118 | > & { |
| 119 | queueId?: string; |
| 120 | /** |
| 121 | * Alchemy-only (stripped before upload): dev-mode remote-producer shim |
| 122 | * for an `Alchemy.remote()` queue. Cloudflare preview sessions reject |
| 123 | * queue bindings, so a local worker produces to the live queue through |
| 124 | * this deployed shim worker instead (see `Queues/QueueShim.ts`). |
| 125 | */ |
| 126 | shim?: { |
| 127 | /** The shim worker's workers.dev URL. */ |
| 128 | url: string | undefined; |
| 129 | /** Bearer token the shim requires. */ |
| 130 | token: Redacted.Redacted<string> | string; |
| 131 | }; |
| 132 | }; |
| 133 | |
| 134 | /** |
| 135 | * The `service` metadata binding extended with workerd's `ctx.props`. |
| 136 | * `props` is what a `Cloudflare.WorkerEntrypoint(worker, { props })` env |
| 137 | * entry lowers to; the local runtime delivers it to the target entrypoint. |
| 138 | * The Cloudflare API's binding schema does not carry the field yet, so on |
| 139 | * live uploads it is dropped at encode until the distilled `workers` |
| 140 | * service adds it. |
| 141 | */ |
| 142 | export type ServiceWorkerBinding = Extract< |
| 143 | DistilledWorkerBinding, |
| 144 | { type: "service" } |
| 145 | > & { |
| 146 | props?: Record<string, unknown>; |
| 147 | }; |
| 148 | |
| 149 | /** |
| 150 | * The wire-shape binding union the Cloudflare API accepts — {@link WorkerBinding} |
| 151 | * minus the alchemy-only members that must be lowered before upload. |
| 152 | */ |
| 153 | export type WireWorkerBinding = Exclude< |
| 154 | WorkerBinding, |
| 155 | SelfUrlWorkerBinding | SelfServiceWorkerBinding | R2S3CredentialsWorkerBinding |
| 156 | >; |
| 157 | |
| 158 | export type WorkerBinding = |
| 159 | | Exclude< |
| 160 | DistilledWorkerBinding, |
| 161 | | { type: "durable_object_namespace" } |
| 162 | | { type: "queue" } |
| 163 | | { type: "service" } |
| 164 | > |
| 165 | | DurableObjectNamespaceWorkerBinding |
| 166 | | QueueWorkerBinding |
| 167 | | ServiceWorkerBinding |
| 168 | | SelfUrlWorkerBinding |
| 169 | | SelfServiceWorkerBinding |
| 170 | | R2S3CredentialsWorkerBinding; |
| 171 | |
| 172 | /** |
| 173 | * Drop dev-only binding sentinels ({@link R2S3CredentialsWorkerBinding}) |
| 174 | * before a live upload. They are only emitted for Workers running locally, |
| 175 | * so a live Worker never carries one; this narrows the type for the wire. |
| 176 | */ |
| 177 | export const withoutDevOnlyBindings = <B extends WorkerBinding>( |
| 178 | bindings: ReadonlyArray<B>, |
| 179 | ): Array<Exclude<B, R2S3CredentialsWorkerBinding>> => |
| 180 | bindings.filter( |
| 181 | (binding): binding is Exclude<B, R2S3CredentialsWorkerBinding> => |
| 182 | binding.type !== "r2_s3_credentials", |
| 183 | ); |
| 184 | |
| 185 | export type WorkerSettingsBinding = Exclude< |
| 186 | workers.GetScriptScriptAndVersionSettingResponse["bindings"], |
| 187 | null | undefined |
| 188 | >[number]; |
| 189 | |
| 190 | export type WorkerBindingResource = |
| 191 | // Config values |
| 192 | | Json |
| 193 | | Redacted.Redacted<Json> |
| 194 | | Config.Config<Json> |
| 195 | // Outputs that resolve to a plain env value (e.g. `Alchemy.makeRandom`, |
| 196 | // `Output.literal`), classified by their resolved value at deploy time. |
| 197 | // Whole-resource Outputs (`Output.of(bucket)`) cannot be excluded here: |
| 198 | // `Input<T>` wraps this whole union in `Output<T>`, so any Output whose |
| 199 | // A is structurally Json (most resource attribute shapes) is admitted |
| 200 | // upstream regardless of this arm. `bindWorkerAsyncBindings` rejects |
| 201 | // them at deploy time instead. |
| 202 | | Output.Output<Json | Redacted.Redacted<Json>, unknown> |
| 203 | // CF resources |
| 204 | | Assets |
| 205 | | Bucket |
| 206 | | S3Credentials |
| 207 | | D1Database |
| 208 | | Namespace |
| 209 | | Queue |
| 210 | | AiGateway |
| 211 | | AIBinding |
| 212 | | SearchInstance |
| 213 | | SearchNamespace |
| 214 | | Dataset |
| 215 | | SendEmail |
| 216 | | ArtifactsNamespace |
| 217 | | RateLimitBinding |
| 218 | | SecretKeyBinding |
| 219 | | BrowserBinding |
| 220 | | FlagshipApp |
| 221 | | ImagesBinding |
| 222 | | StreamBinding |
| 223 | | PipelinesStream |
| 224 | | LegacyPipeline |
| 225 | | Hyperdrive |
| 226 | | VectorizeIndex |
| 227 | | Secret |
| 228 | | Worker |
| 229 | | WorkerEntrypointBinding<any> |
| 230 | | WorkerLoader |
| 231 | | VersionMetadataBinding |
| 232 | // The Worker's own URL (`Worker.URL`). |
| 233 | | URLEffect |
| 234 | // A Worker-only binding lifted by `.pipe(Alchemy.remote())`. |
| 235 | | AnyBindingEffect |
| 236 | | DispatchNamespace |
| 237 | | DurableObjectLike<any> |
| 238 | | WorkflowLike<any> |
| 239 | | VpcService |
| 240 | | VpcServiceLookup |
| 241 | // A Container bound directly in `env` declares a container-backed Durable |
| 242 | // Object class (DO namespace binding + ContainerApplication in one). |
| 243 | | Container.Decl.Any; |
| 244 | |
| 245 | export type WorkerBindings = { |
| 246 | [bindingName in string]: WorkerBindingResource; |
| 247 | }; |
| 248 | |
| 249 | export const bindWorker = Effect.fn(function* <Shape, Req = never>( |
| 250 | workerEff: |
| 251 | | (Worker & Rpc<Shape>) |
| 252 | | Effect.Effect<Worker & Rpc<Shape>, never, Req>, |
| 253 | ) { |
| 254 | // Worker classes and regular Effects are both yieldable here. |
| 255 | const worker = isYieldableEffectLike(workerEff) |
| 256 | ? yield* workerEff as Effect.Effect<Worker & Rpc<Shape>, never, Req> |
| 257 | : workerEff; |
| 258 | const self = yield* Worker; |
| 259 | yield* self.bind`${worker}`({ |
| 260 | bindings: [ |
| 261 | { |
| 262 | type: "service", |
| 263 | name: worker.LogicalId, |
| 264 | service: worker.workerName, |
| 265 | }, |
| 266 | ], |
| 267 | }); |
| 268 | |
| 269 | // `bindWorker` runs at *init* phase (both at plantime and at runtime |
| 270 | // cold-start). `WorkerEnvironment` only exists at exec phase on the |
| 271 | // deployed worker, so we hand `makeRpcStub` an `Effect<stub>` that |
| 272 | // resolves the binding lazily on each method call. |
| 273 | const stubEff = WorkerEnvironment.pipe( |
| 274 | Effect.map((env) => (env as Record<string, unknown>)[worker.LogicalId]), |
| 275 | ); |
| 276 | return makeRpcStub<Shape>(stubEff); |
| 277 | }); |