@effect/workflow 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/WorkflowProxy/package.json +6 -0
  2. package/WorkflowProxyServer/package.json +6 -0
  3. package/dist/cjs/Activity.js +17 -8
  4. package/dist/cjs/Activity.js.map +1 -1
  5. package/dist/cjs/DurableDeferred.js +40 -2
  6. package/dist/cjs/DurableDeferred.js.map +1 -1
  7. package/dist/cjs/Workflow.js +44 -21
  8. package/dist/cjs/Workflow.js.map +1 -1
  9. package/dist/cjs/WorkflowEngine.js +14 -1
  10. package/dist/cjs/WorkflowEngine.js.map +1 -1
  11. package/dist/cjs/WorkflowProxy.js +111 -0
  12. package/dist/cjs/WorkflowProxy.js.map +1 -0
  13. package/dist/cjs/WorkflowProxyServer.js +61 -0
  14. package/dist/cjs/WorkflowProxyServer.js.map +1 -0
  15. package/dist/cjs/index.js +5 -1
  16. package/dist/dts/Activity.d.ts +13 -6
  17. package/dist/dts/Activity.d.ts.map +1 -1
  18. package/dist/dts/DurableDeferred.d.ts +28 -1
  19. package/dist/dts/DurableDeferred.d.ts.map +1 -1
  20. package/dist/dts/Workflow.d.ts +28 -1
  21. package/dist/dts/Workflow.d.ts.map +1 -1
  22. package/dist/dts/WorkflowEngine.d.ts +6 -5
  23. package/dist/dts/WorkflowEngine.d.ts.map +1 -1
  24. package/dist/dts/WorkflowProxy.d.ts +90 -0
  25. package/dist/dts/WorkflowProxy.d.ts.map +1 -0
  26. package/dist/dts/WorkflowProxyServer.d.ts +27 -0
  27. package/dist/dts/WorkflowProxyServer.d.ts.map +1 -0
  28. package/dist/dts/index.d.ts +8 -0
  29. package/dist/dts/index.d.ts.map +1 -1
  30. package/dist/esm/Activity.js +15 -6
  31. package/dist/esm/Activity.js.map +1 -1
  32. package/dist/esm/DurableDeferred.js +38 -1
  33. package/dist/esm/DurableDeferred.js.map +1 -1
  34. package/dist/esm/Workflow.js +41 -19
  35. package/dist/esm/Workflow.js.map +1 -1
  36. package/dist/esm/WorkflowEngine.js +14 -1
  37. package/dist/esm/WorkflowEngine.js.map +1 -1
  38. package/dist/esm/WorkflowProxy.js +101 -0
  39. package/dist/esm/WorkflowProxy.js.map +1 -0
  40. package/dist/esm/WorkflowProxyServer.js +52 -0
  41. package/dist/esm/WorkflowProxyServer.js.map +1 -0
  42. package/dist/esm/index.js +8 -0
  43. package/dist/esm/index.js.map +1 -1
  44. package/package.json +20 -2
  45. package/src/Activity.ts +47 -13
  46. package/src/DurableDeferred.ts +103 -2
  47. package/src/Workflow.ts +111 -22
  48. package/src/WorkflowEngine.ts +19 -10
  49. package/src/WorkflowProxy.ts +178 -0
  50. package/src/WorkflowProxyServer.ts +103 -0
  51. package/src/index.ts +10 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import type { NonEmptyReadonlyArray } from "effect/Array"
4
5
  import type * as Brand from "effect/Brand"
5
6
  import type * as Cause from "effect/Cause"
6
7
  import * as Context from "effect/Context"
@@ -10,7 +11,7 @@ import * as Exit from "effect/Exit"
10
11
  import { dual } from "effect/Function"
11
12
  import * as Option from "effect/Option"
12
13
  import * as Schema from "effect/Schema"
13
- import type * as Workflow from "./Workflow.js"
14
+ import * as Workflow from "./Workflow.js"
14
15
  import type { WorkflowEngine, WorkflowInstance } from "./WorkflowEngine.js"
15
16
 
16
17
  /**
@@ -94,7 +95,7 @@ const await_: <Success extends Schema.Schema.Any, Error extends Schema.Schema.Al
94
95
  >(self: DurableDeferred<Success, Error>) {
95
96
  const engine = yield* EngineTag
96
97
  const instance = yield* InstanceTag
97
- const oexit = yield* engine.deferredResult(self)
98
+ const oexit = yield* Workflow.wrapActivityResult(engine.deferredResult(self), Option.isNone)
98
99
  if (Option.isNone(oexit)) {
99
100
  instance.suspended = true
100
101
  return yield* Effect.interrupt
@@ -112,6 +113,106 @@ export {
112
113
  await_ as await
113
114
  }
114
115
 
116
+ /**
117
+ * @since 1.0.0
118
+ * @category Combinators
119
+ */
120
+ export const into: {
121
+ /**
122
+ * @since 1.0.0
123
+ * @category Combinators
124
+ */
125
+ <Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(self: DurableDeferred<Success, Error>): <R>(effect: Effect.Effect<Success["Type"], Error["Type"], R>) => Effect.Effect<
126
+ Success["Type"],
127
+ Error["Type"],
128
+ R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"]
129
+ >
130
+ /**
131
+ * @since 1.0.0
132
+ * @category Combinators
133
+ */
134
+ <Success extends Schema.Schema.Any, Error extends Schema.Schema.All, R>(
135
+ effect: Effect.Effect<Success["Type"], Error["Type"], R>,
136
+ self: DurableDeferred<Success, Error>
137
+ ): Effect.Effect<
138
+ Success["Type"],
139
+ Error["Type"],
140
+ R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"]
141
+ >
142
+ } = dual(2, <Success extends Schema.Schema.Any, Error extends Schema.Schema.All, R>(
143
+ effect: Effect.Effect<Success["Type"], Error["Type"], R>,
144
+ self: DurableDeferred<Success, Error>
145
+ ): Effect.Effect<
146
+ Success["Type"],
147
+ Error["Type"],
148
+ R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"]
149
+ > =>
150
+ Effect.contextWithEffect((context: Context.Context<WorkflowEngine | WorkflowInstance>) => {
151
+ const engine = Context.get(context, EngineTag)
152
+ const instance = Context.get(context, InstanceTag)
153
+ return Effect.onExit(
154
+ effect,
155
+ Effect.fnUntraced(function*(exit) {
156
+ if (instance.suspended) return
157
+ const encodedExit = yield* Effect.orDie(Schema.encode(self.exitSchema)(exit))
158
+ yield* engine.deferredDone({
159
+ workflowName: instance.workflow.name,
160
+ executionId: instance.executionId,
161
+ deferred: self,
162
+ exit: encodedExit as any
163
+ })
164
+ })
165
+ )
166
+ }))
167
+
168
+ /**
169
+ * @since 1.0.0
170
+ * @category Racing
171
+ */
172
+ export const raceAll = <
173
+ const Effects extends NonEmptyReadonlyArray<Effect.Effect<any, any, any>>,
174
+ SI,
175
+ SR,
176
+ EI,
177
+ ER
178
+ >(options: {
179
+ name: string
180
+ success: Schema.Schema<
181
+ Effects[number] extends Effect.Effect<infer S, infer _E, infer _R> ? S : never,
182
+ SI,
183
+ SR
184
+ >
185
+ error: Schema.Schema<
186
+ Effects[number] extends Effect.Effect<infer _S, infer E, infer _R> ? E : never,
187
+ EI,
188
+ ER
189
+ >
190
+ effects: Effects
191
+ }): Effect.Effect<
192
+ (Effects[number] extends Effect.Effect<infer _A, infer _E, infer _R> ? _A : never),
193
+ (Effects[number] extends Effect.Effect<infer _A, infer _E, infer _R> ? _E : never),
194
+ | (Effects[number] extends Effect.Effect<infer _A, infer _R, infer R> ? R : never)
195
+ | SR
196
+ | ER
197
+ | WorkflowEngine
198
+ | WorkflowInstance
199
+ > => {
200
+ const deferred = make<any, any>(`raceAll/${options.name}`, {
201
+ success: options.success,
202
+ error: options.error
203
+ })
204
+ return Effect.gen(function*() {
205
+ const engine = yield* EngineTag
206
+ const oexit = yield* Workflow.wrapActivityResult(engine.deferredResult(deferred), Option.isNone)
207
+ if (Option.isSome(oexit)) {
208
+ return yield* (Effect.flatten(Effect.orDie(
209
+ Schema.decodeUnknown(deferred.exitSchema)(oexit.value)
210
+ )) as Effect.Effect<any, any, any>)
211
+ }
212
+ return yield* into(Effect.raceAll(options.effects), deferred)
213
+ })
214
+ }
215
+
115
216
  /**
116
217
  * @since 1.0.0
117
218
  * @category Token
package/src/Workflow.ts CHANGED
@@ -45,6 +45,27 @@ export interface Workflow<
45
45
  readonly payloadSchema: Payload
46
46
  readonly successSchema: Success
47
47
  readonly errorSchema: Error
48
+ readonly annotations: Context.Context<never>
49
+
50
+ /**
51
+ * Add an annotation to the workflow.
52
+ */
53
+ annotate<I, S>(tag: Context.Tag<I, S>, value: S): Workflow<
54
+ Name,
55
+ Payload,
56
+ Success,
57
+ Error
58
+ >
59
+
60
+ /**
61
+ * Add the annotations from a Context object to the workflow.
62
+ */
63
+ annotateContext<I>(context: Context.Context<I>): Workflow<
64
+ Name,
65
+ Payload,
66
+ Success,
67
+ Error
68
+ >
48
69
 
49
70
  /**
50
71
  * Execute the workflow with the given payload.
@@ -166,9 +187,34 @@ export interface Any {
166
187
  readonly payloadSchema: AnyStructSchema
167
188
  readonly successSchema: Schema.Schema.Any
168
189
  readonly errorSchema: Schema.Schema.All
190
+ readonly annotations: Context.Context<never>
169
191
  readonly executionId: (payload: any) => Effect.Effect<string>
170
192
  }
171
193
 
194
+ /**
195
+ * @since 1.0.0
196
+ * @category Models
197
+ */
198
+ export type Registrations<Workflows extends Any> = Workflows extends Workflow<
199
+ infer _Name,
200
+ infer _Payload,
201
+ infer _Success,
202
+ infer _Error
203
+ > ? Registration<_Name> :
204
+ never
205
+
206
+ /**
207
+ * @since 1.0.0
208
+ * @category Models
209
+ */
210
+ export type Requirements<Workflows extends Any> = Workflows extends Workflow<
211
+ infer _Name,
212
+ infer _Payload,
213
+ infer _Success,
214
+ infer _Error
215
+ > ? _Payload["Context"] | _Success["Context"] | _Error["Context"] :
216
+ never
217
+
172
218
  const EngineTag = Context.GenericTag<WorkflowEngine, WorkflowEngine["Type"]>(
173
219
  "@effect/workflow/WorkflowEngine" satisfies typeof WorkflowEngine.key
174
220
  )
@@ -196,6 +242,7 @@ export const make = <
196
242
  readonly success?: Success
197
243
  readonly error?: Error
198
244
  readonly suspendedRetrySchedule?: Schedule.Schedule<any, unknown> | undefined
245
+ readonly annotations?: Context.Context<never>
199
246
  }
200
247
  ): Workflow<Name, Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload, Success, Error> => {
201
248
  const suspendedRetrySchedule = options.suspendedRetrySchedule ?? defaultRetrySchedule
@@ -206,6 +253,19 @@ export const make = <
206
253
  payloadSchema: Schema.isSchema(options.payload) ? options.payload : Schema.Struct(options.payload as any),
207
254
  successSchema: options.success ?? Schema.Void as any,
208
255
  errorSchema: options.error ?? Schema.Never as any,
256
+ annotations: options.annotations ?? Context.empty(),
257
+ annotate(tag, value) {
258
+ return make({
259
+ ...options,
260
+ annotations: Context.add(self.annotations, tag, value)
261
+ })
262
+ },
263
+ annotateContext(context) {
264
+ return make({
265
+ ...options,
266
+ annotations: Context.merge(self.annotations, context)
267
+ })
268
+ },
209
269
  execute: Effect.fnUntraced(
210
270
  function*(fields: any, opts) {
211
271
  const payload = self.payloadSchema.make(fields)
@@ -231,7 +291,6 @@ export const make = <
231
291
  if (result._tag === "Complete") {
232
292
  return yield* result.exit as Exit.Exit<Success["Type"], Error["Type"]>
233
293
  }
234
- // @effect-diagnostics effect/floatingEffect:off
235
294
  sleep ??= (yield* Schedule.driver(suspendedRetrySchedule)).next(void 0).pipe(
236
295
  Effect.catchAll(() => Effect.dieMessage(`${options.name}.execute: suspendedRetrySchedule exhausted`))
237
296
  )
@@ -420,18 +479,47 @@ export const Result = <Success extends Schema.Schema.Any, Error extends Schema.S
420
479
  */
421
480
  export const intoResult = <A, E, R>(
422
481
  effect: Effect.Effect<A, E, R>
423
- ): Effect.Effect<Result<A, E>, never, R | WorkflowInstance> =>
424
- Effect.uninterruptibleMask((restore) =>
425
- Effect.withFiberRuntime((fiber) =>
426
- Effect.matchCause(restore(effect), {
427
- onSuccess: (value) => new Complete({ exit: Exit.succeed(value) }),
428
- onFailure(cause) {
429
- const instance = Context.unsafeGet(fiber.currentContext, InstanceTag)
430
- return instance.suspended ? new Suspended() : new Complete({ exit: Exit.failCause(cause) })
431
- }
432
- })
482
+ ): Effect.Effect<Result<A, E>, never, Exclude<R, Scope.Scope> | WorkflowInstance> =>
483
+ Effect.contextWithEffect((context: Context.Context<WorkflowInstance>) => {
484
+ const instance = Context.get(context, InstanceTag)
485
+ return Effect.uninterruptibleMask((restore) =>
486
+ restore(effect).pipe(
487
+ Effect.scoped,
488
+ Effect.matchCause({
489
+ onSuccess: (value) => new Complete({ exit: Exit.succeed(value) }),
490
+ onFailure: (cause) => instance.suspended ? new Suspended() : new Complete({ exit: Exit.failCause(cause) })
491
+ })
492
+ )
433
493
  )
434
- )
494
+ })
495
+
496
+ /**
497
+ * @since 1.0.0
498
+ * @category Result
499
+ */
500
+ export const wrapActivityResult = <A, E, R>(
501
+ effect: Effect.Effect<A, E, R>,
502
+ isSuspend: (value: A) => boolean
503
+ ): Effect.Effect<A, E, R | WorkflowInstance> =>
504
+ Effect.contextWithEffect((context: Context.Context<WorkflowInstance>) => {
505
+ const instance = Context.get(context, InstanceTag)
506
+ const state = instance.activityState
507
+ if (instance.suspended) {
508
+ return state.count > 0 ?
509
+ state.latch.await.pipe(
510
+ Effect.andThen(Effect.yieldNow()),
511
+ Effect.andThen(Effect.interrupt)
512
+ ) :
513
+ Effect.interrupt
514
+ }
515
+ if (state.count === 0) state.latch.unsafeClose()
516
+ state.count++
517
+ return Effect.onExit(effect, (exit) => {
518
+ state.count--
519
+ const isSuspended = Exit.isSuccess(exit) && isSuspend(exit.value)
520
+ return state.count === 0 ? state.latch.open : isSuspended ? state.latch.await : Effect.void
521
+ })
522
+ })
435
523
 
436
524
  /**
437
525
  * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
@@ -481,14 +569,15 @@ export const withCompensation: {
481
569
  compensation: (value: A, cause: Cause.Cause<unknown>) => Effect.Effect<void, never, R2>
482
570
  ): Effect.Effect<A, E, R | R2 | WorkflowInstance | Scope.Scope> =>
483
571
  Effect.uninterruptibleMask((restore) =>
484
- Effect.contextWithEffect((context: Context.Context<WorkflowInstance>) => {
485
- const instance = Context.get(context, InstanceTag)
486
- return Effect.tap(restore(effect), (value) =>
487
- Effect.addFinalizer((exit) => {
488
- if (Exit.isSuccess(exit) || instance.suspended) {
489
- return Effect.void
490
- }
491
- return compensation(value, exit.cause)
492
- }))
493
- })
572
+ Effect.tap(
573
+ restore(effect),
574
+ (value) =>
575
+ Effect.contextWithEffect((context: Context.Context<WorkflowInstance>) =>
576
+ Effect.addFinalizer((exit) =>
577
+ Exit.isSuccess(exit) || Context.get(context, InstanceTag).suspended
578
+ ? Effect.void
579
+ : compensation(value, exit.cause)
580
+ )
581
+ )
582
+ )
494
583
  ))
@@ -2,7 +2,7 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
  import * as Context from "effect/Context"
5
- import type * as Effect from "effect/Effect"
5
+ import * as Effect from "effect/Effect"
6
6
  import type * as Option from "effect/Option"
7
7
  import type * as Schema from "effect/Schema"
8
8
  import type * as Activity from "./Activity.js"
@@ -48,14 +48,6 @@ export class WorkflowEngine extends Context.Tag("@effect/workflow/WorkflowEngine
48
48
  executionId: string
49
49
  ) => Effect.Effect<void>
50
50
 
51
- /**
52
- * Resume a suspended workflow.
53
- */
54
- readonly resume: (
55
- workflowName: string,
56
- executionId: string
57
- ) => Effect.Effect<void>
58
-
59
51
  /**
60
52
  * Execute an activity from a workflow.
61
53
  */
@@ -118,5 +110,22 @@ export class WorkflowInstance extends Context.Tag("@effect/workflow/WorkflowEngi
118
110
  * Whether the workflow has requested to be suspended.
119
111
  */
120
112
  suspended: boolean
113
+
114
+ readonly activityState: {
115
+ count: number
116
+ readonly latch: Effect.Latch
117
+ }
121
118
  }
122
- >() {}
119
+ >() {
120
+ static initial(workflow: Workflow.Any, executionId: string): WorkflowInstance["Type"] {
121
+ return WorkflowInstance.of({
122
+ executionId,
123
+ workflow,
124
+ suspended: false,
125
+ activityState: {
126
+ count: 0,
127
+ latch: Effect.unsafeMakeLatch()
128
+ }
129
+ })
130
+ }
131
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint"
5
+ import * as HttpApiGroup from "@effect/platform/HttpApiGroup"
6
+ import * as Rpc from "@effect/rpc/Rpc"
7
+ import * as RpcGroup from "@effect/rpc/RpcGroup"
8
+ import type { NonEmptyReadonlyArray } from "effect/Array"
9
+ import type * as Workflow from "./Workflow.js"
10
+
11
+ /**
12
+ * Derives an `RpcGroup` from a list of workflows.
13
+ *
14
+ * ```ts
15
+ * import { RpcServer } from "@effect/rpc"
16
+ * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "@effect/workflow"
17
+ * import { Layer, Schema } from "effect"
18
+ *
19
+ * const EmailWorkflow = Workflow.make({
20
+ * name: "EmailWorkflow",
21
+ * payload: {
22
+ * id: Schema.String,
23
+ * to: Schema.String
24
+ * },
25
+ * idempotencyKey: ({ id }) => id
26
+ * })
27
+ *
28
+ * const myWorkflows = [EmailWorkflow] as const
29
+ *
30
+ * // Use WorkflowProxy.toRpcGroup to create a `RpcGroup` from the
31
+ * // workflows
32
+ * class MyRpcs extends WorkflowProxy.toRpcGroup(myWorkflows) {}
33
+ *
34
+ * // Use WorkflowProxyServer.layerRpcHandlers to create a layer that implements
35
+ * // the rpc handlers
36
+ * const ApiLayer = RpcServer.layer(MyRpcs).pipe(
37
+ * Layer.provide(WorkflowProxyServer.layerRpcHandlers(myWorkflows))
38
+ * )
39
+ * ```
40
+ *
41
+ * @since 1.0.0
42
+ * @category Constructors
43
+ */
44
+ export const toRpcGroup = <
45
+ const Workflows extends NonEmptyReadonlyArray<Workflow.Any>,
46
+ const Prefix extends string = ""
47
+ >(
48
+ workflows: Workflows,
49
+ options?: {
50
+ readonly prefix?: Prefix | undefined
51
+ }
52
+ ): RpcGroup.RpcGroup<ConvertRpcs<Workflows[number], Prefix>> => {
53
+ const prefix = options?.prefix ?? ""
54
+ const rpcs: Array<Rpc.Any> = []
55
+ for (const workflow of workflows) {
56
+ rpcs.push(
57
+ Rpc.make(`${prefix}${workflow.name}`, {
58
+ payload: workflow.payloadSchema,
59
+ error: workflow.errorSchema,
60
+ success: workflow.successSchema
61
+ }).annotateContext(workflow.annotations),
62
+ Rpc.make(`${prefix}${workflow.name}Discard`, {
63
+ payload: workflow.payloadSchema
64
+ }).annotateContext(workflow.annotations)
65
+ )
66
+ }
67
+ return RpcGroup.make(...rpcs) as any
68
+ }
69
+
70
+ /**
71
+ * @since 1.0.0
72
+ */
73
+ export type ConvertRpcs<Workflows extends Workflow.Any, Prefix extends string> = Workflows extends Workflow.Workflow<
74
+ infer _Name,
75
+ infer _Payload,
76
+ infer _Success,
77
+ infer _Error
78
+ > ?
79
+ | Rpc.Rpc<`${Prefix}${_Name}`, _Payload, _Success, _Error>
80
+ | Rpc.Rpc<`${Prefix}${_Name}Discard`, _Payload>
81
+ : never
82
+
83
+ /**
84
+ * Derives an `HttpApiGroup` from a list of workflows.
85
+ *
86
+ * ```ts
87
+ * import { HttpApi, HttpApiBuilder } from "@effect/platform"
88
+ * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "@effect/workflow"
89
+ * import { Layer, Schema } from "effect"
90
+ *
91
+ * const EmailWorkflow = Workflow.make({
92
+ * name: "EmailWorkflow",
93
+ * payload: {
94
+ * id: Schema.String,
95
+ * to: Schema.String
96
+ * },
97
+ * idempotencyKey: ({ id }) => id
98
+ * })
99
+ *
100
+ * const myWorkflows = [EmailWorkflow] as const
101
+ *
102
+ * // Use WorkflowProxy.toHttpApiGroup to create a `HttpApiGroup` from the
103
+ * // workflows
104
+ * class MyApi extends HttpApi.make("api")
105
+ * .add(WorkflowProxy.toHttpApiGroup("workflows", myWorkflows))
106
+ * {}
107
+ *
108
+ * // Use WorkflowProxyServer.layerHttpApi to create a layer that implements the
109
+ * // workflows HttpApiGroup
110
+ * const ApiLayer = HttpApiBuilder.api(MyApi).pipe(
111
+ * Layer.provide(WorkflowProxyServer.layerHttpApi(MyApi, "workflows", myWorkflows))
112
+ * )
113
+ * ```
114
+ *
115
+ * @since 1.0.0
116
+ * @category Constructors
117
+ */
118
+ export const toHttpApiGroup = <const Name extends string, const Workflows extends NonEmptyReadonlyArray<Workflow.Any>>(
119
+ name: Name,
120
+ workflows: Workflows
121
+ ): HttpApiGroup.HttpApiGroup<Name, ConvertHttpApi<Workflows[number]>> => {
122
+ let group = HttpApiGroup.make(name)
123
+ for (const workflow of workflows) {
124
+ const path = `/${tagToPath(workflow.name)}` as const
125
+ group = group.add(
126
+ HttpApiEndpoint.post(workflow.name, path)
127
+ .setPayload(workflow.payloadSchema)
128
+ .addSuccess(workflow.successSchema)
129
+ .addError(workflow.errorSchema as any)
130
+ .annotateContext(workflow.annotations)
131
+ ).add(
132
+ HttpApiEndpoint.post(workflow.name + "Discard", `${path}/discard`)
133
+ .setPayload(workflow.payloadSchema)
134
+ .annotateContext(workflow.annotations)
135
+ ) as any
136
+ }
137
+ return group as any
138
+ }
139
+
140
+ const tagToPath = (tag: string): string =>
141
+ tag
142
+ .replace(/[^a-zA-Z0-9]+/g, "-") // Replace non-alphanumeric characters with hyphen
143
+ .replace(/([a-z])([A-Z])/g, "$1-$2") // Insert hyphen before uppercase letters
144
+ .toLowerCase()
145
+
146
+ /**
147
+ * @since 1.0.0
148
+ */
149
+ export type ConvertHttpApi<Workflows extends Workflow.Any> = Workflows extends Workflow.Workflow<
150
+ infer _Name,
151
+ infer _Payload,
152
+ infer _Success,
153
+ infer _Error
154
+ > ?
155
+ | HttpApiEndpoint.HttpApiEndpoint<
156
+ _Name,
157
+ "POST",
158
+ never,
159
+ never,
160
+ _Payload["Type"],
161
+ never,
162
+ _Success["Type"],
163
+ _Error["Type"],
164
+ _Payload["Context"] | _Success["Context"],
165
+ _Error["Context"]
166
+ >
167
+ | HttpApiEndpoint.HttpApiEndpoint<
168
+ `${_Name}Discard`,
169
+ "POST",
170
+ never,
171
+ never,
172
+ _Payload["Type"],
173
+ never,
174
+ void,
175
+ never,
176
+ _Payload["Context"]
177
+ > :
178
+ never
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import type * as HttpApi from "@effect/platform/HttpApi"
5
+ import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder"
6
+ import type { ApiGroup, HttpApiGroup } from "@effect/platform/HttpApiGroup"
7
+ import type * as Rpc from "@effect/rpc/Rpc"
8
+ import type { NonEmptyReadonlyArray } from "effect/Array"
9
+ import * as Context from "effect/Context"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Layer from "effect/Layer"
12
+ import type * as Workflow from "./Workflow.js"
13
+ import type { WorkflowEngine } from "./WorkflowEngine.js"
14
+
15
+ /**
16
+ * @since 1.0.0
17
+ * @category Layers
18
+ */
19
+ export const layerHttpApi = <
20
+ ApiId extends string,
21
+ Groups extends HttpApiGroup.Any,
22
+ ApiE,
23
+ ApiR,
24
+ Name extends HttpApiGroup.Name<Groups>,
25
+ const Workflows extends NonEmptyReadonlyArray<Workflow.Any>
26
+ >(
27
+ api: HttpApi.HttpApi<ApiId, Groups, ApiE, ApiR>,
28
+ name: Name,
29
+ workflows: Workflows
30
+ ): Layer.Layer<
31
+ ApiGroup<ApiId, Name>,
32
+ never,
33
+ WorkflowEngine | Workflow.Registrations<Workflows[number]> | Workflow.Requirements<Workflows[number]>
34
+ > =>
35
+ HttpApiBuilder.group(
36
+ api,
37
+ name,
38
+ Effect.fnUntraced(function*(handlers_) {
39
+ let handlers = handlers_ as any
40
+ for (const workflow_ of workflows) {
41
+ const workflow = workflow_ as Workflow.Workflow<string, any, any, any>
42
+ handlers = handlers
43
+ .handle(
44
+ workflow.name as any,
45
+ ({ payload }: { payload: any }) => workflow.execute(payload)
46
+ )
47
+ .handle(
48
+ workflow.name + "Discard" as any,
49
+ ({ payload }: { payload: any }) => workflow.execute(payload, { discard: true } as any)
50
+ )
51
+ }
52
+ return handlers as HttpApiBuilder.Handlers<never, never, never>
53
+ })
54
+ )
55
+
56
+ /**
57
+ * @since 1.0.0
58
+ * @category Layers
59
+ */
60
+ export const layerRpcHandlers = <
61
+ const Workflows extends NonEmptyReadonlyArray<Workflow.Any>,
62
+ const Prefix extends string = ""
63
+ >(workflows: Workflows, options?: {
64
+ readonly prefix?: Prefix
65
+ }): Layer.Layer<
66
+ RpcHandlers<Workflows[number], Prefix>,
67
+ never,
68
+ WorkflowEngine | Workflow.Registrations<Workflows[number]> | Workflow.Requirements<Workflows[number]>
69
+ > =>
70
+ Layer.effectContext(Effect.gen(function*() {
71
+ const context = yield* Effect.context<never>()
72
+ const prefix = options?.prefix ?? ""
73
+ const handlers = new Map<string, Rpc.Handler<string>>()
74
+ for (const workflow_ of workflows) {
75
+ const workflow = workflow_ as Workflow.Workflow<string, any, any, any>
76
+ const tag = `${prefix}${workflow.name}`
77
+ const tagDiscard = `${tag}Discard`
78
+ const key = `@effect/rpc/Rpc/${tag}`
79
+ const keyDiscard = `${key}Discard`
80
+ handlers.set(key, {
81
+ context,
82
+ tag,
83
+ handler: (payload: any) => workflow.execute(payload) as any
84
+ } as any)
85
+ handlers.set(keyDiscard, {
86
+ context,
87
+ tag: tagDiscard,
88
+ handler: (payload: any) => workflow.execute(payload, { discard: true } as any) as any
89
+ } as any)
90
+ }
91
+ return Context.unsafeMake(handlers)
92
+ }))
93
+
94
+ /**
95
+ * @since 1.0.0
96
+ */
97
+ export type RpcHandlers<Workflows extends Workflow.Any, Prefix extends string> = Workflows extends Workflow.Workflow<
98
+ infer _Name,
99
+ infer _Payload,
100
+ infer _Success,
101
+ infer _Error
102
+ > ? Rpc.Handler<`${Prefix}${_Name}`> | Rpc.Handler<`${Prefix}${_Name}Discard`>
103
+ : never
package/src/index.ts CHANGED
@@ -22,3 +22,13 @@ export * as Workflow from "./Workflow.js"
22
22
  * @since 1.0.0
23
23
  */
24
24
  export * as WorkflowEngine from "./WorkflowEngine.js"
25
+
26
+ /**
27
+ * @since 1.0.0
28
+ */
29
+ export * as WorkflowProxy from "./WorkflowProxy.js"
30
+
31
+ /**
32
+ * @since 1.0.0
33
+ */
34
+ export * as WorkflowProxyServer from "./WorkflowProxyServer.js"