@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.
- package/WorkflowProxy/package.json +6 -0
- package/WorkflowProxyServer/package.json +6 -0
- package/dist/cjs/Activity.js +17 -8
- package/dist/cjs/Activity.js.map +1 -1
- package/dist/cjs/DurableDeferred.js +40 -2
- package/dist/cjs/DurableDeferred.js.map +1 -1
- package/dist/cjs/Workflow.js +44 -21
- package/dist/cjs/Workflow.js.map +1 -1
- package/dist/cjs/WorkflowEngine.js +14 -1
- package/dist/cjs/WorkflowEngine.js.map +1 -1
- package/dist/cjs/WorkflowProxy.js +111 -0
- package/dist/cjs/WorkflowProxy.js.map +1 -0
- package/dist/cjs/WorkflowProxyServer.js +61 -0
- package/dist/cjs/WorkflowProxyServer.js.map +1 -0
- package/dist/cjs/index.js +5 -1
- package/dist/dts/Activity.d.ts +13 -6
- package/dist/dts/Activity.d.ts.map +1 -1
- package/dist/dts/DurableDeferred.d.ts +28 -1
- package/dist/dts/DurableDeferred.d.ts.map +1 -1
- package/dist/dts/Workflow.d.ts +28 -1
- package/dist/dts/Workflow.d.ts.map +1 -1
- package/dist/dts/WorkflowEngine.d.ts +6 -5
- package/dist/dts/WorkflowEngine.d.ts.map +1 -1
- package/dist/dts/WorkflowProxy.d.ts +90 -0
- package/dist/dts/WorkflowProxy.d.ts.map +1 -0
- package/dist/dts/WorkflowProxyServer.d.ts +27 -0
- package/dist/dts/WorkflowProxyServer.d.ts.map +1 -0
- package/dist/dts/index.d.ts +8 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/Activity.js +15 -6
- package/dist/esm/Activity.js.map +1 -1
- package/dist/esm/DurableDeferred.js +38 -1
- package/dist/esm/DurableDeferred.js.map +1 -1
- package/dist/esm/Workflow.js +41 -19
- package/dist/esm/Workflow.js.map +1 -1
- package/dist/esm/WorkflowEngine.js +14 -1
- package/dist/esm/WorkflowEngine.js.map +1 -1
- package/dist/esm/WorkflowProxy.js +101 -0
- package/dist/esm/WorkflowProxy.js.map +1 -0
- package/dist/esm/WorkflowProxyServer.js +52 -0
- package/dist/esm/WorkflowProxyServer.js.map +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +20 -2
- package/src/Activity.ts +47 -13
- package/src/DurableDeferred.ts +103 -2
- package/src/Workflow.ts +111 -22
- package/src/WorkflowEngine.ts +19 -10
- package/src/WorkflowProxy.ts +178 -0
- package/src/WorkflowProxyServer.ts +103 -0
- package/src/index.ts +10 -0
package/src/DurableDeferred.ts
CHANGED
|
@@ -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
|
|
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.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
Effect.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
))
|
package/src/WorkflowEngine.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
4
|
import * as Context from "effect/Context"
|
|
5
|
-
import
|
|
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"
|