1/**
2 * RPC and HTTP API definitions for workflows.
3 *
4 * Given one or more `Workflow` values, `toRpcGroup` creates the RPC definitions
5 * for clients and servers, while `toHttpApiGroup` creates HTTP POST endpoints
6 * that can be mounted in an API. Each workflow gets execute, discard, and
7 * resume operations, so callers can start a workflow or resume a suspended run
8 * by `executionId` without importing the workflow handler directly.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import type { NonEmptyReadonlyArray } from "../Array.ts"
14import * as HttpApiEndpoint from "../http-api/HttpApiEndpoint.ts"
15import * as HttpApiGroup from "../http-api/HttpApiGroup.ts"
16import * as Rpc from "../rpc/Rpc.ts"
17import * as RpcGroup from "../rpc/RpcGroup.ts"
18import * as Schema from "../Schema.ts"
19import type * as Workflow from "./Workflow.ts"
20
21/**
22 * Derives an `RpcGroup` from a list of workflows.
23 *
24 * **Example** (Deriving RPC endpoints from workflows)
25 *
26 * ```ts import.meta.vitest
27 * import { Layer, Schema } from "effect"
28 * import { RpcServer } from "effect/rpc"
29 * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/workflow"
30 *
31 * const EmailWorkflow = Workflow.make("EmailWorkflow", {
32 * payload: {
33 * id: Schema.String,
34 * to: Schema.String
35 * },
36 * idempotencyKey: ({ id }) => id
37 * })
38 *
39 * const myWorkflows = [EmailWorkflow] as const
40 *
41 * // Use WorkflowProxy.toRpcGroup to create a `RpcGroup` from the
42 * // workflows
43 * class MyRpcs extends WorkflowProxy.toRpcGroup(myWorkflows) {}
44 *
45 * // Use WorkflowProxyServer.layerRpcHandlers to create a layer that implements
46 * // the rpc handlers
47 * const ApiLayer = RpcServer.layer(MyRpcs).pipe(
48 * Layer.provide(WorkflowProxyServer.layerRpcHandlers(myWorkflows))
49 * )
50 * const result = [MyRpcs.requests.size, Layer.isLayer(ApiLayer)] // => [3, true]
51 * ```
52 *
53 * @stability unstable
54 * @category constructors
55 * @since 4.0.0
56 */
57export const toRpcGroup = <
58 const Workflows extends NonEmptyReadonlyArray<Workflow.Any>,
59 const Prefix extends string = ""
60>(
61 workflows: Workflows,
62 options?: {
63 readonly prefix?: Prefix | undefined
64 }
65): RpcGroup.RpcGroup<ConvertRpcs<Workflows[number], Prefix>> => {
66 const prefix = options?.prefix ?? ""
67 const rpcs: Array<Rpc.Any> = []
68 for (const workflow_ of workflows) {
69 const workflow = workflow_ as Workflow.AnyWithProps
70 rpcs.push(
71 Rpc.make(`${prefix}${workflow._tag}`, {
72 payload: workflow.payloadSchema,
73 error: workflow.errorSchema,
74 success: workflow.successSchema
75 }).annotateMerge(workflow.annotations),
76 Rpc.make(`${prefix}${workflow._tag}Discard`, {
77 payload: workflow.payloadSchema,
78 success: Schema.String
79 }).annotateMerge(workflow.annotations),
80 Rpc.make(`${prefix}${workflow._tag}Resume`, { payload: ResumePayload })
81 .annotateMerge(workflow.annotations)
82 )
83 }
84 return RpcGroup.make(...rpcs) as any
85}
86
87/**
88 * Maps each workflow to the RPC definitions generated for execute, discard,
89 * and resume operations.
90 *
91 * @stability unstable
92 * @category converting
93 * @since 4.0.0
94 */
95export type ConvertRpcs<Workflows extends Workflow.Any, Prefix extends string> = Workflows extends Workflow.Workflow<
96 infer _Name,
97 infer _Payload,
98 infer _Success,
99 infer _Error
100> ?
101 | Rpc.Rpc<`${Prefix}${_Name}`, _Payload, _Success, _Error>
102 | Rpc.Rpc<`${Prefix}${_Name}Discard`, _Payload, typeof Schema.String>
103 | Rpc.Rpc<`${Prefix}${_Name}Resume`, typeof ResumePayload>
104 : never
105
106/**
107 * Derives an `HttpApiGroup` from a list of workflows.
108 *
109 * **Example** (Deriving HTTP API endpoints from workflows)
110 *
111 * ```ts import.meta.vitest
112 * import { Layer, Schema } from "effect"
113 * import { HttpApi, HttpApiBuilder } from "effect/http-api"
114 * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/workflow"
115 *
116 * const EmailWorkflow = Workflow.make("EmailWorkflow", {
117 * payload: {
118 * id: Schema.String,
119 * to: Schema.String
120 * },
121 * idempotencyKey: ({ id }) => id
122 * })
123 *
124 * const myWorkflows = [EmailWorkflow] as const
125 *
126 * // Use WorkflowProxy.toHttpApiGroup to create a `HttpApiGroup` from the
127 * // workflows
128 * class MyApi extends HttpApi.make("api")
129 * .add(WorkflowProxy.toHttpApiGroup("workflows", myWorkflows))
130 * {}
131 *
132 * // Use WorkflowProxyServer.layerHttpApi to create a layer that implements the
133 * // workflows HttpApiGroup
134 * const ApiLayer = HttpApiBuilder.layer(MyApi).pipe(
135 * Layer.provide(
136 * WorkflowProxyServer.layerHttpApi(MyApi, "workflows", myWorkflows)
137 * )
138 * )
139 * const result = [Object.keys(MyApi.groups.workflows.endpoints).length, Layer.isLayer(ApiLayer)] // => [3, true]
140 * ```
141 *
142 * @stability unstable
143 * @category constructors
144 * @since 4.0.0
145 */
146export const toHttpApiGroup = <const Name extends string, const Workflows extends NonEmptyReadonlyArray<Workflow.Any>>(
147 name: Name,
148 workflows: Workflows
149): HttpApiGroup.HttpApiGroup<Name, ConvertHttpApi<Workflows[number]>> => {
150 let group = HttpApiGroup.make(name)
151 for (const workflow_ of workflows) {
152 const workflow = workflow_ as Workflow.AnyWithProps
153 const path = `/${tagToPath(workflow._tag)}` as const
154 group = group.add(
155 HttpApiEndpoint.post(workflow._tag, path, {
156 payload: workflow.payloadSchema,
157 success: workflow.successSchema,
158 error: workflow.errorSchema
159 }).annotateMerge(workflow.annotations),
160 HttpApiEndpoint.post(workflow._tag + "Discard", `${path}/discard`, {
161 payload: workflow.payloadSchema,
162 success: Schema.String
163 }).annotateMerge(workflow.annotations),
164 HttpApiEndpoint.post(workflow._tag + "Resume", `${path}/resume`, {
165 payload: ResumePayload
166 }).annotateMerge(workflow.annotations)
167 ) as any
168 }
169 return group as any
170}
171
172const tagToPath = (tag: string): string =>
173 tag
174 // .replace(/[^a-zA-Z0-9]+/g, "-") // Replace non-alphanumeric characters with hyphen
175 // .replace(/([a-z])([A-Z])/g, "$1-$2") // Insert hyphen before uppercase letters
176 .toLowerCase()
177
178/**
179 * Maps each workflow to the HTTP API endpoints generated for execute,
180 * discard, and resume operations.
181 *
182 * @stability unstable
183 * @category converting
184 * @since 4.0.0
185 */
186export type ConvertHttpApi<Workflows extends Workflow.Any> = Workflows extends Workflow.Workflow<
187 infer _Name,
188 infer _Payload,
189 infer _Success,
190 infer _Error
191> ?
192 | HttpApiEndpoint.HttpApiEndpoint<
193 _Name,
194 "POST",
195 `/${Lowercase<_Name>}`,
196 never,
197 never,
198 _Payload,
199 never,
200 _Success,
201 _Error
202 >
203 | HttpApiEndpoint.HttpApiEndpoint<
204 `${_Name}Discard`,
205 "POST",
206 `/${Lowercase<_Name>}/discard`,
207 never,
208 never,
209 _Payload,
210 never,
211 typeof Schema.String
212 >
213 | HttpApiEndpoint.HttpApiEndpoint<
214 `${_Name}Resume`,
215 "POST",
216 `/${Lowercase<_Name>}/resume`,
217 never,
218 never,
219 typeof ResumePayload
220 > :
221 never
222
223const ResumePayload = Schema.Struct({ executionId: Schema.String })