diff --git a/packages/alchemy/src/Cloudflare/Workers/InferEnv.ts b/packages/alchemy/src/Cloudflare/Workers/InferEnv.ts index f30063348..d16204e8a 100644 --- a/packages/alchemy/src/Cloudflare/Workers/InferEnv.ts +++ b/packages/alchemy/src/Cloudflare/Workers/InferEnv.ts @@ -24,49 +24,58 @@ export type GetBindingType = ? Flagship : T extends Cloudflare.Assets ? Service - : T extends Rpc + : T extends Cloudflare.WorkerEntrypointBinding< + infer Shape extends object + > ? RpcWireShape & Service - : T extends Cloudflare.D1Database - ? D1Database - : T extends Cloudflare.R2Bucket - ? R2Bucket - : T extends Cloudflare.KVNamespace - ? KVNamespace - : T extends Cloudflare.Queue - ? Queue - : T extends Cloudflare.AiGateway - ? Ai - : T extends Cloudflare.AiSearchInstance - ? AiSearchInstance - : T extends Cloudflare.AiSearchNamespace - ? AiSearchNamespace - : T extends Cloudflare.SendEmail - ? SendEmail - : T extends Cloudflare.AnalyticsEngineDataset - ? AnalyticsEngineDataset - : T extends Cloudflare.Artifacts - ? Artifacts - : T extends Cloudflare.RateLimit - ? RateLimit - : T extends Cloudflare.Images - ? ImagesBinding - : T extends Cloudflare.Browser - ? BrowserRun - : T extends Cloudflare.Hyperdrive - ? Hyperdrive - : T extends Cloudflare.VersionMetadata - ? WorkerVersionMetadata - : T extends Cloudflare.DynamicWorkerLoader - ? Cloudflare.DynamicWorkerLoaderBinding - : T extends Cloudflare.DurableObjectNamespaceLike - ? DurableObjectNamespace< - Exclude - > - : T extends Redacted - ? // redacteds are always stored as secret_text, so are always string - // we JSON.stringify when not a Redacted - string - : T; + : T extends Rpc + ? RpcWireShape & Service + : T extends Cloudflare.WorkflowResource + ? Workflow + : T extends Cloudflare.D1Database + ? D1Database + : T extends Cloudflare.R2Bucket + ? R2Bucket + : T extends Cloudflare.KVNamespace + ? KVNamespace + : T extends Cloudflare.Queue + ? Queue + : T extends Cloudflare.AiGateway + ? Ai + : T extends Cloudflare.AiSearchInstance + ? AiSearchInstance + : T extends Cloudflare.AiSearchNamespace + ? AiSearchNamespace + : T extends Cloudflare.SendEmail + ? SendEmail + : T extends Cloudflare.AnalyticsEngineDataset + ? AnalyticsEngineDataset + : T extends Cloudflare.Artifacts + ? Artifacts + : T extends Cloudflare.RateLimit + ? RateLimit + : T extends Cloudflare.Images + ? ImagesBinding + : T extends Cloudflare.Browser + ? BrowserRun + : T extends Cloudflare.Hyperdrive + ? Hyperdrive + : T extends Cloudflare.VersionMetadata + ? WorkerVersionMetadata + : T extends Cloudflare.DynamicWorkerLoader + ? Cloudflare.DynamicWorkerLoaderBinding + : T extends Cloudflare.DurableObjectNamespaceLike + ? DurableObjectNamespace< + Exclude< + T["Shape"], + undefined + > + > + : T extends Redacted + ? // redacteds are always stored as secret_text, so are always string + // we JSON.stringify when not a Redacted + string + : T; /** * Cloudflare service-binding wire shape for an Effect-native Worker. diff --git a/packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.ts b/packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.ts index 91e808e29..3b1f4443e 100644 --- a/packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.ts +++ b/packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.ts @@ -29,7 +29,12 @@ import { isDynamicWorkerLoader } from "./DynamicWorkerLoader.ts"; import { isVersionMetadata } from "./VersionMetadata.ts"; import type { WorkerBindingProps } from "./Worker.ts"; import { isWorker, type Worker, type WorkerProps } from "./Worker.ts"; -import type { WorkerBinding, WorkerBindingResource } from "./WorkerBinding.ts"; +import type { + WorkerBinding, + WorkerBindingResource, + WorkerEntrypointBinding, +} from "./WorkerBinding.ts"; +import type { WorkflowResource } from "./Workflow.ts"; export const bindWorkerAsyncBindings = Effect.fnUntraced(function* ( resource: Worker, @@ -75,7 +80,7 @@ type BindingSpec = InputProps< Exclude[number] >; -const toBinding = ( +export const toBinding = ( bindingName: string, binding: WorkerBindingResource, ): BindingSpec => { @@ -219,6 +224,21 @@ const toBinding = ( name: bindingName, service: binding.workerName, }; + } else if (isWorkerEntrypointBinding(binding)) { + return { + type: "service", + name: bindingName, + service: binding.service, + entrypoint: binding.entrypoint, + }; + } else if (isWorkflowResource(binding)) { + return { + type: "workflow", + name: bindingName, + workflowName: binding.workflowName, + className: binding.className, + scriptName: binding.scriptName, + }; } else if (isVectorizeIndex(binding)) { return { type: "vectorize", @@ -251,6 +271,20 @@ const toBinding = ( } }; +const hasBindingType = (binding: unknown, type: string) => + typeof binding === "object" && + binding !== null && + "BindingType" in binding && + binding.BindingType === type; + +const isWorkerEntrypointBinding = ( + binding: unknown, +): binding is WorkerEntrypointBinding => + hasBindingType(binding, "Cloudflare.WorkerEntrypointBinding"); + +const isWorkflowResource = (binding: unknown): binding is WorkflowResource => + hasBindingType(binding, "Cloudflare.Workflow"); + export const getCronBindings = ( bindings: ReadonlyArray>, ) => Array.from(new Set(bindings.flatMap((b) => b.data.crons ?? []))); diff --git a/packages/alchemy/src/Cloudflare/Workers/WorkerBinding.ts b/packages/alchemy/src/Cloudflare/Workers/WorkerBinding.ts index 3b0a11fcd..b9b292eb7 100644 --- a/packages/alchemy/src/Cloudflare/Workers/WorkerBinding.ts +++ b/packages/alchemy/src/Cloudflare/Workers/WorkerBinding.ts @@ -28,6 +28,7 @@ import type { DurableObjectNamespaceLike } from "./DurableObjectNamespace.ts"; import type { DynamicWorkerLoader } from "./DynamicWorkerLoader.ts"; import { makeRpcStub } from "./Rpc.ts"; import type { VersionMetadata } from "./VersionMetadata.ts"; +import type { WorkflowResource } from "./Workflow.ts"; import { isWorker, Worker, WorkerEnvironment } from "./Worker.ts"; export type WorkerBinding = Exclude< @@ -40,6 +41,12 @@ export type WorkerSettingsBinding = Exclude< null | undefined >[number]; +export interface WorkerEntrypointBinding { + readonly BindingType: "Cloudflare.WorkerEntrypointBinding"; + readonly service: string; + readonly entrypoint: string; +} + export type WorkerBindingResource = // Config values | Json @@ -65,7 +72,9 @@ export type WorkerBindingResource = | VectorizeIndex | Secret | Worker + | WorkerEntrypointBinding | DynamicWorkerLoader + | WorkflowResource | VersionMetadata | DurableObjectNamespaceLike; diff --git a/packages/alchemy/test/Cloudflare/Workers/WorkerAsyncBindings.test.ts b/packages/alchemy/test/Cloudflare/Workers/WorkerAsyncBindings.test.ts new file mode 100644 index 000000000..165275f89 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Workers/WorkerAsyncBindings.test.ts @@ -0,0 +1,59 @@ +import * as Cloudflare from "@/Cloudflare"; +import { toBinding } from "@/Cloudflare/Workers/WorkerAsyncBindings.ts"; +import * as Effect from "effect/Effect"; +import { describe, expect, it } from "vitest"; + +interface WorkflowActionsEntrypoint { + run(name: string): Effect.Effect; +} + +type Assert = T; +type IsAssignable = A extends B ? true : false; + +const workflowActions: Cloudflare.WorkerEntrypointBinding = + { + BindingType: "Cloudflare.WorkerEntrypointBinding", + service: "api-worker", + entrypoint: "WorkflowActions", + }; + +const dynamicWorkflow = { + BindingType: "Cloudflare.Workflow", + workflowName: "dynamic-workflow", + className: "DynamicUserWorkflow", + scriptName: "api-worker", +} as unknown as Cloudflare.WorkflowResource; + +type _EntrypointIsService = Assert< + IsAssignable, Service> +>; +type _EntrypointMethodIsRpc = Assert< + IsAssignable< + Cloudflare.GetBindingType["run"], + (name: string) => Promise + > +>; +type _WorkflowIsRuntimeWorkflow = Assert< + IsAssignable, Workflow> +>; + +describe("toBinding", () => { + it("converts named worker entrypoint bindings to service bindings", () => { + expect(toBinding("WORKFLOW_ACTIONS", workflowActions)).toEqual({ + type: "service", + name: "WORKFLOW_ACTIONS", + service: "api-worker", + entrypoint: "WorkflowActions", + }); + }); + + it("converts WorkflowResource values to workflow bindings", () => { + expect(toBinding("DYNAMIC_WORKFLOW", dynamicWorkflow)).toEqual({ + type: "workflow", + name: "DYNAMIC_WORKFLOW", + workflowName: "dynamic-workflow", + className: "DynamicUserWorkflow", + scriptName: "api-worker", + }); + }); +});