@effect/workflow 0.12.5 → 0.13.1
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/DurableQueue/package.json +6 -0
- package/dist/cjs/Activity.js +2 -6
- package/dist/cjs/Activity.js.map +1 -1
- package/dist/cjs/DurableClock.js +1 -2
- package/dist/cjs/DurableClock.js.map +1 -1
- package/dist/cjs/DurableDeferred.js +16 -18
- package/dist/cjs/DurableDeferred.js.map +1 -1
- package/dist/cjs/DurableQueue.js +186 -0
- package/dist/cjs/DurableQueue.js.map +1 -0
- package/dist/cjs/Workflow.js +8 -53
- package/dist/cjs/Workflow.js.map +1 -1
- package/dist/cjs/WorkflowEngine.js +234 -3
- package/dist/cjs/WorkflowEngine.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/dts/Activity.d.ts +1 -0
- package/dist/dts/Activity.d.ts.map +1 -1
- package/dist/dts/DurableClock.d.ts.map +1 -1
- package/dist/dts/DurableDeferred.d.ts +2 -2
- package/dist/dts/DurableDeferred.d.ts.map +1 -1
- package/dist/dts/DurableQueue.d.ts +116 -0
- package/dist/dts/DurableQueue.d.ts.map +1 -0
- package/dist/dts/Workflow.d.ts +2 -2
- package/dist/dts/Workflow.d.ts.map +1 -1
- package/dist/dts/WorkflowEngine.d.ts +59 -30
- package/dist/dts/WorkflowEngine.d.ts.map +1 -1
- package/dist/dts/index.d.ts +4 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/Activity.js +2 -6
- package/dist/esm/Activity.js.map +1 -1
- package/dist/esm/DurableClock.js +1 -2
- package/dist/esm/DurableClock.js.map +1 -1
- package/dist/esm/DurableDeferred.js +16 -18
- package/dist/esm/DurableDeferred.js.map +1 -1
- package/dist/esm/DurableQueue.js +176 -0
- package/dist/esm/DurableQueue.js.map +1 -0
- package/dist/esm/Workflow.js +7 -52
- package/dist/esm/Workflow.js.map +1 -1
- package/dist/esm/WorkflowEngine.js +232 -2
- package/dist/esm/WorkflowEngine.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +13 -5
- package/src/Activity.ts +3 -8
- package/src/DurableClock.ts +1 -2
- package/src/DurableDeferred.ts +24 -30
- package/src/DurableQueue.ts +332 -0
- package/src/Workflow.ts +9 -61
- package/src/WorkflowEngine.ts +509 -44
- package/src/index.ts +5 -0
package/src/WorkflowEngine.ts
CHANGED
|
@@ -4,16 +4,21 @@
|
|
|
4
4
|
import type * as Cause from "effect/Cause"
|
|
5
5
|
import * as Context from "effect/Context"
|
|
6
6
|
import * as Effect from "effect/Effect"
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import * as Exit from "effect/Exit"
|
|
8
|
+
import * as Fiber from "effect/Fiber"
|
|
9
|
+
import * as FiberMap from "effect/FiberMap"
|
|
10
|
+
import * as Layer from "effect/Layer"
|
|
11
|
+
import * as Option from "effect/Option"
|
|
12
|
+
import * as Schedule from "effect/Schedule"
|
|
13
|
+
import * as Schema from "effect/Schema"
|
|
9
14
|
import type * as Scope from "effect/Scope"
|
|
10
15
|
import type * as Activity from "./Activity.js"
|
|
11
16
|
import type { DurableClock } from "./DurableClock.js"
|
|
12
17
|
import type * as DurableDeferred from "./DurableDeferred.js"
|
|
13
|
-
import
|
|
18
|
+
import * as Workflow from "./Workflow.js"
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
* @since
|
|
21
|
+
* @since 4.0.0
|
|
17
22
|
* @category Services
|
|
18
23
|
*/
|
|
19
24
|
export class WorkflowEngine extends Context.Tag("@effect/workflow/WorkflowEngine")<
|
|
@@ -22,39 +27,77 @@ export class WorkflowEngine extends Context.Tag("@effect/workflow/WorkflowEngine
|
|
|
22
27
|
/**
|
|
23
28
|
* Register a workflow with the engine.
|
|
24
29
|
*/
|
|
25
|
-
readonly register:
|
|
26
|
-
|
|
30
|
+
readonly register: <
|
|
31
|
+
Name extends string,
|
|
32
|
+
Payload extends Workflow.AnyStructSchema,
|
|
33
|
+
Success extends Schema.Schema.Any,
|
|
34
|
+
Error extends Schema.Schema.All,
|
|
35
|
+
R
|
|
36
|
+
>(
|
|
37
|
+
workflow: Workflow.Workflow<Name, Payload, Success, Error>,
|
|
27
38
|
execute: (
|
|
28
|
-
payload:
|
|
39
|
+
payload: Payload["Type"],
|
|
29
40
|
executionId: string
|
|
30
|
-
) => Effect.Effect<
|
|
31
|
-
) => Effect.Effect<
|
|
41
|
+
) => Effect.Effect<Success["Type"], Error["Type"], R>
|
|
42
|
+
) => Effect.Effect<
|
|
43
|
+
void,
|
|
44
|
+
never,
|
|
45
|
+
| Scope.Scope
|
|
46
|
+
| Exclude<
|
|
47
|
+
R,
|
|
48
|
+
| WorkflowEngine
|
|
49
|
+
| WorkflowInstance
|
|
50
|
+
| Workflow.Execution<Name>
|
|
51
|
+
| Scope.Scope
|
|
52
|
+
>
|
|
53
|
+
| Payload["Context"]
|
|
54
|
+
| Success["Context"]
|
|
55
|
+
| Error["Context"]
|
|
56
|
+
>
|
|
32
57
|
|
|
33
58
|
/**
|
|
34
59
|
* Execute a registered workflow.
|
|
35
60
|
*/
|
|
36
|
-
readonly execute: <
|
|
61
|
+
readonly execute: <
|
|
62
|
+
Name extends string,
|
|
63
|
+
Payload extends Workflow.AnyStructSchema,
|
|
64
|
+
Success extends Schema.Schema.Any,
|
|
65
|
+
Error extends Schema.Schema.All,
|
|
66
|
+
const Discard extends boolean = false
|
|
67
|
+
>(
|
|
68
|
+
workflow: Workflow.Workflow<Name, Payload, Success, Error>,
|
|
37
69
|
options: {
|
|
38
|
-
readonly workflow: Workflow.Any
|
|
39
70
|
readonly executionId: string
|
|
40
|
-
readonly payload:
|
|
41
|
-
readonly discard
|
|
42
|
-
readonly
|
|
71
|
+
readonly payload: Payload["Type"]
|
|
72
|
+
readonly discard?: Discard | undefined
|
|
73
|
+
readonly suspendedRetrySchedule?:
|
|
74
|
+
| Schedule.Schedule<any, unknown>
|
|
75
|
+
| undefined
|
|
43
76
|
}
|
|
44
|
-
) => Effect.Effect<
|
|
77
|
+
) => Effect.Effect<
|
|
78
|
+
Discard extends true ? string : Success["Type"],
|
|
79
|
+
Error["Type"],
|
|
80
|
+
| Payload["Context"]
|
|
81
|
+
| Success["Context"]
|
|
82
|
+
| Error["Context"]
|
|
83
|
+
>
|
|
45
84
|
|
|
46
85
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* If the workflow has not run yet, it will return `undefined`, otherwise it
|
|
50
|
-
* will return the current `Workflow.Result`.
|
|
86
|
+
* Execute a registered workflow.
|
|
51
87
|
*/
|
|
52
|
-
readonly poll:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
readonly poll: <
|
|
89
|
+
Name extends string,
|
|
90
|
+
Payload extends Workflow.AnyStructSchema,
|
|
91
|
+
Success extends Schema.Schema.Any,
|
|
92
|
+
Error extends Schema.Schema.All
|
|
93
|
+
>(
|
|
94
|
+
workflow: Workflow.Workflow<Name, Payload, Success, Error>,
|
|
95
|
+
executionId: string
|
|
96
|
+
) => Effect.Effect<
|
|
97
|
+
Workflow.Result<Success["Type"], Error["Type"]> | undefined,
|
|
98
|
+
never,
|
|
99
|
+
Success["Context"] | Error["Context"]
|
|
100
|
+
>
|
|
58
101
|
|
|
59
102
|
/**
|
|
60
103
|
* Interrupt a registered workflow.
|
|
@@ -75,46 +118,72 @@ export class WorkflowEngine extends Context.Tag("@effect/workflow/WorkflowEngine
|
|
|
75
118
|
/**
|
|
76
119
|
* Execute an activity from a workflow.
|
|
77
120
|
*/
|
|
78
|
-
readonly activityExecute:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
readonly activityExecute: <
|
|
122
|
+
Success extends Schema.Schema.Any,
|
|
123
|
+
Error extends Schema.Schema.All,
|
|
124
|
+
R
|
|
125
|
+
>(
|
|
126
|
+
activity: Activity.Activity<Success, Error, R>,
|
|
127
|
+
attempt: number
|
|
128
|
+
) => Effect.Effect<
|
|
129
|
+
Workflow.Result<Success["Type"], Error["Type"]>,
|
|
130
|
+
never,
|
|
131
|
+
| Success["Context"]
|
|
132
|
+
| Error["Context"]
|
|
133
|
+
| R
|
|
134
|
+
| WorkflowInstance
|
|
135
|
+
>
|
|
84
136
|
|
|
85
137
|
/**
|
|
86
138
|
* Try to retrieve the result of an DurableDeferred
|
|
87
139
|
*/
|
|
88
|
-
readonly deferredResult:
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
readonly deferredResult: <
|
|
141
|
+
Success extends Schema.Schema.Any,
|
|
142
|
+
Error extends Schema.Schema.All
|
|
143
|
+
>(
|
|
144
|
+
deferred: DurableDeferred.DurableDeferred<Success, Error>
|
|
145
|
+
) => Effect.Effect<
|
|
146
|
+
Exit.Exit<Success["Type"], Error["Type"]> | undefined,
|
|
147
|
+
never,
|
|
148
|
+
WorkflowInstance
|
|
149
|
+
>
|
|
91
150
|
|
|
92
151
|
/**
|
|
93
152
|
* Set the result of a DurableDeferred, and then resume any waiting
|
|
94
153
|
* workflows.
|
|
95
154
|
*/
|
|
96
|
-
readonly deferredDone:
|
|
155
|
+
readonly deferredDone: <
|
|
156
|
+
Success extends Schema.Schema.Any,
|
|
157
|
+
Error extends Schema.Schema.All
|
|
158
|
+
>(
|
|
159
|
+
deferred: DurableDeferred.DurableDeferred<Success, Error>,
|
|
97
160
|
options: {
|
|
98
161
|
readonly workflowName: string
|
|
99
162
|
readonly executionId: string
|
|
100
163
|
readonly deferredName: string
|
|
101
|
-
readonly exit:
|
|
164
|
+
readonly exit: Exit.Exit<Success["Type"], Error["Type"]>
|
|
102
165
|
}
|
|
103
|
-
) => Effect.Effect<
|
|
166
|
+
) => Effect.Effect<
|
|
167
|
+
void,
|
|
168
|
+
never,
|
|
169
|
+
Success["Context"] | Error["Context"]
|
|
170
|
+
>
|
|
104
171
|
|
|
105
172
|
/**
|
|
106
173
|
* Schedule a wake up for a DurableClock
|
|
107
174
|
*/
|
|
108
|
-
readonly scheduleClock: (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
readonly scheduleClock: (
|
|
176
|
+
workflow: Workflow.Any,
|
|
177
|
+
options: {
|
|
178
|
+
readonly executionId: string
|
|
179
|
+
readonly clock: DurableClock
|
|
180
|
+
}
|
|
181
|
+
) => Effect.Effect<void>
|
|
113
182
|
}
|
|
114
183
|
>() {}
|
|
115
184
|
|
|
116
185
|
/**
|
|
117
|
-
* @since
|
|
186
|
+
* @since 4.0.0
|
|
118
187
|
* @category Services
|
|
119
188
|
*/
|
|
120
189
|
export class WorkflowInstance extends Context.Tag("@effect/workflow/WorkflowEngine/WorkflowInstance")<
|
|
@@ -152,7 +221,10 @@ export class WorkflowInstance extends Context.Tag("@effect/workflow/WorkflowEngi
|
|
|
152
221
|
}
|
|
153
222
|
}
|
|
154
223
|
>() {
|
|
155
|
-
static initial(
|
|
224
|
+
static initial(
|
|
225
|
+
workflow: Workflow.Any,
|
|
226
|
+
executionId: string
|
|
227
|
+
): WorkflowInstance["Type"] {
|
|
156
228
|
return WorkflowInstance.of({
|
|
157
229
|
executionId,
|
|
158
230
|
workflow,
|
|
@@ -166,3 +238,396 @@ export class WorkflowInstance extends Context.Tag("@effect/workflow/WorkflowEngi
|
|
|
166
238
|
})
|
|
167
239
|
}
|
|
168
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @since 4.0.0
|
|
244
|
+
* @category Encoded
|
|
245
|
+
*/
|
|
246
|
+
export interface Encoded {
|
|
247
|
+
readonly register: (
|
|
248
|
+
workflow: Workflow.Any,
|
|
249
|
+
execute: (
|
|
250
|
+
payload: object,
|
|
251
|
+
executionId: string
|
|
252
|
+
) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
|
|
253
|
+
) => Effect.Effect<void, never, Scope.Scope>
|
|
254
|
+
readonly execute: <const Discard extends boolean>(
|
|
255
|
+
workflow: Workflow.Any,
|
|
256
|
+
options: {
|
|
257
|
+
readonly executionId: string
|
|
258
|
+
readonly payload: object
|
|
259
|
+
readonly discard: Discard
|
|
260
|
+
readonly parent?: WorkflowInstance["Type"] | undefined
|
|
261
|
+
}
|
|
262
|
+
) => Effect.Effect<
|
|
263
|
+
Discard extends true ? void : Workflow.Result<unknown, unknown>
|
|
264
|
+
>
|
|
265
|
+
readonly poll: (
|
|
266
|
+
workflow: Workflow.Any,
|
|
267
|
+
executionId: string
|
|
268
|
+
) => Effect.Effect<Workflow.Result<unknown, unknown> | undefined>
|
|
269
|
+
readonly interrupt: (
|
|
270
|
+
workflow: Workflow.Any,
|
|
271
|
+
executionId: string
|
|
272
|
+
) => Effect.Effect<void>
|
|
273
|
+
readonly resume: (
|
|
274
|
+
workflow: Workflow.Any,
|
|
275
|
+
executionId: string
|
|
276
|
+
) => Effect.Effect<void>
|
|
277
|
+
readonly activityExecute: (
|
|
278
|
+
activity: Activity.Any,
|
|
279
|
+
attempt: number
|
|
280
|
+
) => Effect.Effect<
|
|
281
|
+
Workflow.Result<unknown, unknown>,
|
|
282
|
+
never,
|
|
283
|
+
WorkflowInstance
|
|
284
|
+
>
|
|
285
|
+
readonly deferredResult: (
|
|
286
|
+
deferred: DurableDeferred.Any
|
|
287
|
+
) => Effect.Effect<
|
|
288
|
+
Exit.Exit<unknown, unknown> | undefined,
|
|
289
|
+
never,
|
|
290
|
+
WorkflowInstance
|
|
291
|
+
>
|
|
292
|
+
readonly deferredDone: (options: {
|
|
293
|
+
readonly workflowName: string
|
|
294
|
+
readonly executionId: string
|
|
295
|
+
readonly deferredName: string
|
|
296
|
+
readonly exit: Exit.Exit<unknown, unknown>
|
|
297
|
+
}) => Effect.Effect<void>
|
|
298
|
+
readonly scheduleClock: (
|
|
299
|
+
workflow: Workflow.Any,
|
|
300
|
+
options: {
|
|
301
|
+
readonly executionId: string
|
|
302
|
+
readonly clock: DurableClock
|
|
303
|
+
}
|
|
304
|
+
) => Effect.Effect<void>
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @since 4.0.0
|
|
309
|
+
* @category Constructors
|
|
310
|
+
*/
|
|
311
|
+
export const makeUnsafe = (options: Encoded): WorkflowEngine["Type"] =>
|
|
312
|
+
WorkflowEngine.of({
|
|
313
|
+
register: Effect.fnUntraced(function*(workflow, execute) {
|
|
314
|
+
const context = yield* Effect.context<WorkflowEngine>()
|
|
315
|
+
yield* options.register(workflow, (payload, executionId) =>
|
|
316
|
+
Effect.suspend(() =>
|
|
317
|
+
execute(payload, executionId)
|
|
318
|
+
).pipe(
|
|
319
|
+
Effect.mapInputContext(
|
|
320
|
+
(input) => Context.merge(context, input) as Context.Context<any>
|
|
321
|
+
)
|
|
322
|
+
))
|
|
323
|
+
}),
|
|
324
|
+
execute: Effect.fnUntraced(function*<
|
|
325
|
+
Name extends string,
|
|
326
|
+
Payload extends Workflow.AnyStructSchema,
|
|
327
|
+
Success extends Schema.Schema.Any,
|
|
328
|
+
Error extends Schema.Schema.All,
|
|
329
|
+
const Discard extends boolean = false
|
|
330
|
+
>(
|
|
331
|
+
self: Workflow.Workflow<Name, Payload, Success, Error>,
|
|
332
|
+
opts: {
|
|
333
|
+
readonly executionId: string
|
|
334
|
+
readonly payload: Payload["Type"]
|
|
335
|
+
readonly discard?: Discard | undefined
|
|
336
|
+
readonly suspendedRetrySchedule?:
|
|
337
|
+
| Schedule.Schedule<any, unknown>
|
|
338
|
+
| undefined
|
|
339
|
+
}
|
|
340
|
+
) {
|
|
341
|
+
const payload = opts.payload
|
|
342
|
+
const executionId = opts.executionId
|
|
343
|
+
const suspendedRetrySchedule = opts.suspendedRetrySchedule ?? defaultRetrySchedule
|
|
344
|
+
yield* Effect.annotateCurrentSpan({ executionId })
|
|
345
|
+
let result: Workflow.Result<Success["Type"], Error["Type"]> | undefined
|
|
346
|
+
|
|
347
|
+
// link interruption with parent workflow
|
|
348
|
+
const parentInstance = yield* Effect.serviceOption(WorkflowInstance)
|
|
349
|
+
if (Option.isSome(parentInstance)) {
|
|
350
|
+
const instance = parentInstance.value
|
|
351
|
+
yield* Effect.addFinalizer(() => {
|
|
352
|
+
if (!instance.interrupted || result?._tag === "Complete") {
|
|
353
|
+
return Effect.void
|
|
354
|
+
}
|
|
355
|
+
return options.interrupt(self, executionId)
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (opts.discard) {
|
|
360
|
+
yield* options.execute(self, {
|
|
361
|
+
executionId,
|
|
362
|
+
payload: payload as object,
|
|
363
|
+
discard: true
|
|
364
|
+
})
|
|
365
|
+
return executionId
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const run = options.execute(self, {
|
|
369
|
+
executionId,
|
|
370
|
+
payload: payload as object,
|
|
371
|
+
discard: false,
|
|
372
|
+
parent: Option.getOrUndefined(parentInstance)
|
|
373
|
+
})
|
|
374
|
+
if (Option.isSome(parentInstance)) {
|
|
375
|
+
result = yield* Workflow.wrapActivityResult(
|
|
376
|
+
run,
|
|
377
|
+
(result) => result._tag === "Suspended"
|
|
378
|
+
)
|
|
379
|
+
if (result._tag === "Suspended") {
|
|
380
|
+
return yield* Workflow.suspend(parentInstance.value)
|
|
381
|
+
}
|
|
382
|
+
return yield* result.exit
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let sleep: Effect.Effect<any> | undefined
|
|
386
|
+
while (true) {
|
|
387
|
+
result = yield* run
|
|
388
|
+
if (result._tag === "Complete") {
|
|
389
|
+
return yield* result.exit as Exit.Exit<any>
|
|
390
|
+
}
|
|
391
|
+
sleep ??= (yield* Schedule.driver(suspendedRetrySchedule)).next(void 0).pipe(
|
|
392
|
+
Effect.catchAll(() => Effect.dieMessage(`${self.name}.execute: suspendedRetrySchedule exhausted`))
|
|
393
|
+
)
|
|
394
|
+
yield* sleep
|
|
395
|
+
}
|
|
396
|
+
}),
|
|
397
|
+
poll: options.poll,
|
|
398
|
+
interrupt: options.interrupt,
|
|
399
|
+
resume: options.resume,
|
|
400
|
+
activityExecute: Effect.fnUntraced(function*<
|
|
401
|
+
Success extends Schema.Schema.Any,
|
|
402
|
+
Error extends Schema.Schema.All,
|
|
403
|
+
R
|
|
404
|
+
>(activity: Activity.Activity<Success, Error, R>, attempt: number) {
|
|
405
|
+
const result = yield* options.activityExecute(activity, attempt)
|
|
406
|
+
if (result._tag === "Suspended") {
|
|
407
|
+
return result
|
|
408
|
+
}
|
|
409
|
+
const exit = yield* Effect.orDie(
|
|
410
|
+
Schema.decode(activity.exitSchema)(result.exit)
|
|
411
|
+
)
|
|
412
|
+
return new Workflow.Complete({ exit })
|
|
413
|
+
}),
|
|
414
|
+
deferredResult: Effect.fnUntraced(
|
|
415
|
+
function*<Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(
|
|
416
|
+
deferred: DurableDeferred.DurableDeferred<Success, Error>
|
|
417
|
+
) {
|
|
418
|
+
const instance = yield* WorkflowInstance
|
|
419
|
+
yield* Effect.annotateCurrentSpan({
|
|
420
|
+
executionId: instance.executionId
|
|
421
|
+
})
|
|
422
|
+
const exit = yield* options.deferredResult(deferred)
|
|
423
|
+
if (exit === undefined) {
|
|
424
|
+
return exit
|
|
425
|
+
}
|
|
426
|
+
return yield* Effect.orDie(
|
|
427
|
+
Schema.decodeUnknown(deferred.exitSchema)(exit)
|
|
428
|
+
) as Effect.Effect<Exit.Exit<Success["Type"], Error["Type"]>>
|
|
429
|
+
}
|
|
430
|
+
),
|
|
431
|
+
deferredDone: Effect.fnUntraced(
|
|
432
|
+
function*<Success extends Schema.Schema.Any, Error extends Schema.Schema.All>(
|
|
433
|
+
deferred: DurableDeferred.DurableDeferred<Success, Error>,
|
|
434
|
+
opts: {
|
|
435
|
+
readonly workflowName: string
|
|
436
|
+
readonly executionId: string
|
|
437
|
+
readonly deferredName: string
|
|
438
|
+
readonly exit: Exit.Exit<Success["Type"], Error["Type"]>
|
|
439
|
+
}
|
|
440
|
+
) {
|
|
441
|
+
return yield* options.deferredDone({
|
|
442
|
+
workflowName: opts.workflowName,
|
|
443
|
+
executionId: opts.executionId,
|
|
444
|
+
deferredName: opts.deferredName,
|
|
445
|
+
exit: yield* Schema.encode(deferred.exitSchema)(
|
|
446
|
+
opts.exit
|
|
447
|
+
) as Effect.Effect<Exit.Exit<unknown, unknown>>
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
),
|
|
451
|
+
scheduleClock: options.scheduleClock
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const defaultRetrySchedule = Schedule.exponential(200, 1.5).pipe(
|
|
455
|
+
Schedule.either(Schedule.spaced(30000))
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @since 1.0.0
|
|
460
|
+
* @category In-memory
|
|
461
|
+
*/
|
|
462
|
+
export const layerMemory: Layer.Layer<WorkflowEngine> = Layer.scoped(
|
|
463
|
+
WorkflowEngine,
|
|
464
|
+
Effect.gen(function*() {
|
|
465
|
+
const scope = yield* Effect.scope
|
|
466
|
+
|
|
467
|
+
const workflows = new Map<string, {
|
|
468
|
+
readonly workflow: Workflow.Any
|
|
469
|
+
readonly execute: (
|
|
470
|
+
payload: object,
|
|
471
|
+
executionId: string
|
|
472
|
+
) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
|
|
473
|
+
readonly scope: Scope.Scope
|
|
474
|
+
}>()
|
|
475
|
+
|
|
476
|
+
type ExecutionState = {
|
|
477
|
+
readonly payload: object
|
|
478
|
+
readonly execute: (
|
|
479
|
+
payload: object,
|
|
480
|
+
executionId: string
|
|
481
|
+
) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
|
|
482
|
+
readonly parent: string | undefined
|
|
483
|
+
instance: WorkflowInstance["Type"]
|
|
484
|
+
fiber: Fiber.RuntimeFiber<Workflow.Result<unknown, unknown>> | undefined
|
|
485
|
+
resumeLatch?: Effect.Latch
|
|
486
|
+
}
|
|
487
|
+
const executions = new Map<string, ExecutionState>()
|
|
488
|
+
|
|
489
|
+
type ActivityState = {
|
|
490
|
+
exit: Exit.Exit<Workflow.Result<unknown, unknown>> | undefined
|
|
491
|
+
}
|
|
492
|
+
const activities = new Map<string, ActivityState>()
|
|
493
|
+
|
|
494
|
+
const resume = Effect.fnUntraced(function*(executionId: string): Effect.fn.Return<void> {
|
|
495
|
+
const state = executions.get(executionId)
|
|
496
|
+
if (!state) return
|
|
497
|
+
const exit = state.fiber?.unsafePoll()
|
|
498
|
+
if (exit && exit._tag === "Success" && exit.value._tag === "Complete") {
|
|
499
|
+
return
|
|
500
|
+
} else if (state.fiber && !exit) {
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const entry = workflows.get(state.instance.workflow.name)!
|
|
505
|
+
const instance = WorkflowInstance.initial(state.instance.workflow, state.instance.executionId)
|
|
506
|
+
instance.interrupted = state.instance.interrupted
|
|
507
|
+
state.instance = instance
|
|
508
|
+
state.fiber = yield* state.execute(state.payload, state.instance.executionId).pipe(
|
|
509
|
+
Effect.onExit(() => {
|
|
510
|
+
if (!instance.interrupted) {
|
|
511
|
+
return Effect.void
|
|
512
|
+
}
|
|
513
|
+
instance.suspended = false
|
|
514
|
+
return Effect.withFiberRuntime<void>((fiber) => Effect.interruptible(Fiber.interrupt(fiber)))
|
|
515
|
+
}),
|
|
516
|
+
Workflow.intoResult,
|
|
517
|
+
Effect.provideService(WorkflowInstance, instance),
|
|
518
|
+
Effect.provideService(WorkflowEngine, engine),
|
|
519
|
+
Effect.tap((result) => {
|
|
520
|
+
if (!state.parent || result._tag !== "Complete") {
|
|
521
|
+
return Effect.void
|
|
522
|
+
}
|
|
523
|
+
return Effect.forkIn(resume(state.parent), scope)
|
|
524
|
+
}),
|
|
525
|
+
Effect.forkIn(entry.scope)
|
|
526
|
+
)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
const deferredResults = new Map<string, Exit.Exit<any, any>>()
|
|
530
|
+
|
|
531
|
+
const clocks = yield* FiberMap.make()
|
|
532
|
+
|
|
533
|
+
const engine = makeUnsafe({
|
|
534
|
+
register: Effect.fnUntraced(function*(workflow, execute) {
|
|
535
|
+
workflows.set(workflow.name, {
|
|
536
|
+
workflow,
|
|
537
|
+
execute,
|
|
538
|
+
scope: yield* Effect.scope
|
|
539
|
+
})
|
|
540
|
+
}),
|
|
541
|
+
execute: Effect.fnUntraced(function*(workflow, options) {
|
|
542
|
+
const entry = workflows.get(workflow.name)
|
|
543
|
+
if (!entry) {
|
|
544
|
+
return yield* Effect.die(`Workflow ${workflow.name} is not registered`)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let state = executions.get(options.executionId)
|
|
548
|
+
if (!state) {
|
|
549
|
+
state = {
|
|
550
|
+
payload: options.payload,
|
|
551
|
+
execute: entry.execute,
|
|
552
|
+
instance: WorkflowInstance.initial(workflow, options.executionId),
|
|
553
|
+
fiber: undefined,
|
|
554
|
+
parent: options.parent?.executionId
|
|
555
|
+
}
|
|
556
|
+
executions.set(options.executionId, state)
|
|
557
|
+
yield* resume(options.executionId)
|
|
558
|
+
}
|
|
559
|
+
if (options.discard) return
|
|
560
|
+
return (yield* Fiber.join(state.fiber!)) as any
|
|
561
|
+
}),
|
|
562
|
+
interrupt: Effect.fnUntraced(function*(_workflow, executionId) {
|
|
563
|
+
const state = executions.get(executionId)
|
|
564
|
+
if (!state) return
|
|
565
|
+
state.instance.interrupted = true
|
|
566
|
+
yield* resume(executionId)
|
|
567
|
+
}),
|
|
568
|
+
resume(_workflow, executionId) {
|
|
569
|
+
return resume(executionId)
|
|
570
|
+
},
|
|
571
|
+
activityExecute: Effect.fnUntraced(function*(activity, attempt) {
|
|
572
|
+
const instance = yield* WorkflowInstance
|
|
573
|
+
const activityId = `${instance.executionId}/${activity.name}/${attempt}`
|
|
574
|
+
let state = activities.get(activityId)
|
|
575
|
+
if (state) {
|
|
576
|
+
const exit = state.exit
|
|
577
|
+
if (exit && exit._tag === "Success" && exit.value._tag === "Suspended") {
|
|
578
|
+
state.exit = undefined
|
|
579
|
+
} else if (exit) {
|
|
580
|
+
return yield* exit
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
state = { exit: undefined }
|
|
584
|
+
activities.set(activityId, state)
|
|
585
|
+
}
|
|
586
|
+
const activityInstance = WorkflowInstance.initial(instance.workflow, instance.executionId)
|
|
587
|
+
activityInstance.interrupted = instance.interrupted
|
|
588
|
+
return yield* activity.executeEncoded.pipe(
|
|
589
|
+
Workflow.intoResult,
|
|
590
|
+
Effect.provideService(WorkflowInstance, activityInstance),
|
|
591
|
+
Effect.onExit((exit) => {
|
|
592
|
+
state.exit = exit
|
|
593
|
+
return Effect.void
|
|
594
|
+
})
|
|
595
|
+
)
|
|
596
|
+
}),
|
|
597
|
+
poll: (_workflow, executionId) =>
|
|
598
|
+
Effect.suspend(() => {
|
|
599
|
+
const state = executions.get(executionId)
|
|
600
|
+
if (!state) {
|
|
601
|
+
return Effect.succeed(undefined)
|
|
602
|
+
}
|
|
603
|
+
const exit = state.fiber?.unsafePoll()
|
|
604
|
+
return exit ?? Effect.succeed(undefined)
|
|
605
|
+
}),
|
|
606
|
+
deferredResult: Effect.fnUntraced(function*(deferred) {
|
|
607
|
+
const instance = yield* WorkflowInstance
|
|
608
|
+
const id = `${instance.executionId}/${deferred.name}`
|
|
609
|
+
return deferredResults.get(id)
|
|
610
|
+
}),
|
|
611
|
+
deferredDone: (options) =>
|
|
612
|
+
Effect.suspend(() => {
|
|
613
|
+
const id = `${options.executionId}/${options.deferredName}`
|
|
614
|
+
if (deferredResults.has(id)) return Effect.void
|
|
615
|
+
deferredResults.set(id, options.exit)
|
|
616
|
+
return resume(options.executionId)
|
|
617
|
+
}),
|
|
618
|
+
scheduleClock: (workflow, options) =>
|
|
619
|
+
engine.deferredDone(options.clock.deferred, {
|
|
620
|
+
workflowName: workflow.name,
|
|
621
|
+
executionId: options.executionId,
|
|
622
|
+
deferredName: options.clock.deferred.name,
|
|
623
|
+
exit: Exit.void
|
|
624
|
+
}).pipe(
|
|
625
|
+
Effect.delay(options.clock.duration),
|
|
626
|
+
FiberMap.run(clocks, `${options.executionId}/${options.clock.name}`, { onlyIfMissing: true }),
|
|
627
|
+
Effect.asVoid
|
|
628
|
+
)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
return engine
|
|
632
|
+
})
|
|
633
|
+
)
|
package/src/index.ts
CHANGED