@effect/workflow 0.1.0

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 (58) hide show
  1. package/Activity/package.json +6 -0
  2. package/DurableClock/package.json +6 -0
  3. package/DurableDeferred/package.json +6 -0
  4. package/LICENSE +21 -0
  5. package/README.md +133 -0
  6. package/Workflow/package.json +6 -0
  7. package/WorkflowEngine/package.json +6 -0
  8. package/dist/cjs/Activity.js +100 -0
  9. package/dist/cjs/Activity.js.map +1 -0
  10. package/dist/cjs/DurableClock.js +49 -0
  11. package/dist/cjs/DurableClock.js.map +1 -0
  12. package/dist/cjs/DurableDeferred.js +158 -0
  13. package/dist/cjs/DurableDeferred.js.map +1 -0
  14. package/dist/cjs/Workflow.js +198 -0
  15. package/dist/cjs/Workflow.js.map +1 -0
  16. package/dist/cjs/WorkflowEngine.js +25 -0
  17. package/dist/cjs/WorkflowEngine.js.map +1 -0
  18. package/dist/cjs/index.js +18 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/internal/crypto.js +19 -0
  21. package/dist/cjs/internal/crypto.js.map +1 -0
  22. package/dist/dts/Activity.d.ts +70 -0
  23. package/dist/dts/Activity.d.ts.map +1 -0
  24. package/dist/dts/DurableClock.d.ts +42 -0
  25. package/dist/dts/DurableClock.d.ts.map +1 -0
  26. package/dist/dts/DurableDeferred.d.ts +252 -0
  27. package/dist/dts/DurableDeferred.d.ts.map +1 -0
  28. package/dist/dts/Workflow.d.ts +211 -0
  29. package/dist/dts/Workflow.d.ts.map +1 -0
  30. package/dist/dts/WorkflowEngine.d.ts +91 -0
  31. package/dist/dts/WorkflowEngine.d.ts.map +1 -0
  32. package/dist/dts/index.d.ts +21 -0
  33. package/dist/dts/index.d.ts.map +1 -0
  34. package/dist/dts/internal/crypto.d.ts +2 -0
  35. package/dist/dts/internal/crypto.d.ts.map +1 -0
  36. package/dist/esm/Activity.js +90 -0
  37. package/dist/esm/Activity.js.map +1 -0
  38. package/dist/esm/DurableClock.js +40 -0
  39. package/dist/esm/DurableClock.js.map +1 -0
  40. package/dist/esm/DurableDeferred.js +155 -0
  41. package/dist/esm/DurableDeferred.js.map +1 -0
  42. package/dist/esm/Workflow.js +183 -0
  43. package/dist/esm/Workflow.js.map +1 -0
  44. package/dist/esm/WorkflowEngine.js +15 -0
  45. package/dist/esm/WorkflowEngine.js.map +1 -0
  46. package/dist/esm/index.js +21 -0
  47. package/dist/esm/index.js.map +1 -0
  48. package/dist/esm/internal/crypto.js +11 -0
  49. package/dist/esm/internal/crypto.js.map +1 -0
  50. package/dist/esm/package.json +4 -0
  51. package/package.json +74 -0
  52. package/src/Activity.ts +177 -0
  53. package/src/DurableClock.ts +82 -0
  54. package/src/DurableDeferred.ts +461 -0
  55. package/src/Workflow.ts +401 -0
  56. package/src/WorkflowEngine.ts +122 -0
  57. package/src/index.ts +24 -0
  58. package/src/internal/crypto.ts +15 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as Context from "effect/Context"
5
+ import * as Data from "effect/Data"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Exit from "effect/Exit"
8
+ import * as Layer from "effect/Layer"
9
+ import type { Pipeable } from "effect/Pipeable"
10
+ import * as Predicate from "effect/Predicate"
11
+ import * as PrimaryKey from "effect/PrimaryKey"
12
+ import * as Schedule from "effect/Schedule"
13
+ import * as Schema from "effect/Schema"
14
+ import type * as AST from "effect/SchemaAST"
15
+ import { makeHashDigest } from "./internal/crypto.js"
16
+ import type { WorkflowEngine, WorkflowInstance } from "./WorkflowEngine.js"
17
+
18
+ /**
19
+ * @since 1.0.0
20
+ * @category Symbols
21
+ */
22
+ export const TypeId: unique symbol = Symbol.for("@effect/workflow/Workflow")
23
+
24
+ /**
25
+ * @since 1.0.0
26
+ * @category Symbols
27
+ */
28
+ export type TypeId = typeof TypeId
29
+
30
+ /**
31
+ * @since 1.0.0
32
+ * @category Models
33
+ */
34
+ export interface Workflow<
35
+ Name extends string,
36
+ Payload extends AnyStructSchema,
37
+ Success extends Schema.Schema.Any,
38
+ Error extends Schema.Schema.All
39
+ > {
40
+ readonly [TypeId]: TypeId
41
+ readonly name: Name
42
+ readonly payloadSchema: Payload
43
+ readonly successSchema: Success
44
+ readonly errorSchema: Error
45
+
46
+ /**
47
+ * Execute the workflow with the given payload.
48
+ */
49
+ readonly execute: <const Discard extends boolean = false>(
50
+ payload: [keyof Payload["fields"]] extends [never] ? void
51
+ : Schema.Simplify<Schema.Struct.Constructor<Payload["fields"]>>,
52
+ options?: {
53
+ readonly discard?: Discard
54
+ }
55
+ ) => Effect.Effect<
56
+ Discard extends true ? void : Success["Type"],
57
+ Discard extends true ? never : Error["Type"],
58
+ WorkflowEngine | Registration<Name> | Payload["Context"] | Success["Context"] | Error["Context"]
59
+ >
60
+
61
+ /**
62
+ * Interrupt a workflow execution for the given execution ID.
63
+ */
64
+ readonly interrupt: (executionId: string) => Effect.Effect<void, never, WorkflowEngine | Registration<Name>>
65
+
66
+ /**
67
+ * Create a layer that registers the workflow and provides an effect to
68
+ * execute it.
69
+ */
70
+ readonly toLayer: <R>(
71
+ execute: (
72
+ payload: Payload["Type"],
73
+ executionId: string
74
+ ) => Effect.Effect<Success["Type"], Error["Type"], R>
75
+ ) => Layer.Layer<
76
+ Registration<Name> | WorkflowEngine,
77
+ never,
78
+ | WorkflowEngine
79
+ | Exclude<R, WorkflowEngine | WorkflowInstance>
80
+ | Payload["Context"]
81
+ | Success["Context"]
82
+ | Error["Context"]
83
+ >
84
+
85
+ /**
86
+ * For the given payload, compute the deterministic execution ID.
87
+ */
88
+ readonly executionId: (
89
+ payload: Schema.Simplify<Schema.Struct.Constructor<Payload["fields"]>>
90
+ ) => Effect.Effect<string>
91
+ }
92
+
93
+ /**
94
+ * @since 1.0.0
95
+ */
96
+ export interface AnyStructSchema extends Pipeable {
97
+ readonly [Schema.TypeId]: any
98
+ readonly make: any
99
+ readonly Type: any
100
+ readonly Encoded: any
101
+ readonly Context: any
102
+ readonly ast: AST.AST
103
+ readonly fields: Schema.Struct.Fields
104
+ readonly annotations: any
105
+ }
106
+
107
+ /**
108
+ * @since 1.0.0
109
+ * @category constructors
110
+ */
111
+ export interface AnyTaggedRequestSchema extends AnyStructSchema {
112
+ readonly _tag: string
113
+ readonly Type: PrimaryKey.PrimaryKey
114
+ readonly success: Schema.Schema.Any
115
+ readonly failure: Schema.Schema.All
116
+ }
117
+
118
+ /**
119
+ * @since 1.0.0
120
+ * @category Models
121
+ */
122
+ export interface Registration<Name extends string> {
123
+ readonly _: unique symbol
124
+ readonly name: Name
125
+ }
126
+
127
+ /**
128
+ * @since 1.0.0
129
+ * @category Models
130
+ */
131
+ export interface Any {
132
+ readonly [TypeId]: TypeId
133
+ readonly name: string
134
+ readonly payloadSchema: AnyStructSchema
135
+ readonly successSchema: Schema.Schema.Any
136
+ readonly errorSchema: Schema.Schema.All
137
+ readonly executionId: (payload: any) => Effect.Effect<string>
138
+ }
139
+
140
+ const EngineTag = Context.GenericTag<WorkflowEngine, WorkflowEngine["Type"]>(
141
+ "@effect/workflow/WorkflowEngine" satisfies typeof WorkflowEngine.key
142
+ )
143
+
144
+ const InstanceTag = Context.GenericTag<WorkflowInstance, WorkflowInstance["Type"]>(
145
+ "@effect/workflow/WorkflowEngine/WorkflowInstance" satisfies typeof WorkflowInstance.key
146
+ )
147
+
148
+ /**
149
+ * @since 1.0.0
150
+ * @category Constructors
151
+ */
152
+ export const make = <
153
+ const Name extends string,
154
+ Payload extends Schema.Struct.Fields | AnyStructSchema,
155
+ Success extends Schema.Schema.Any = typeof Schema.Void,
156
+ Error extends Schema.Schema.All = typeof Schema.Never
157
+ >(
158
+ options: {
159
+ readonly name: Name
160
+ readonly payload: Payload
161
+ readonly idempotencyKey: (
162
+ payload: Payload extends Schema.Struct.Fields ? Schema.Struct.Type<Payload> : Payload["Type"]
163
+ ) => string
164
+ readonly success?: Success
165
+ readonly error?: Error
166
+ readonly suspendedRetrySchedule?: Schedule.Schedule<any, unknown> | undefined
167
+ }
168
+ ): Workflow<Name, Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload, Success, Error> => {
169
+ const suspendedRetrySchedule = options.suspendedRetrySchedule ?? defaultRetrySchedule
170
+ const makeExecutionId = (payload: any) => makeHashDigest(`${options.name}-${options.idempotencyKey(payload)}`)
171
+ const self: Workflow<Name, any, Success, Error> = {
172
+ [TypeId]: TypeId,
173
+ name: options.name,
174
+ payloadSchema: Schema.isSchema(options.payload) ? options.payload : Schema.Struct(options.payload as any),
175
+ successSchema: options.success ?? Schema.Void as any,
176
+ errorSchema: options.error ?? Schema.Never as any,
177
+ execute: Effect.fnUntraced(
178
+ function*(fields: any, opts) {
179
+ const payload = self.payloadSchema.make(fields)
180
+ const engine = yield* EngineTag
181
+ const executionId = yield* makeExecutionId(payload)
182
+ yield* Effect.annotateCurrentSpan({ executionId })
183
+ if (opts?.discard) {
184
+ return yield* engine.execute({
185
+ workflow: self,
186
+ executionId,
187
+ payload,
188
+ discard: true
189
+ })
190
+ }
191
+ let sleep: Effect.Effect<any> | undefined
192
+ while (true) {
193
+ const result = yield* engine.execute({
194
+ workflow: self,
195
+ executionId,
196
+ payload,
197
+ discard: false
198
+ })
199
+ if (result._tag === "Complete") {
200
+ return yield* result.exit as Exit.Exit<Success["Type"], Error["Type"]>
201
+ }
202
+ // @effect-diagnostics effect/floatingEffect:off
203
+ sleep ??= (yield* Schedule.driver(suspendedRetrySchedule)).next(void 0).pipe(
204
+ Effect.catchAll(() => Effect.dieMessage(`${options.name}.execute: suspendedRetrySchedule exhausted`))
205
+ )
206
+ yield* sleep
207
+ }
208
+ },
209
+ Effect.withSpan(`${options.name}.execute`, { captureStackTrace: false })
210
+ ),
211
+ interrupt: Effect.fnUntraced(
212
+ function*(executionId: string) {
213
+ const engine = yield* EngineTag
214
+ yield* engine.interrupt(self, executionId)
215
+ },
216
+ (effect, executionId) =>
217
+ Effect.withSpan(effect, `${options.name}.interrupt`, {
218
+ captureStackTrace: false,
219
+ attributes: { executionId }
220
+ })
221
+ ),
222
+ toLayer: (execute) =>
223
+ Layer.effectContext(Effect.gen(function*() {
224
+ const context = yield* Effect.context<WorkflowEngine>()
225
+ const engine = Context.get(context, EngineTag)
226
+ yield* engine.register(self, (payload, executionId) =>
227
+ execute(payload, executionId).pipe(
228
+ Effect.provide(context)
229
+ ) as any)
230
+ return EngineTag.context(engine)
231
+ })) as any,
232
+ executionId: (payload) => makeExecutionId(self.payloadSchema.make(payload))
233
+ }
234
+
235
+ return self
236
+ }
237
+
238
+ const defaultRetrySchedule = Schedule.exponential(200, 1.5).pipe(
239
+ Schedule.union(Schedule.spaced(30000))
240
+ )
241
+
242
+ /**
243
+ * @since 1.0.0
244
+ * @category Constructors
245
+ */
246
+ export const fromTaggedRequest = <S extends AnyTaggedRequestSchema>(schema: S, options?: {
247
+ readonly suspendedRetrySchedule?: Schedule.Schedule<any, unknown> | undefined
248
+ }): Workflow<S["_tag"], S, S["success"], S["failure"]> =>
249
+ make({
250
+ name: schema._tag,
251
+ payload: schema as any,
252
+ success: schema.success,
253
+ error: schema.failure,
254
+ idempotencyKey: PrimaryKey.value,
255
+ suspendedRetrySchedule: options?.suspendedRetrySchedule
256
+ })
257
+
258
+ /**
259
+ * @since 1.0.0
260
+ * @category Result
261
+ */
262
+ export const ResultTypeId: unique symbol = Symbol.for("@effect/workflow/Workflow/Result")
263
+
264
+ /**
265
+ * @since 1.0.0
266
+ * @category Result
267
+ */
268
+ export type ResultTypeId = typeof ResultTypeId
269
+
270
+ /**
271
+ * @since 1.0.0
272
+ * @category Result
273
+ */
274
+ export const isResult = <A = unknown, E = unknown>(u: unknown): u is Result<A, E> =>
275
+ Predicate.hasProperty(u, ResultTypeId)
276
+
277
+ /**
278
+ * @since 1.0.0
279
+ * @category Result
280
+ */
281
+ export type Result<A, E> = Complete<A, E> | Suspended
282
+
283
+ /**
284
+ * @since 1.0.0
285
+ * @category Result
286
+ */
287
+ export type ResultEncoded<A, E> = CompleteEncoded<A, E> | typeof Suspended.Encoded
288
+
289
+ /**
290
+ * @since 1.0.0
291
+ * @category Result
292
+ */
293
+ export class Complete<A, E> extends Data.TaggedClass("Complete")<{
294
+ readonly exit: Exit.Exit<A, E>
295
+ }> {
296
+ /**
297
+ * @since 1.0.0
298
+ */
299
+ readonly [ResultTypeId]: ResultTypeId = ResultTypeId
300
+
301
+ /**
302
+ * @since 1.0.0
303
+ */
304
+ static SchemaFromSelf<Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(_options: {
305
+ readonly success: Success
306
+ readonly error: Error
307
+ }): Schema.Schema<Complete<Success["Type"], Error["Type"]>> {
308
+ return Schema.declare((u): u is Complete<Success["Type"], Error["Type"]> => isResult(u) && u._tag === "Complete")
309
+ }
310
+
311
+ /**
312
+ * @since 1.0.0
313
+ */
314
+ static SchemaEncoded<Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(options: {
315
+ readonly success: Success
316
+ readonly error: Error
317
+ }) {
318
+ return Schema.Struct({
319
+ _tag: Schema.tag("Complete"),
320
+ exit: Schema.Exit({ success: options.success, failure: options.error, defect: Schema.Defect })
321
+ })
322
+ }
323
+
324
+ /**
325
+ * @since 1.0.0
326
+ */
327
+ static Schema<Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(options: {
328
+ readonly success: Success
329
+ readonly error: Error
330
+ }): Schema.Schema<
331
+ Complete<Success["Type"], Error["Type"]>,
332
+ CompleteEncoded<Success["Encoded"], Error["Encoded"]>
333
+ > {
334
+ return Schema.transform(
335
+ this.SchemaEncoded(options),
336
+ this.SchemaFromSelf(options),
337
+ {
338
+ decode(fromA) {
339
+ return new Complete({ exit: fromA.exit })
340
+ },
341
+ encode(toI) {
342
+ return toI
343
+ }
344
+ }
345
+ ) as any
346
+ }
347
+ }
348
+
349
+ /**
350
+ * @since 1.0.0
351
+ * @category Result
352
+ */
353
+ export interface CompleteEncoded<A, E> {
354
+ readonly _tag: "Complete"
355
+ readonly exit: Schema.ExitEncoded<A, E, unknown>
356
+ }
357
+
358
+ /**
359
+ * @since 1.0.0
360
+ * @category Result
361
+ */
362
+ export class Suspended extends Schema.TaggedClass<Suspended>("@effect/workflow/Workflow/Suspended")("Suspended", {}) {
363
+ /**
364
+ * @since 1.0.0
365
+ */
366
+ readonly [ResultTypeId]: ResultTypeId = ResultTypeId
367
+ }
368
+
369
+ /**
370
+ * @since 1.0.0
371
+ * @category Result
372
+ */
373
+ export const Result = <Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(
374
+ options: {
375
+ readonly success: Success
376
+ readonly error: Error
377
+ }
378
+ ): Schema.Schema<
379
+ Result<Success["Type"], Error["Type"]>,
380
+ ResultEncoded<Success["Encoded"], Error["Encoded"]>,
381
+ Success["Context"] | Error["Context"]
382
+ > => Schema.Union(Complete.Schema(options), Suspended)
383
+
384
+ /**
385
+ * @since 1.0.0
386
+ * @category Result
387
+ */
388
+ export const intoResult = <A, E, R>(
389
+ effect: Effect.Effect<A, E, R>
390
+ ): Effect.Effect<Result<A, E>, never, R | WorkflowInstance> =>
391
+ Effect.uninterruptibleMask((restore) =>
392
+ Effect.withFiberRuntime((fiber) =>
393
+ Effect.matchCause(restore(effect), {
394
+ onSuccess: (value) => new Complete({ exit: Exit.succeed(value) }),
395
+ onFailure(cause) {
396
+ const instance = Context.unsafeGet(fiber.currentContext, InstanceTag)
397
+ return instance.suspended ? new Suspended() : new Complete({ exit: Exit.failCause(cause) })
398
+ }
399
+ })
400
+ )
401
+ )
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as Context from "effect/Context"
5
+ import type * as Effect from "effect/Effect"
6
+ import type * as Option from "effect/Option"
7
+ import type * as Schema from "effect/Schema"
8
+ import type * as Activity from "./Activity.js"
9
+ import type { DurableClock } from "./DurableClock.js"
10
+ import type * as DurableDeferred from "./DurableDeferred.js"
11
+ import type * as Workflow from "./Workflow.js"
12
+
13
+ /**
14
+ * @since 1.0.0
15
+ * @category Services
16
+ */
17
+ export class WorkflowEngine extends Context.Tag("@effect/workflow/WorkflowEngine")<
18
+ WorkflowEngine,
19
+ {
20
+ /**
21
+ * Register a workflow with the engine.
22
+ */
23
+ readonly register: (
24
+ workflow: Workflow.Any,
25
+ execute: (
26
+ payload: object,
27
+ executionId: string
28
+ ) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
29
+ ) => Effect.Effect<void>
30
+
31
+ /**
32
+ * Execute a registered workflow.
33
+ */
34
+ readonly execute: <const Discard extends boolean>(
35
+ options: {
36
+ readonly workflow: Workflow.Any
37
+ readonly executionId: string
38
+ readonly payload: object
39
+ readonly discard: Discard
40
+ }
41
+ ) => Effect.Effect<Discard extends true ? void : Workflow.Result<unknown, unknown>>
42
+
43
+ /**
44
+ * Interrupt a registered workflow.
45
+ */
46
+ readonly interrupt: (
47
+ workflow: Workflow.Any,
48
+ executionId: string
49
+ ) => Effect.Effect<void>
50
+
51
+ /**
52
+ * Resume a suspended workflow.
53
+ */
54
+ readonly resume: (
55
+ workflowName: string,
56
+ executionId: string
57
+ ) => Effect.Effect<void>
58
+
59
+ /**
60
+ * Execute an activity from a workflow.
61
+ */
62
+ readonly activityExecute: (
63
+ options: {
64
+ readonly activity: Activity.Any
65
+ readonly attempt: number
66
+ }
67
+ ) => Effect.Effect<Workflow.Result<unknown, unknown>, never, WorkflowInstance>
68
+
69
+ /**
70
+ * Try to retrieve the result of an DurableDeferred
71
+ */
72
+ readonly deferredResult: (
73
+ deferred: DurableDeferred.Any
74
+ ) => Effect.Effect<Option.Option<Schema.ExitEncoded<unknown, unknown, unknown>>, never, WorkflowInstance>
75
+
76
+ /**
77
+ * Set the result of a DurableDeferred, and then resume any waiting
78
+ * workflows.
79
+ */
80
+ readonly deferredDone: (
81
+ options: {
82
+ readonly workflowName: string
83
+ readonly executionId: string
84
+ readonly deferred: DurableDeferred.Any
85
+ readonly exit: Schema.ExitEncoded<unknown, unknown, unknown>
86
+ }
87
+ ) => Effect.Effect<void>
88
+
89
+ /**
90
+ * Schedule a wake up for a DurableClock
91
+ */
92
+ readonly scheduleClock: (options: {
93
+ readonly workflow: Workflow.Any
94
+ readonly executionId: string
95
+ readonly clock: DurableClock
96
+ }) => Effect.Effect<void>
97
+ }
98
+ >() {}
99
+
100
+ /**
101
+ * @since 1.0.0
102
+ * @category Services
103
+ */
104
+ export class WorkflowInstance extends Context.Tag("@effect/workflow/WorkflowEngine/WorkflowInstance")<
105
+ WorkflowInstance,
106
+ {
107
+ /**
108
+ * The workflow execution ID.
109
+ */
110
+ readonly executionId: string
111
+
112
+ /**
113
+ * The workflow definition.
114
+ */
115
+ readonly workflow: Workflow.Any
116
+
117
+ /**
118
+ * Whether the workflow has requested to be suspended.
119
+ */
120
+ suspended: boolean
121
+ }
122
+ >() {}
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ export * as Activity from "./Activity.js"
5
+
6
+ /**
7
+ * @since 1.0.0
8
+ */
9
+ export * as DurableClock from "./DurableClock.js"
10
+
11
+ /**
12
+ * @since 1.0.0
13
+ */
14
+ export * as DurableDeferred from "./DurableDeferred.js"
15
+
16
+ /**
17
+ * @since 1.0.0
18
+ */
19
+ export * as Workflow from "./Workflow.js"
20
+
21
+ /**
22
+ * @since 1.0.0
23
+ */
24
+ export * as WorkflowEngine from "./WorkflowEngine.js"
@@ -0,0 +1,15 @@
1
+ import * as Effect from "effect/Effect"
2
+
3
+ /** @internal */
4
+ export const makeHashDigest = (original: string) =>
5
+ Effect.map(
6
+ Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(original))),
7
+ (buffer) => {
8
+ const data = new Uint8Array(buffer)
9
+ let hexString = ""
10
+ for (let i = 0; i < 16; i++) {
11
+ hexString += data[i].toString(16).padStart(2, "0")
12
+ }
13
+ return hexString
14
+ }
15
+ )