diff --git a/examples/sst-nextjs/.gitignore b/examples/sst-nextjs/.gitignore new file mode 100644 index 00000000..70137258 --- /dev/null +++ b/examples/sst-nextjs/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# sst +.sst + +# open-next +.open-next \ No newline at end of file diff --git a/examples/sst-nextjs/README.md b/examples/sst-nextjs/README.md new file mode 100644 index 00000000..c4033664 --- /dev/null +++ b/examples/sst-nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/sst-nextjs/next.config.js b/examples/sst-nextjs/next.config.js new file mode 100644 index 00000000..658404ac --- /dev/null +++ b/examples/sst-nextjs/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +module.exports = nextConfig; diff --git a/examples/sst-nextjs/package.json b/examples/sst-nextjs/package.json new file mode 100644 index 00000000..36d3b90d --- /dev/null +++ b/examples/sst-nextjs/package.json @@ -0,0 +1,32 @@ +{ + "name": "sst-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "sst bind next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@eventual/core": "workspace:^", + "@eventual/client": "workspace:^", + "@eventual/aws-client": "workspace:^", + "react": "^18", + "react-dom": "^18", + "next": "14.0.4" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "sst": "^2.39.2", + "aws-cdk-lib": "2.110.1", + "@eventual/aws-cdk": "workspace:^", + "constructs": "10.3.0" + } +} diff --git a/examples/sst-nextjs/postcss.config.js b/examples/sst-nextjs/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/examples/sst-nextjs/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/sst-nextjs/public/next.svg b/examples/sst-nextjs/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/examples/sst-nextjs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/sst-nextjs/public/vercel.svg b/examples/sst-nextjs/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/examples/sst-nextjs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/sst-nextjs/src/app/api/start/route.tsx b/examples/sst-nextjs/src/app/api/start/route.tsx new file mode 100644 index 00000000..c279aa06 --- /dev/null +++ b/examples/sst-nextjs/src/app/api/start/route.tsx @@ -0,0 +1,12 @@ +import { client } from "@/server/client"; +import { NextResponse } from "next/server"; + +export default async function handler() { + const executionHandle = await client.tickTock.startExecution(); + + return new NextResponse( + JSON.stringify({ + executionId: executionHandle.executionId, + }) + ); +} diff --git a/examples/sst-nextjs/src/app/favicon.ico b/examples/sst-nextjs/src/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/examples/sst-nextjs/src/app/favicon.ico differ diff --git a/examples/sst-nextjs/src/app/globals.css b/examples/sst-nextjs/src/app/globals.css new file mode 100644 index 00000000..fd81e885 --- /dev/null +++ b/examples/sst-nextjs/src/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/examples/sst-nextjs/src/app/layout.tsx b/examples/sst-nextjs/src/app/layout.tsx new file mode 100644 index 00000000..323bd9c9 --- /dev/null +++ b/examples/sst-nextjs/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/sst-nextjs/src/app/page.tsx b/examples/sst-nextjs/src/app/page.tsx new file mode 100644 index 00000000..992bc443 --- /dev/null +++ b/examples/sst-nextjs/src/app/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+

+ Get started by editing  + src/app/page.tsx +

+
+ + By{" "} + Vercel Logo + +
+
+ +
+ Next.js Logo +
+ +
+ +

+ Docs{" "} + + -> + +

+

+ Find in-depth information about Next.js features and API. +

+
+ + +

+ Learn{" "} + + -> + +

+

+ Learn about Next.js in an interactive course with quizzes! +

+
+ + +

+ Templates{" "} + + -> + +

+

+ Explore starter templates for Next.js. +

+
+ + +

+ Deploy{" "} + + -> + +

+

+ Instantly deploy your Next.js site to a shareable URL with Vercel. +

+
+
+
+ ); +} diff --git a/examples/sst-nextjs/src/server/client.ts b/examples/sst-nextjs/src/server/client.ts new file mode 100644 index 00000000..a5410b1d --- /dev/null +++ b/examples/sst-nextjs/src/server/client.ts @@ -0,0 +1,6 @@ +import type * as server from "."; +import { AWSServiceClient } from "@eventual/aws-client"; + +export const client = new AWSServiceClient({ + serviceUrl: process.env.SERVICE_URL!, +}); diff --git a/examples/sst-nextjs/src/server/event.ts b/examples/sst-nextjs/src/server/event.ts new file mode 100644 index 00000000..d4819e54 --- /dev/null +++ b/examples/sst-nextjs/src/server/event.ts @@ -0,0 +1,9 @@ +import { event } from "@eventual/core"; + +export const tick = event<{ + time: number; +}>("tick"); + +export const tock = event<{ + time: number; +}>("tick"); diff --git a/examples/sst-nextjs/src/server/index.ts b/examples/sst-nextjs/src/server/index.ts new file mode 100644 index 00000000..eb52b1a9 --- /dev/null +++ b/examples/sst-nextjs/src/server/index.ts @@ -0,0 +1,3 @@ +export * from "./event"; +export * from "./socket"; +export * from "./workflow"; diff --git a/examples/sst-nextjs/src/server/socket.ts b/examples/sst-nextjs/src/server/socket.ts new file mode 100644 index 00000000..6d8c936b --- /dev/null +++ b/examples/sst-nextjs/src/server/socket.ts @@ -0,0 +1,8 @@ +import { socket } from "@eventual/core"; + +// expose a websocket endpoint for +export const tickTockFeed = socket("tickTockFeed", { + $connect: () => {}, + $disconnect: () => {}, + $default: () => {}, +}); diff --git a/examples/sst-nextjs/src/server/workflow.ts b/examples/sst-nextjs/src/server/workflow.ts new file mode 100644 index 00000000..7938cca2 --- /dev/null +++ b/examples/sst-nextjs/src/server/workflow.ts @@ -0,0 +1,24 @@ +import { duration, workflow } from "@eventual/core"; +import { tick, tock } from "./event"; + +export const tickTock = workflow("tickTock", async (input?: string) => { + let i = 0; + while (true) { + // emit the tick event + await tick.emit({ + time: Date.now(), + }); + + // put the workflow to sleep for 1 minute + await duration(1, "minute"); + + // emit the tock event + await tock.emit({ + time: Date.now(), + }); + + if (i === 100) { + break; + } + } +}); diff --git a/examples/sst-nextjs/sst-env.d.ts b/examples/sst-nextjs/sst-env.d.ts new file mode 100644 index 00000000..3e233439 --- /dev/null +++ b/examples/sst-nextjs/sst-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/sst-nextjs/sst.config.ts b/examples/sst-nextjs/sst.config.ts new file mode 100644 index 00000000..732af1e4 --- /dev/null +++ b/examples/sst-nextjs/sst.config.ts @@ -0,0 +1,20 @@ +import { SSTConfig } from "sst"; +import { NextjsSite } from "sst/constructs"; + +export default { + config(_input) { + return { + name: "sst-nextjs", + region: "us-east-1", + }; + }, + stacks(app) { + app.stack(function Site({ stack }) { + const site = new NextjsSite(stack, "site"); + + stack.addOutputs({ + SiteUrl: site.url, + }); + }); + }, +} satisfies SSTConfig; diff --git a/examples/sst-nextjs/tailwind.config.ts b/examples/sst-nextjs/tailwind.config.ts new file mode 100644 index 00000000..e9a0944e --- /dev/null +++ b/examples/sst-nextjs/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/examples/sst-nextjs/tsconfig.json b/examples/sst-nextjs/tsconfig.json new file mode 100644 index 00000000..e59724b2 --- /dev/null +++ b/examples/sst-nextjs/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/@eventual/client/src/http-eventual-client.ts b/packages/@eventual/client/src/http-eventual-client.ts index e81be1e6..e8eec308 100644 --- a/packages/@eventual/client/src/http-eventual-client.ts +++ b/packages/@eventual/client/src/http-eventual-client.ts @@ -15,9 +15,10 @@ import { type SendTaskFailureRequest, type SendTaskHeartbeatResponse, type SendTaskSuccessRequest, - type StartExecutionRequest, + type DirectStartExecutionRequest, type Transaction, type Workflow, + WorkflowOutput, } from "@eventual/core"; import type { EventualService, @@ -49,8 +50,8 @@ export class HttpEventualClient implements EventualServiceClient { } public async startExecution( - request: StartExecutionRequest - ): Promise> { + request: DirectStartExecutionRequest + ): Promise>> { // serialize the workflow object to a string const workflow = typeof request.workflow === "string" diff --git a/packages/@eventual/client/src/service-client.ts b/packages/@eventual/client/src/service-client.ts index 5b0f89be..59f6e337 100644 --- a/packages/@eventual/client/src/service-client.ts +++ b/packages/@eventual/client/src/service-client.ts @@ -1,4 +1,14 @@ -import { Command } from "@eventual/core"; +import type { + Command, + Event, + Execution, + ExecutionHandle, + SendSignalProps, + Signal, + Socket, + StartExecutionRequest, + Workflow, +} from "@eventual/core"; import { HttpServiceClient, HttpServiceClientProps, @@ -104,14 +114,30 @@ type ServiceClientName = T extends { name: infer Name extends string } ? Name : never; -type ServiceClientMethod = T extends Command< - any, - infer Input, - infer Output, - any, - any, - any -> +type ServiceClientMethod = T extends Workflow + ? { + startExecution: [undefined] extends [Input] + ? ( + req?: StartExecutionRequest + ) => Promise> + : ( + req: StartExecutionRequest + ) => Promise>; + getHandle: (executionId: string) => Promise>; + getStatus(executionId: string): Promise>; + sendSignal( + executionId: string, + signal: string | Signal, + ...args: SendSignalProps + ): Promise; + } + : T extends Event + ? { + emit: (payload: Payload) => Promise; + } + : T extends Socket + ? never + : T extends Command ? [Input] extends [undefined] ? { ( @@ -134,7 +160,9 @@ type ServiceClientMethod = T extends Command< type KeysWhereNameIsSame = { [k in keyof Service]: k extends Extract["name"] ? // we only want commands to show up - Service[k] extends { kind: "Command" } + Service[k] extends { + kind: "Command" | "Workflow" | "Event"; + } ? k : never : never; diff --git a/packages/@eventual/core-runtime/src/clients/runtime-service-clients.ts b/packages/@eventual/core-runtime/src/clients/runtime-service-clients.ts index 630f56f2..a4641826 100644 --- a/packages/@eventual/core-runtime/src/clients/runtime-service-clients.ts +++ b/packages/@eventual/core-runtime/src/clients/runtime-service-clients.ts @@ -16,9 +16,10 @@ import { SendTaskHeartbeatRequest, SendTaskHeartbeatResponse, SendTaskSuccessRequest, - StartExecutionRequest, + DirectStartExecutionRequest, Transaction, Workflow, + WorkflowOutput, } from "@eventual/core"; import type { WorkflowProvider } from "../providers/workflow-provider.js"; import type { ExecutionHistoryStateStore } from "../stores/execution-history-state-store.js"; @@ -73,8 +74,8 @@ export class RuntimeFallbackServiceClient implements EventualServiceClient { } public async startExecution( - request: StartExecutionRequest - ): Promise> { + request: DirectStartExecutionRequest + ): Promise>> { if (!this.props.workflowClient) { return this.fallbackServiceClient.startExecution(request); } diff --git a/packages/@eventual/core-runtime/src/clients/workflow-client.ts b/packages/@eventual/core-runtime/src/clients/workflow-client.ts index 4a45fb89..db9e14dd 100644 --- a/packages/@eventual/core-runtime/src/clients/workflow-client.ts +++ b/packages/@eventual/core-runtime/src/clients/workflow-client.ts @@ -5,7 +5,7 @@ import { FailedExecution, FailExecutionRequest, InProgressExecution, - StartExecutionRequest, + DirectStartExecutionRequest, SucceededExecution, SucceedExecutionRequest, Workflow, @@ -52,8 +52,8 @@ export class WorkflowClient { timeout, ...request }: - | StartExecutionRequest - | StartChildExecutionRequest): Promise { + | DirectStartExecutionRequest + | DirectStartChildExecutionRequest): Promise { if ( typeof workflow === "string" && !this.workflowProvider.workflowExists(workflow) @@ -191,8 +191,8 @@ export class WorkflowClient { } } -export interface StartChildExecutionRequest - extends StartExecutionRequest, +export interface DirectStartChildExecutionRequest + extends DirectStartExecutionRequest, WorkflowExecutionOptions { parentExecutionId: ExecutionID; /** diff --git a/packages/@eventual/core-runtime/test/workflow-executor.test.ts b/packages/@eventual/core-runtime/test/workflow-executor.test.ts index 642569ae..448b22a0 100644 --- a/packages/@eventual/core-runtime/test/workflow-executor.test.ts +++ b/packages/@eventual/core-runtime/test/workflow-executor.test.ts @@ -66,17 +66,15 @@ const context: WorkflowContext = { const workflow = (() => { let n = 0; - return ( - handler: WorkflowHandler - ) => { - return _workflow(`wf${n++}`, handler); + return (handler: H) => { + return _workflow(`wf${n++}`, handler); }; })(); const myTask = task("my-task", async () => {}); const myTask0 = task("my-task-0", async () => {}); const myTask2 = task("my-task-2", async () => {}); -const handleErrorTask = task("handle-error", async () => {}); +const handleErrorTask = task("handle-error", async (_err?: any) => {}); const processItemTask = task("processItem", (_item?: string) => {}); const beforeTask = task("before", (_v: string) => {}); const insideTask = task("inside", (_v: string) => { @@ -2016,14 +2014,11 @@ describe("signals", () => { mySignalHappened++; } ); - const myOtherSignalHandler = onSignal( - "MyOtherSignal", - async function (payload) { - myOtherSignalHappened++; - await myTask(payload); - myOtherSignalCompleted++; - } - ); + const myOtherSignalHandler = onSignal("MyOtherSignal", async function () { + myOtherSignalHappened++; + await myTask(); + myOtherSignalCompleted++; + }); await time(testTime); @@ -3513,7 +3508,7 @@ describe("using then, catch, finally", () => { x++; return myTask0(); }), - cwf("workflow1", undefined).catch(() => { + cwf().catch(() => { x++; return myTask0(); }), diff --git a/packages/@eventual/core/src/execution.ts b/packages/@eventual/core/src/execution.ts index bcebcdad..b65849da 100644 --- a/packages/@eventual/core/src/execution.ts +++ b/packages/@eventual/core/src/execution.ts @@ -9,7 +9,6 @@ import { isOrchestratorWorker } from "./internal/service-type.js"; import { SignalTargetType } from "./internal/signal.js"; import { EventualServiceClient } from "./service-client.js"; import type { SendSignalProps, Signal } from "./signals.js"; -import type { Workflow, WorkflowOutput } from "./workflow.js"; export enum ExecutionStatus { IN_PROGRESS = "IN_PROGRESS", @@ -86,7 +85,7 @@ export function isSucceededExecution( * Note: This object should be usable within a workflow. It should only contain deterministic logic * {@link EventualCall}s or {@link EventualProperty}s via the {@link EventualHook}. */ -export class ExecutionHandle { +export class ExecutionHandle { constructor( public executionId: ExecutionID, private serviceClient?: EventualServiceClient @@ -95,7 +94,7 @@ export class ExecutionHandle { /** * @return the {@link Execution} with the status, result, error, and other data based on the current status. */ - public async getStatus(): Promise>> { + public async getStatus(): Promise> { const hook = tryGetEventualHook(); if (hook) { return hook.executeEventualCall( @@ -106,7 +105,7 @@ export class ExecutionHandle { } else if (this.serviceClient && !isOrchestratorWorker()) { return (await this.serviceClient.getExecution( this.executionId - )) as Execution>; + )) as Execution; } else { throw new Error( "No EventualHook or EventualServiceClient available to get execution status." diff --git a/packages/@eventual/core/src/function-input.ts b/packages/@eventual/core/src/function-input.ts new file mode 100644 index 00000000..2ecd2877 --- /dev/null +++ b/packages/@eventual/core/src/function-input.ts @@ -0,0 +1,10 @@ +export type FunctionInput any> = + Parameters extends [infer Input, ...any[]] + ? Input + : Parameters extends [] + ? undefined + : Parameters extends [any?, ...any[]] + ? Parameters[0] + : Parameters extends [undefined?, ...any[]] + ? undefined + : never; diff --git a/packages/@eventual/core/src/service-client.ts b/packages/@eventual/core/src/service-client.ts index dadee21b..9bcc1986 100644 --- a/packages/@eventual/core/src/service-client.ts +++ b/packages/@eventual/core/src/service-client.ts @@ -23,6 +23,7 @@ import type { Workflow, WorkflowExecutionOptions, WorkflowInput, + WorkflowOutput, } from "./workflow.js"; /** @@ -37,8 +38,8 @@ export interface EventualServiceClient { * @param input Workflow parameters */ startExecution( - request: StartExecutionRequest - ): Promise>; + request: DirectStartExecutionRequest + ): Promise>>; /** * Retrieves one or more workflow execution. @@ -117,7 +118,7 @@ export interface EventualServiceClient { export type EmitEventsRequest = CommandInput; -export interface StartExecutionRequest +export interface DirectStartExecutionRequest extends WorkflowExecutionOptions { /** * Name of the workflow execution. @@ -138,6 +139,13 @@ export interface StartExecutionRequest input: WorkflowInput; } +export interface StartExecutionRequest extends WorkflowExecutionOptions { + /** + * Input payload for the workflow function. + */ + input: Input; +} + export interface SucceedExecutionRequest { executionId: string; result?: Result; diff --git a/packages/@eventual/core/src/task.ts b/packages/@eventual/core/src/task.ts index fe767b1d..39e12be4 100644 --- a/packages/@eventual/core/src/task.ts +++ b/packages/@eventual/core/src/task.ts @@ -1,5 +1,6 @@ import { duration, time } from "./await-time.js"; import type { ExecutionID } from "./execution.js"; +import { FunctionInput } from "./function-input.js"; import type { FunctionBundleProps, FunctionRuntimeProps, @@ -184,6 +185,10 @@ export interface TaskHandler { | Promise>>; } +export type TaskHandlerOutput = UnwrapAsync< + Awaited> +>; + export type UnwrapAsync = Output extends AsyncResult ? O : Output; @@ -246,15 +251,17 @@ export async function asyncResult( * @param taskID a string that uniquely identifies the Task within a single workflow context. * @param handler the function that handles the task */ -export function task( +export function task( taskID: Name, - handler: TaskHandler -): Task; -export function task( + handler: Handler +): Task, TaskHandlerOutput>; + +export function task( taskID: Name, opts: TaskOptions, - handler: TaskHandler -): Task; + handler: Handler +): Task, TaskHandlerOutput>; + export function task( ...args: | [ diff --git a/packages/@eventual/core/src/workflow.ts b/packages/@eventual/core/src/workflow.ts index 798019d0..d4aa0b97 100644 --- a/packages/@eventual/core/src/workflow.ts +++ b/packages/@eventual/core/src/workflow.ts @@ -19,6 +19,7 @@ import { WorkflowSpec } from "./internal/service-spec.js"; import { SignalTargetType } from "./internal/signal.js"; import type { DurationSchedule, Schedule } from "./schedule.js"; import type { StartExecutionRequest } from "./service-client.js"; +import type { FunctionInput } from "./function-input.js"; export interface WorkflowHandler { (input: Input, context: WorkflowContext): Promise; @@ -43,6 +44,15 @@ export interface WorkflowExecutionOptions { * @default - workflow will never timeout. */ timeout?: Schedule; + /** + * Name of the workflow execution. + * + * Only one workflow can exist for an ID. Requests to start a workflow + * with the name of an existing workflow will fail. + * + * @default - a unique name is generated. + */ + executionName?: string; } /** @@ -88,6 +98,7 @@ export interface Workflow< in Input = any, Output = any > extends WorkflowSpec { + // input?: (i: Input) => any; options?: WorkflowDefinitionOptions; kind: "Workflow"; @@ -106,11 +117,8 @@ export interface Workflow< * Starts a workflow execution */ startExecution( - request: Omit< - StartExecutionRequest>, - "workflow" - > - ): Promise>>; + request: StartExecutionRequest + ): Promise>; } /** @@ -139,23 +147,17 @@ export interface Workflow< * @param name a globally unique ID for this workflow. * @param definition the workflow definition. */ -export function workflow< - Name extends string = string, - Input = any, - Output = any ->( +export function workflow( name: Name, - definition: WorkflowHandler -): Workflow; -export function workflow< - Name extends string = string, - Input = any, - Output = any ->( + definition: Handler +): Workflow, Awaited>>; + +export function workflow( name: Name, opts: WorkflowDefinitionOptions, - definition: WorkflowHandler -): Workflow; + definition: Handler +): Workflow, Awaited>>; + export function workflow< Name extends string = string, Input = any, @@ -217,16 +219,16 @@ export function workflow< Object.defineProperty(workflow, "name", { value: name, writable: false }); - workflow.startExecution = async function (input) { + workflow.startExecution = async function (req) { const serviceClient = getEventualHook().getEventualProperty( createEventualProperty(PropertyKind.ServiceClient, {}) ); return await serviceClient.startExecution>({ workflow: name, - executionName: input.executionName, - input: input.input, - timeout: input.timeout, + executionName: req.executionName, + input: req.input!, + timeout: req.timeout, ...opts, }); }; diff --git a/packages/@eventual/core/test/workflow.test.ts b/packages/@eventual/core/test/workflow.test.ts new file mode 100644 index 00000000..bab8b3f2 --- /dev/null +++ b/packages/@eventual/core/test/workflow.test.ts @@ -0,0 +1,52 @@ +import { FunctionInput } from "../src/function-input.js"; +type A = FunctionInput<(args: any, context: any) => Promise>; + +type B = FunctionInput<() => Promise>; + +type C = FunctionInput<(input?: undefined, context?: any) => Promise>; +type D = FunctionInput<(input?: string, context?: any) => Promise>; +type E = FunctionInput<(input: string, context: any) => Promise>; +type F = FunctionInput<(input: undefined, context: any) => Promise>; +type G = FunctionInput<(thr?: any) => Promise>; + +declare const a: A; +declare const b: B; +declare const c: C; +declare const d: D; +declare const e: E; +declare const f: F; +declare const g: G; + +type IsAny = 0 extends 1 & T ? true : false; + +// eslint-disable-next-line no-unused-expressions +() => { + isAny(a, true); + // @ts-expect-error + isAny(a, false); + + is(b); + + is(c); + + is(d); + // @ts-expect-error + is(d); + + is(e); + assertNever(is(e)); + + is(f); + + isAny(g, true); +}; + +declare function isAny(value: T, isAny: IsAny): any; + +declare function is( + value: T +): IsExact extends true ? void : never; + +type IsExact = T extends U ? (U extends T ? true : false) : false; + +declare function assertNever(_value: never); diff --git a/packages/@eventual/testing/src/environment.ts b/packages/@eventual/testing/src/environment.ts index 9f02518e..3705ea57 100644 --- a/packages/@eventual/testing/src/environment.ts +++ b/packages/@eventual/testing/src/environment.ts @@ -10,11 +10,12 @@ import { SendTaskHeartbeatRequest, SendTaskHeartbeatResponse, SendTaskSuccessRequest, - StartExecutionRequest, + DirectStartExecutionRequest, SubscriptionHandler, Task, TaskOutput, Workflow, + WorkflowOutput, } from "@eventual/core"; import { LocalContainer, @@ -270,8 +271,8 @@ export class TestEnvironment extends RuntimeServiceClient { * progresses time by one second ({@link tick}) */ public override async startExecution( - request: StartExecutionRequest - ): Promise> { + request: DirectStartExecutionRequest + ): Promise>> { const execution = await super.startExecution(request); // tick forward on explicit user action (triggering the workflow to start running) await this.tick(); diff --git a/packages/@eventual/testing/test/env.test.ts b/packages/@eventual/testing/test/env.test.ts index e98dde04..dc6b9864 100644 --- a/packages/@eventual/testing/test/env.test.ts +++ b/packages/@eventual/testing/test/env.test.ts @@ -59,21 +59,19 @@ const { const task = (() => { let n = 0; - return (handler: TaskHandler) => { + return (handler: H) => { // eslint-disable-next-line no-empty while (getEventualResource("Task", `task${++n}`)) {} - return _task(`task${n}`, handler); + return _task(`task${n}`, handler); }; })(); const workflow = (() => { let n = 0; - return ( - handler: WorkflowHandler - ) => { + return (handler: H) => { // eslint-disable-next-line no-empty while (getEventualResource("Workflow", `wf${++n}`)) {} - return _workflow(`wf${n}`, handler); + return _workflow(`wf${n}`, handler); }; })(); diff --git a/tsconfig.json b/tsconfig.json index 4c5fb288..d1872375 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ { "path": "packages/@eventual/testing" }, { "path": "packages/@eventual/testing/tsconfig.cjs.json" }, { "path": "packages/@eventual/testing/tsconfig.test.json" }, - { "path": "packages/create-eventual" } + { "path": "packages/create-eventual" }, + { "path": "examples/sst-nextjs/tsconfig.json" } ] }