@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.
- package/Activity/package.json +6 -0
- package/DurableClock/package.json +6 -0
- package/DurableDeferred/package.json +6 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/Workflow/package.json +6 -0
- package/WorkflowEngine/package.json +6 -0
- package/dist/cjs/Activity.js +100 -0
- package/dist/cjs/Activity.js.map +1 -0
- package/dist/cjs/DurableClock.js +49 -0
- package/dist/cjs/DurableClock.js.map +1 -0
- package/dist/cjs/DurableDeferred.js +158 -0
- package/dist/cjs/DurableDeferred.js.map +1 -0
- package/dist/cjs/Workflow.js +198 -0
- package/dist/cjs/Workflow.js.map +1 -0
- package/dist/cjs/WorkflowEngine.js +25 -0
- package/dist/cjs/WorkflowEngine.js.map +1 -0
- package/dist/cjs/index.js +18 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/crypto.js +19 -0
- package/dist/cjs/internal/crypto.js.map +1 -0
- package/dist/dts/Activity.d.ts +70 -0
- package/dist/dts/Activity.d.ts.map +1 -0
- package/dist/dts/DurableClock.d.ts +42 -0
- package/dist/dts/DurableClock.d.ts.map +1 -0
- package/dist/dts/DurableDeferred.d.ts +252 -0
- package/dist/dts/DurableDeferred.d.ts.map +1 -0
- package/dist/dts/Workflow.d.ts +211 -0
- package/dist/dts/Workflow.d.ts.map +1 -0
- package/dist/dts/WorkflowEngine.d.ts +91 -0
- package/dist/dts/WorkflowEngine.d.ts.map +1 -0
- package/dist/dts/index.d.ts +21 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/crypto.d.ts +2 -0
- package/dist/dts/internal/crypto.d.ts.map +1 -0
- package/dist/esm/Activity.js +90 -0
- package/dist/esm/Activity.js.map +1 -0
- package/dist/esm/DurableClock.js +40 -0
- package/dist/esm/DurableClock.js.map +1 -0
- package/dist/esm/DurableDeferred.js +155 -0
- package/dist/esm/DurableDeferred.js.map +1 -0
- package/dist/esm/Workflow.js +183 -0
- package/dist/esm/Workflow.js.map +1 -0
- package/dist/esm/WorkflowEngine.js +15 -0
- package/dist/esm/WorkflowEngine.js.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/crypto.js +11 -0
- package/dist/esm/internal/crypto.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/package.json +74 -0
- package/src/Activity.ts +177 -0
- package/src/DurableClock.ts +82 -0
- package/src/DurableDeferred.ts +461 -0
- package/src/Workflow.ts +401 -0
- package/src/WorkflowEngine.ts +122 -0
- package/src/index.ts +24 -0
- package/src/internal/crypto.ts +15 -0
package/src/Workflow.ts
ADDED
|
@@ -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
|
+
)
|