Skip to content

Commit

Permalink
Workflows binding (#7012)
Browse files Browse the repository at this point in the history
* start from workflows branch, squashed

* add Workflow support to `wrangler types`

* update fixture

* add (skipped) e2e test

* mark validation as TODO + remove commented code

Co-authored-by: Luís Duarte <[email protected]>
  • Loading branch information
RamIdeas and LuisDuarte1 authored Oct 18, 2024
1 parent 1f6ff8b commit 244aa57
Show file tree
Hide file tree
Showing 35 changed files with 457 additions and 7 deletions.
22 changes: 22 additions & 0 deletions .changeset/green-parrots-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"wrangler": minor
---

Add support for Workflow bindings (in deployments, not yet in local dev)

To bind to a workflow, add a `workflows` section in your wrangler.toml:

```toml
[[workflows]]
binding = "WORKFLOW"
name = "my-workflow"
class_name = "MyDemoWorkflow"
```

and export an entrypoint (e.g. `MyDemoWorkflow`) in your script:

```typescript
import { WorkflowEntrypoint } from "cloudflare:workers";

export class MyDemoWorkflow extends WorkflowEntrypoint<Env, Params> {...}
```
15 changes: 15 additions & 0 deletions fixtures/workflow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "my-workflow",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"start": "wrangler dev --x-dev-env"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241011.0",
"wrangler": "workspace:*"
},
"volta": {
"extends": "../../package.json"
}
}
50 changes: 50 additions & 0 deletions fixtures/workflow/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
WorkerEntrypoint,
Workflow,
WorkflowEvent,
WorkflowStep,
} from "cloudflare:workers";

type Params = {
name: string;
};
export class Demo extends Workflow<{}, Params> {
async run(events: Array<WorkflowEvent<Params>>, step: WorkflowStep) {
const { timestamp, payload } = events[0];
const result = await step.do("First step", async function () {
return {
output: "First step result",
};
});

await step.sleep("Wait", "1 minute");

const result2 = await step.do("Second step", async function () {
return {
output: "Second step result",
};
});

return {
result,
result2,
timestamp,
payload,
};
}
}

type Env = {
WORKFLOW: {
create: (id: string) => {
pause: () => {};
};
};
};
export default class extends WorkerEntrypoint<Env> {
async fetch() {
const handle = await this.env.WORKFLOW.create(crypto.randomUUID());
await handle.pause();
return new Response();
}
}
13 changes: 13 additions & 0 deletions fixtures/workflow/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["es2021"],
"module": "es2022",
"types": ["@cloudflare/workers-types/experimental"],
"noEmit": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
5 changes: 5 additions & 0 deletions fixtures/workflow/worker-configuration.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Generated by Wrangler by running `wrangler types`

interface Env {
WORKFLOW: Workflow;
}
9 changes: 9 additions & 0 deletions fixtures/workflow/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#:schema node_modules/wrangler/config-schema.json
name = "my-workflow-demo"
main = "src/index.ts"
compatibility_date = "2024-10-11"

[[workflows]]
binding = "WORKFLOW"
name = "my-workflow"
class_name = "Demo"
41 changes: 41 additions & 0 deletions packages/wrangler/e2e/dev-with-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,47 @@ describe.sequential.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => {
await worker.readUntil(/✉️/);
});

// TODO: enable for remove dev once realish preview supports it
// TODO: enable for local dev once implemented
it.skip("exposes Workflow bindings", async () => {
await helper.seed({
"wrangler.toml": dedent`
name = "my-workflow-demo"
main = "src/index.ts"
compatibility_date = "2024-10-11"
[[workflows]]
binding = "WORKFLOW"
name = "my-workflow"
class_name = "Demo"
`,
"src/index.ts": dedent`
import { WorkflowEntrypoint } from "cloudflare:workers";
export default {
async fetch(request, env, ctx) {
if (env.WORKFLOW === undefined) {
return new Response("env.WORKFLOW is undefined");
}
return new Response("env.WORKFLOW is available");
}
}
export class Demo extends WorkflowEntrypoint {
run() {
// blank
}
}
`,
});
const worker = helper.runLongLived(`wrangler dev ${flags}`);
const { url } = await worker.waitForReady();
const res = await fetch(url);

await expect(res.text()).resolves.toBe("env.WORKFLOW is available");
});

// TODO(soon): implement E2E tests for other bindings
it.todo("exposes hyperdrive bindings");
it.skipIf(isLocal).todo("exposes send email bindings");
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe("normalizeAndValidateConfig()", () => {
placement: undefined,
tail_consumers: undefined,
pipelines: [],
workflows: [],
});
expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.hasWarnings()).toBe(false);
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/__tests__/navigator-user-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe("defineNavigatorUserAgent is respected", () => {
serveLegacyAssetsFromWorker: false,
mockAnalyticsEngineDatasets: [],
doBindings: [],
workflowBindings: [],
define: {},
alias: {},
checkFetch: false,
Expand Down Expand Up @@ -175,6 +176,7 @@ describe("defineNavigatorUserAgent is respected", () => {
moduleCollector: noopModuleCollector,
serveLegacyAssetsFromWorker: false,
doBindings: [],
workflowBindings: [],
define: {},
alias: {},
mockAnalyticsEngineDatasets: [],
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const bindingsConfigMock: Omit<
},
],
},
workflows: [],
r2_buckets: [
{
binding: "R2_BUCKET_BINDING",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function createWorkerBundleFormData(
ai: config?.ai,
version_metadata: config?.version_metadata,
durable_objects: config?.durable_objects,
workflows: config?.workflows,
queues: config?.queues.producers?.map((producer) => {
return { binding: producer.binding, queue_name: producer.queue };
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/startDevWorker/BundlerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class BundlerController extends Controller<BundlerControllerEventMap> {
serveLegacyAssetsFromWorker: Boolean(
config.legacy?.legacyAssets && !config.dev?.remote
),
workflowBindings: bindings?.workflows ?? [],
doBindings: bindings?.durable_objects?.bindings ?? [],
jsxFactory: config.build.jsxFactory,
jsxFragment: config.build.jsxFactory,
Expand Down Expand Up @@ -238,6 +239,7 @@ export class BundlerController extends Controller<BundlerControllerEventMap> {
noBundle: !config.build?.bundle,
findAdditionalModules: config.build?.findAdditionalModules,
durableObjects: bindings?.durable_objects ?? { bindings: [] },
workflows: bindings?.workflows ?? [],
mockAnalyticsEngineDatasets: bindings.analytics_engine_datasets ?? [],
local: !config.dev?.remote,
// startDevWorker only applies to "dev"
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/startDevWorker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
CfService,
CfUnsafe,
CfVectorize,
CfWorkflow,
} from "../../deployment-bundle/worker";
import type { WorkerRegistry } from "../../dev-registry";
import type { CfAccount } from "../../dev/create-worker-preview";
Expand Down Expand Up @@ -254,6 +255,7 @@ export type Binding =
| { type: "version_metadata" }
| { type: "data_blob"; source: BinaryFile }
| ({ type: "durable_object_namespace" } & BindingOmit<CfDurableObject>)
| ({ type: "workflow" } & BindingOmit<CfWorkflow>)
| ({ type: "queue" } & BindingOmit<CfQueue>)
| ({ type: "r2_bucket" } & BindingOmit<CfR2Bucket>)
| ({ type: "d1" } & Omit<CfD1Database, "binding">)
Expand Down
7 changes: 7 additions & 0 deletions packages/wrangler/src/api/startDevWorker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ export function convertCfWorkerInitBindingstoBindings(
}
break;
}
case "workflows": {
for (const { binding, ...x } of info) {
output[binding] = { type: "workflow", ...x };
}
break;
}
case "queues": {
for (const { binding, ...x } of info) {
output[binding] = { type: "queue", ...x };
Expand Down Expand Up @@ -278,6 +284,7 @@ export async function convertBindingsToCfWorkerInitBindings(
durable_objects: undefined,
queues: undefined,
r2_buckets: undefined,
workflows: undefined,
d1_databases: undefined,
vectorize: undefined,
hyperdrive: undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ export const defaultWranglerConfig: Config = {
d1_databases: [],
vectorize: [],
hyperdrive: [],
workflows: [],
services: [],
analytics_engine_datasets: [],
ai: undefined,
Expand Down
22 changes: 22 additions & 0 deletions packages/wrangler/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ export type DurableObjectBindings = {
environment?: string;
}[];

export type WorkflowBinding = {
/** The name of the binding used to refer to the Workflow */
binding: string;
/** The name of the Workflow */
name: string;
/** The exported class name of the Workflow */
class_name: string;
/** The script where the Workflow is defined (if it's external to this Worker) */
script_name?: string;
};

/**
* The `EnvironmentNonInheritable` interface declares all the configuration fields for an environment
* that cannot be inherited from the top-level environment, and must be defined specifically.
Expand Down Expand Up @@ -417,6 +428,17 @@ export interface EnvironmentNonInheritable {
bindings: DurableObjectBindings;
};

/**
* A list of workflows that your Worker should be bound to.
*
* NOTE: This field is not automatically inherited from the top level environment,
* and so must be specified in every named environment.
*
* @default `[]`
* @nonInheritable
*/
workflows: WorkflowBinding[];

/**
* Cloudchamber configuration
*
Expand Down
18 changes: 18 additions & 0 deletions packages/wrangler/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export function printBindings(
const {
data_blobs,
durable_objects,
workflows,
kv_namespaces,
send_email,
queues,
Expand Down Expand Up @@ -278,6 +279,23 @@ export function printBindings(
});
}

if (workflows !== undefined && workflows.length > 0) {
output.push({
type: "Workflows",
entries: workflows.map(({ class_name, script_name, binding }) => {
let value = class_name;
if (script_name) {
value += ` (defined in ${script_name})`;
}

return {
key: binding,
value,
};
}),
});
}

if (kv_namespaces !== undefined && kv_namespaces.length > 0) {
output.push({
type: "KV Namespaces",
Expand Down
19 changes: 19 additions & 0 deletions packages/wrangler/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,16 @@ function normalizeAndValidateEnvironment(
bindings: [],
}
),
workflows: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"workflows",
validateBindingArray(envName, validateWorkflowBinding),
[]
),
migrations: inheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -2008,6 +2018,15 @@ const validateDurableObjectBinding: ValidatorFn = (
return isValid;
};

/**
* Check that the given field is a valid "workflow" binding object.
*/
const validateWorkflowBinding: ValidatorFn = (_diagnostics, _field, _value) => {
// TODO

return true;
};

const validateCflogfwdrObject: (env: string) => ValidatorFn =
(envName) => (diagnostics, field, value, topLevelEnv) => {
//validate the bindings property first, as this also validates that it's an object, etc.
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
serveLegacyAssetsFromWorker:
!props.isWorkersSite && Boolean(props.legacyAssetPaths),
doBindings: config.durable_objects.bindings,
workflowBindings: config.workflows ?? [],
jsxFactory,
jsxFragment,
tsconfig: props.tsconfig ?? config.tsconfig,
Expand Down Expand Up @@ -674,6 +675,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
},
data_blobs: config.data_blobs,
durable_objects: config.durable_objects,
workflows: config.workflows,
queues: config.queues.producers?.map((producer) => {
return { binding: producer.binding, queue_name: producer.queue };
}),
Expand Down
Loading

0 comments on commit 244aa57

Please sign in to comment.