diff --git a/src/cloudflare/internal/workers.d.ts b/src/cloudflare/internal/workers.d.ts index 42749ac738b..48fb53ae348 100644 --- a/src/cloudflare/internal/workers.d.ts +++ b/src/cloudflare/internal/workers.d.ts @@ -12,6 +12,13 @@ export class WorkerEntrypoint { public env: unknown; } +export class Workflow { + public constructor(ctx: unknown, env: unknown); + + public ctx: unknown; + public env: unknown; +} + export class RpcStub { public constructor(server: object); } diff --git a/src/cloudflare/workers.ts b/src/cloudflare/workers.ts index 2fd18587a1f..4956c4a7840 100644 --- a/src/cloudflare/workers.ts +++ b/src/cloudflare/workers.ts @@ -11,3 +11,4 @@ export const WorkerEntrypoint = entrypoints.WorkerEntrypoint; export const DurableObject = entrypoints.DurableObject; export const RpcStub = entrypoints.RpcStub; export const RpcTarget = entrypoints.RpcTarget; +export const Workflow = entrypoints.Workflow; diff --git a/src/node/internal/workers.d.ts b/src/node/internal/workers.d.ts index a2afd6191a5..6c2541a07cb 100644 --- a/src/node/internal/workers.d.ts +++ b/src/node/internal/workers.d.ts @@ -1,5 +1,6 @@ declare namespace _default { class WorkerEntrypoint {} + class Workflow {} class DurableObject {} class RpcPromise {} class RpcProperty {} diff --git a/src/workerd/api/worker-rpc.c++ b/src/workerd/api/worker-rpc.c++ index 3f0d0ecb3a2..16f83736641 100644 --- a/src/workerd/api/worker-rpc.c++ +++ b/src/workerd/api/worker-rpc.c++ @@ -1831,4 +1831,18 @@ jsg::Ref DurableObjectBase::constructor( return jsg::alloc(); } +jsg::Ref Workflow::constructor( + const v8::FunctionCallbackInfo& args, + jsg::Ref ctx, jsg::JsObject env) { + // HACK: We take `FunctionCallbackInfo` mostly so that we can set properties directly on + // `This()`. There ought to be a better way to get access to `this` in a constructor. + // We *also* declare `ctx` and `env` params more explicitly just for the sake of type checking. + jsg::Lock& js = jsg::Lock::from(args.GetIsolate()); + + jsg::JsObject self(args.This()); + self.set(js, "ctx", jsg::JsValue(args[0])); + self.set(js, "env", jsg::JsValue(args[1])); + return jsg::alloc(); +} + }; // namespace workerd::api diff --git a/src/workerd/api/worker-rpc.h b/src/workerd/api/worker-rpc.h index cd323b0c62f..88c745be1e4 100644 --- a/src/workerd/api/worker-rpc.h +++ b/src/workerd/api/worker-rpc.h @@ -478,6 +478,27 @@ class DurableObjectBase: public jsg::Object { JSG_RESOURCE_TYPE(DurableObjectBase) {} }; +// Base class for Workflows +// +// When the worker's top-level module exports a class that extends this class, it means that it +// is a Workflow. +// +// import {Workflow} from "cloudflare:workers"; +// export class MyWorkflow extends Workflow { +// async run(batch, fns) { ... } +// } +// +// `env` and `ctx` are automatically available as `this.env` and `this.ctx`, without the need to +// define a constructor. +class Workflow: public jsg::Object { +public: + static jsg::Ref constructor( + const v8::FunctionCallbackInfo& args, + jsg::Ref ctx, jsg::JsObject env); + + JSG_RESOURCE_TYPE(Workflow) {} +}; + // The "cloudflare:workers" module, which exposes the WorkerEntrypoint and DurableObject types // for extending. class EntrypointsModule: public jsg::Object { @@ -487,6 +508,7 @@ class EntrypointsModule: public jsg::Object { JSG_RESOURCE_TYPE(EntrypointsModule) { JSG_NESTED_TYPE(WorkerEntrypoint); + JSG_NESTED_TYPE(Workflow); JSG_NESTED_TYPE_NAMED(DurableObjectBase, DurableObject); JSG_NESTED_TYPE_NAMED(JsRpcPromise, RpcPromise); JSG_NESTED_TYPE_NAMED(JsRpcProperty, RpcProperty); @@ -501,6 +523,7 @@ class EntrypointsModule: public jsg::Object { api::JsRpcStub, \ api::JsRpcTarget, \ api::WorkerEntrypoint, \ + api::Workflow, \ api::DurableObjectBase, \ api::EntrypointsModule diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 6ab66f56db9..5ec978febdf 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -1539,6 +1539,7 @@ Worker::Worker(kj::Own scriptParam, auto& api = script->isolate->getApi(); auto handlers = api.unwrapExports(lock, ns); + auto features = api.getFeatureFlags(); auto entrypointClasses = api.getEntrypointClasses(lock); for (auto& handler: handlers.fields) { @@ -1563,6 +1564,11 @@ Worker::Worker(kj::Own scriptParam, } else if (handle == entrypointClasses.workerEntrypoint) { impl->statelessClasses.insert(kj::mv(handler.name), kj::mv(cls)); return; + } else if (handle == entrypointClasses.workflow) { + if (features.getWorkerdExperimental()) { + impl->statelessClasses.insert(kj::mv(handler.name), kj::mv(cls)); + } + return; } handle = KJ_UNWRAP_OR(handle.getPrototype(js).tryCast(), { diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index b5ace13dda1..e0db7d4d717 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -458,6 +458,9 @@ class Worker::Api { // Class constructor for DurableObject (aka api::DurableObjectBase). jsg::JsObject durableObject; + + // Class constructor for Workflow. + jsg::JsObject workflow; }; // Get the constructors for classes from which entrypoint classes may inherit. diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index 7bb79ab82a7..c46ab8309f7 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -3,6 +3,7 @@ // https://opensource.org/licenses/Apache-2.0 #include "workerd-api.h" +#include "workerd/api/worker-rpc.h" #include #include @@ -240,6 +241,7 @@ WorkerdApi::EntrypointClasses WorkerdApi::getEntrypointClasses(jsg::Lock& lock) return { .workerEntrypoint = typedLock.getConstructor(lock.v8Context()), .durableObject = typedLock.getConstructor(lock.v8Context()), + .workflow = typedLock.getConstructor(lock.v8Context()), }; } const jsg::TypeHandler& diff --git a/types/defines/rpc.d.ts b/types/defines/rpc.d.ts index e30595768f7..57f61fcfe1a 100644 --- a/types/defines/rpc.d.ts +++ b/types/defines/rpc.d.ts @@ -10,6 +10,7 @@ declare namespace Rpc { export const __RPC_TARGET_BRAND: "__RPC_TARGET_BRAND"; export const __WORKER_ENTRYPOINT_BRAND: "__WORKER_ENTRYPOINT_BRAND"; export const __DURABLE_OBJECT_BRAND: "__DURABLE_OBJECT_BRAND"; + export const __WORKFLOW_BRAND: "__WORKFLOW_BRAND"; export interface RpcTargetBranded { [__RPC_TARGET_BRAND]: never; } @@ -19,9 +20,13 @@ declare namespace Rpc { export interface DurableObjectBranded { [__DURABLE_OBJECT_BRAND]: never; } + export interface WorkflowBranded { + [__WORKFLOW_BRAND]: never; + } export type EntrypointBranded = | WorkerEntrypointBranded - | DurableObjectBranded; + | DurableObjectBranded + | WorkflowBranded; // Types that can be used through `Stub`s export type Stubable = RpcTargetBranded | ((...args: any[]) => any); @@ -150,7 +155,8 @@ declare module "cloudflare:workers" { // `protected` fields don't appear in `keyof`s, so can't be accessed over RPC export abstract class WorkerEntrypoint - implements Rpc.WorkerEntrypointBranded { + implements Rpc.WorkerEntrypointBranded + { [Rpc.__WORKER_ENTRYPOINT_BRAND]: never; protected ctx: ExecutionContext; @@ -166,7 +172,8 @@ declare module "cloudflare:workers" { } export abstract class DurableObject - implements Rpc.DurableObjectBranded { + implements Rpc.DurableObjectBranded + { [Rpc.__DURABLE_OBJECT_BRAND]: never; protected ctx: DurableObjectState; @@ -187,4 +194,41 @@ declare module "cloudflare:workers" { ): void | Promise; webSocketError?(ws: WebSocket, error: unknown): void | Promise; } + + export type DurationLabel = + | "second" + | "minute" + | "hour" + | "day" + | "week" + | "month" + | "year"; + export type SleepDuration = `${number} ${DurationLabel}${"s" | ""}` | number; + + type WorkflowStep = { + do: ( + name: string, + callback: () => T + ) => T | Promise; + sleep: (name: string, duration: SleepDuration) => void | Promise; + }; + + export abstract class Workflow< + Env = unknown, + T extends Rpc.Serializable | unknown = unknown, + > implements Rpc.WorkflowBranded + { + [Rpc.__WORKFLOW_BRAND]: never; + + protected ctx: ExecutionContext; + protected env: Env; + + run( + events: Array<{ + payload: T; + timestamp: Date; + }>, + step: WorkflowStep + ): unknown | Promise; + } }