Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 51 additions & 42 deletions packages/alchemy/src/Cloudflare/Workers/InferEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,58 @@ export type GetBindingType<T> =
? Flagship
: T extends Cloudflare.Assets
? Service
: T extends Rpc<infer Shape extends object>
: T extends Cloudflare.WorkerEntrypointBinding<
infer Shape extends object
>
? RpcWireShape<Shape> & Service
: T extends Cloudflare.D1Database
? D1Database
: T extends Cloudflare.R2Bucket
? R2Bucket
: T extends Cloudflare.KVNamespace
? KVNamespace
: T extends Cloudflare.Queue
? Queue<unknown>
: 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<any>
? // redacteds are always stored as secret_text, so are always string
// we JSON.stringify when not a Redacted<string>
string
: T;
: T extends Rpc<infer Shape extends object>
? RpcWireShape<Shape> & 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<unknown>
: 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<any>
? // redacteds are always stored as secret_text, so are always string
// we JSON.stringify when not a Redacted<string>
string
: T;

/**
* Cloudflare service-binding wire shape for an Effect-native Worker.
Expand Down
38 changes: 36 additions & 2 deletions packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -75,7 +80,7 @@ type BindingSpec = InputProps<
Exclude<PutScriptRequest["metadata"]["bindings"], undefined>[number]
>;

const toBinding = (
export const toBinding = (
bindingName: string,
binding: WorkerBindingResource,
): BindingSpec => {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<ResourceBinding<Worker["Binding"]>>,
) => Array.from(new Set(bindings.flatMap((b) => b.data.crons ?? [])));
9 changes: 9 additions & 0 deletions packages/alchemy/src/Cloudflare/Workers/WorkerBinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -40,6 +41,12 @@ export type WorkerSettingsBinding = Exclude<
null | undefined
>[number];

export interface WorkerEntrypointBinding<Shape extends object = object> {
readonly BindingType: "Cloudflare.WorkerEntrypointBinding";
readonly service: string;
readonly entrypoint: string;
}

export type WorkerBindingResource =
// Config values
| Json
Expand All @@ -65,7 +72,9 @@ export type WorkerBindingResource =
| VectorizeIndex
| Secret
| Worker
| WorkerEntrypointBinding<any>
| DynamicWorkerLoader
| WorkflowResource
| VersionMetadata
| DurableObjectNamespaceLike<any>;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>;
}

type Assert<T extends true> = T;
type IsAssignable<A, B> = A extends B ? true : false;

const workflowActions: Cloudflare.WorkerEntrypointBinding<WorkflowActionsEntrypoint> =
{
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<Cloudflare.GetBindingType<typeof workflowActions>, Service>
>;
type _EntrypointMethodIsRpc = Assert<
IsAssignable<
Cloudflare.GetBindingType<typeof workflowActions>["run"],
(name: string) => Promise<string | Cloudflare.RpcErrorEnvelope>
>
>;
type _WorkflowIsRuntimeWorkflow = Assert<
IsAssignable<Cloudflare.GetBindingType<typeof dynamicWorkflow>, 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",
});
});
});