| /** | |
| * Defines typed durable workflows. | |
| * | |
| * A `Workflow` has a stable tag, schemas for payload, success, and failure, and | |
| * an idempotency key used to derive execution ids. Workflow definitions can be | |
| * executed, discarded, polled, interrupted, resumed, and registered with a | |
| * handler layer. This module also includes workflow result types, compensation | |
| * and cleanup helpers, suspension support, and settings for defect capture or | |
| * failure suspension. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.js"; | |
| import * as Cause from "../../Cause.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Data from "../../Data.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import * as Fiber from "../../Fiber.js"; | |
| import * as Filter from "../../Filter.js"; | |
| import { constFalse, constTrue, dual, identity } from "../../Function.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as SchemaIssue from "../../SchemaIssue.js"; | |
| import * as SchemaParser from "../../SchemaParser.js"; | |
| import * as Tranformation from "../../SchemaTransformation.js"; | |
| import * as Scope from "../../Scope.js"; | |
| import { makeHashDigest } from "./internal/crypto.js"; | |
| const TypeId = "~effect/workflow/Workflow"; | |
| const EngineTag = /*#__PURE__*/Context.Service("effect/workflow/WorkflowEngine"); | |
| const InstanceTag = /*#__PURE__*/Context.Service("effect/workflow/WorkflowEngine/WorkflowInstance"); | |
| const makeExecutionIdFromPayload = (self, payload) => makeHashDigest(`${self._tag}-${self.idempotencyKey(payload)}`); | |
| const Proto = { | |
| [TypeId]: TypeId, | |
| annotate(tag, value) { | |
| return makeProto({ | |
| _tag: this._tag, | |
| payloadSchema: this.payloadSchema, | |
| successSchema: this.successSchema, | |
| errorSchema: this.errorSchema, | |
| annotations: Context.add(this.annotations, tag, value), | |
| idempotencyKey: this.idempotencyKey, | |
| suspendedRetrySchedule: this.suspendedRetrySchedule | |
| }); | |
| }, | |
| annotateMerge(context) { | |
| return makeProto({ | |
| _tag: this._tag, | |
| payloadSchema: this.payloadSchema, | |
| successSchema: this.successSchema, | |
| errorSchema: this.errorSchema, | |
| annotations: Context.merge(this.annotations, context), | |
| idempotencyKey: this.idempotencyKey, | |
| suspendedRetrySchedule: this.suspendedRetrySchedule | |
| }); | |
| }, | |
| execute(fields, opts) { | |
| return Effect.suspend(() => { | |
| const payload = this.payloadSchema.make(fields); | |
| return Effect.flatMap(EngineTag, engine => Effect.flatMap(makeExecutionIdFromPayload(this, payload), executionId => Effect.andThen(Effect.annotateCurrentSpan({ | |
| executionId | |
| }), engine.execute(this, { | |
| executionId, | |
| payload, | |
| discard: opts?.discard, | |
| suspendedRetrySchedule: this.suspendedRetrySchedule | |
| })))); | |
| }).pipe(Effect.withSpan(`${this._tag}.execute`, {}, { | |
| captureStackTrace: false | |
| })); | |
| }, | |
| poll(executionId) { | |
| return Effect.flatMap(EngineTag, engine => engine.poll(this, executionId)).pipe(Effect.withSpan(`${this._tag}.poll`, { | |
| attributes: { | |
| executionId | |
| } | |
| }, { | |
| captureStackTrace: false | |
| })); | |
| }, | |
| interrupt(executionId) { | |
| return Effect.flatMap(EngineTag, engine => engine.interrupt(this, executionId)).pipe(Effect.withSpan(`${this._tag}.interrupt`, { | |
| attributes: { | |
| executionId | |
| } | |
| }, { | |
| captureStackTrace: false | |
| })); | |
| }, | |
| resume(executionId) { | |
| return Effect.flatMap(EngineTag, engine => engine.resume(this, executionId)).pipe(Effect.withSpan(`${this._tag}.resume`, { | |
| attributes: { | |
| executionId | |
| } | |
| }, { | |
| captureStackTrace: false | |
| })); | |
| }, | |
| toLayer(execute) { | |
| return Layer.effectDiscard(Effect.flatMap(EngineTag, engine => engine.register(this, execute))); | |
| }, | |
| executionId(payload) { | |
| return Effect.flatMap(Effect.orDie(this.payloadSchema.makeEffect(payload)), payload => makeExecutionIdFromPayload(this, payload)); | |
| }, | |
| withCompensation: (...args) => withCompensation(...args) | |
| }; | |
| const makeProto = options => { | |
| function Workflow() {} | |
| Object.setPrototypeOf(Workflow, Proto); | |
| Object.assign(Workflow, options); | |
| return Workflow; | |
| }; | |
| /** | |
| * Creates a durable workflow definition with schemas, annotations, and | |
| * deterministic execution IDs derived from the workflow tag and idempotency | |
| * key. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const make = (tag, options) => makeProto({ | |
| _tag: tag, | |
| payloadSchema: Schema.isSchema(options.payload) ? options.payload : Schema.Struct(options.payload), | |
| successSchema: options.success ?? Schema.Void, | |
| errorSchema: options.error ?? Schema.Never, | |
| annotations: options.annotations ?? Context.empty(), | |
| idempotencyKey: options.idempotencyKey, | |
| suspendedRetrySchedule: options.suspendedRetrySchedule | |
| }); | |
| const ResultTypeId = "~effect/workflow/Workflow/Result"; | |
| /** | |
| * Returns `true` when a value is a workflow `Result`. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const isResult = u => Predicate.hasProperty(u, ResultTypeId); | |
| /** | |
| * Represents a completed workflow execution with its success or failure `Exit`. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export class Complete extends /*#__PURE__*/Data.TaggedClass("Complete") { | |
| /** | |
| * Marks this value as a workflow result for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [ResultTypeId] = ResultTypeId; | |
| /** | |
| * Builds the schema for completed workflow results from success and error schemas. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static Schema(options) { | |
| // TODO: extract to a helper function | |
| const schema = Schema.declareConstructor()([Schema.Exit(options.success, options.error, Schema.Defect())], ([exit]) => (input, ast, options) => { | |
| if (!(isResult(input) && input._tag === "Complete")) { | |
| return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))); | |
| } | |
| return Effect.mapBothEager(SchemaParser.decodeEffect(exit)(input.exit, options), { | |
| onSuccess: exit => new Complete({ | |
| exit | |
| }), | |
| onFailure: issue => new SchemaIssue.Composite(ast, Option.some(input), [new SchemaIssue.Pointer(["exit"], issue)]) | |
| }); | |
| }, { | |
| expected: "Workflow.Complete", | |
| toCodecJson: ([exit]) => Schema.link()(Schema.Struct({ | |
| _tag: Schema.tag("Complete"), | |
| exit | |
| }), Tranformation.transform({ | |
| decode: encoded => new Complete({ | |
| exit: encoded.exit | |
| }), | |
| encode: result => ({ | |
| _tag: "Complete", | |
| exit: result.exit | |
| }) | |
| })) | |
| }); | |
| return Schema.make(schema.ast, { | |
| success: options.success, | |
| error: options.error | |
| }); | |
| } | |
| } | |
| /** | |
| * Represents a suspended workflow execution, optionally carrying the cause that | |
| * triggered suspension. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export class Suspended extends /*#__PURE__*/Schema.Class("effect/workflow/Workflow/Suspended")({ | |
| _tag: /*#__PURE__*/Schema.tag("Suspended"), | |
| cause: /*#__PURE__*/Schema.optional(/*#__PURE__*/Schema.Cause(Schema.Never, /*#__PURE__*/Schema.Defect())) | |
| }) { | |
| /** | |
| * Marks this value as a workflow result for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [ResultTypeId] = ResultTypeId; | |
| } | |
| /** | |
| * Creates a schema for workflow results using the supplied success and error | |
| * schemas. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const Result = options => Schema.Union([Complete.Schema(options), Suspended]); | |
| const AnyOrVoid = /*#__PURE__*/Schema.Union([Schema.Any, Schema.Void]); | |
| /** | |
| * Schema for encoded workflow results with generic success and error payloads. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const ResultEncoded = /*#__PURE__*/Schema.toEncoded(/*#__PURE__*/Schema.toCodecJson(/*#__PURE__*/Result({ | |
| success: AnyOrVoid, | |
| error: AnyOrVoid | |
| }))); | |
| /** | |
| * Runs an effect as a workflow execution and converts its outcome into a | |
| * `Result`, handling suspension, defect capture, interruption, and workflow | |
| * scope finalization. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const intoResult = effect => Effect.contextWith(context => { | |
| const instance = Context.get(context, InstanceTag); | |
| const captureDefects = Context.get(instance.workflow.annotations, CaptureDefects); | |
| const suspendOnFailure = Context.get(instance.workflow.annotations, SuspendOnFailure); | |
| return effect.pipe( | |
| // so we can use external interruption to suspend the workflow | |
| Effect.forkChild({ | |
| startImmediately: true | |
| }), Effect.flatMap(fiber => Effect.onInterrupt(Fiber.join(fiber), () => Fiber.interrupt(fiber))), Effect.interruptible, suspendOnFailure ? Effect.catchCause(cause => { | |
| instance.suspended = true; | |
| if (!Cause.hasInterruptsOnly(cause)) { | |
| instance.cause = Cause.die(Cause.squash(cause)); | |
| } | |
| return Effect.interrupt; | |
| }) : identity, Effect.scoped, Effect.matchCauseEffect({ | |
| onSuccess: value => Effect.succeed(new Complete({ | |
| exit: Exit.succeed(value) | |
| })), | |
| onFailure: cause => { | |
| const [reasons, interrupts] = Arr.partition(cause.reasons, Filter.fromPredicate(Cause.isInterruptReason)); | |
| const hasInterruptsOnly = interrupts.length === cause.reasons.length; | |
| const filtered = reasons.length === 0 ? cause : Cause.fromReasons(reasons); | |
| return instance.suspended && hasInterruptsOnly ? Effect.succeed(new Suspended({ | |
| cause: instance.cause | |
| })) : !instance.interrupted && hasInterruptsOnly || !captureDefects && Cause.hasDies(cause) ? Effect.failCause(filtered) : Effect.succeed(new Complete({ | |
| exit: Exit.failCause(filtered) | |
| })); | |
| } | |
| }), eff => Effect.onExitPrimitive(eff, exit => { | |
| if (Exit.isFailure(exit)) { | |
| return Scope.close(instance.scope, exit); | |
| } else if (exit.value._tag === "Complete") { | |
| return Scope.close(instance.scope, exit.value.exit); | |
| } | |
| return Effect.void; | |
| }, true), Effect.uninterruptible); | |
| }); | |
| /** | |
| * Wraps an activity-like effect so workflow suspension waits for currently | |
| * running activities to finish or suspend. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const wrapActivityResult = (effect, isSuspend) => Effect.contextWith(context => { | |
| const instance = Context.get(context, InstanceTag); | |
| const state = instance.activityState; | |
| if (state.count === 0) state.latch.closeUnsafe(); | |
| state.count++; | |
| return Effect.onExit(effect, exit => { | |
| state.count--; | |
| const isSuspended = Exit.isSuccess(exit) && isSuspend(exit.value); | |
| if (Exit.isSuccess(exit) && isResult(exit.value) && exit.value._tag === "Suspended" && exit.value.cause) { | |
| instance.cause = instance.cause ? Cause.combine(instance.cause, exit.value.cause) : exit.value.cause; | |
| } | |
| return state.count === 0 ? state.latch.open : isSuspended ? waitForZero(instance) : Effect.void; | |
| }); | |
| }); | |
| const waitForZero = /*#__PURE__*/Effect.fnUntraced(function* (instance) { | |
| const state = instance.activityState; | |
| while (true) { | |
| if (state.count > 0) { | |
| yield* state.latch.await; | |
| yield* Effect.yieldNow; | |
| continue; | |
| } | |
| yield* Effect.yieldNow; | |
| if (state.count === 0) return; | |
| } | |
| }); | |
| /** | |
| * Accesses the workflow scope, which is only closed when the workflow execution fully completes. | |
| * | |
| * @category resource management | |
| * @since 4.0.0 | |
| */ | |
| export const scope = /*#__PURE__*/Effect.map(InstanceTag, instance => instance.scope); | |
| /** | |
| * Provides the workflow scope to the given effect, and closes the scope only when the workflow execution fully completes. | |
| * | |
| * @category resource management | |
| * @since 4.0.0 | |
| */ | |
| export const provideScope = effect => Effect.flatMap(scope, scope => Scope.provide(effect, scope)); | |
| /** | |
| * Adds an exit finalizer to the current workflow scope, preserving the | |
| * services available when the finalizer is registered. | |
| * | |
| * @category resource management | |
| * @since 4.0.0 | |
| */ | |
| export const addFinalizer = /*#__PURE__*/Effect.fnUntraced(function* (f) { | |
| const scope = (yield* InstanceTag).scope; | |
| const services = yield* Effect.context(); | |
| yield* Scope.addFinalizerExit(scope, exit => Effect.provideContext(f(exit), services)); | |
| }); | |
| /** | |
| * Adds compensation logic to an effect inside a Workflow. | |
| * | |
| * **When to use** | |
| * | |
| * Use when a top-level workflow step needs compensating cleanup if the overall | |
| * workflow later fails after the step succeeds. | |
| * | |
| * **Details** | |
| * | |
| * The compensation finalizer is called if the entire workflow fails, allowing you to perform cleanup or other actions based on the success value and the cause of the workflow failure. | |
| * | |
| * **Gotchas** | |
| * | |
| * Compensation finalizers are only registered for top-level effects in the workflow and do not work for nested activities. | |
| * | |
| * @category Compensation | |
| * @since 4.0.0 | |
| */ | |
| export const withCompensation = /*#__PURE__*/dual(2, (effect, compensation) => Effect.uninterruptibleMask(restore => Effect.tap(restore(effect), value => addFinalizer(exit => Exit.isSuccess(exit) ? Effect.void : compensation(value, exit.cause))))); | |
| /** | |
| * Marks a workflow instance as suspended and interrupts the current fiber to | |
| * stop execution until it is resumed. | |
| * | |
| * @category results | |
| * @since 4.0.0 | |
| */ | |
| export const suspend = instance => Effect.interruptible(Effect.callback(() => { | |
| instance.suspended = true; | |
| const fiber = Fiber.getCurrent(); | |
| fiber.interruptUnsafe(fiber.id); | |
| })); | |
| /** | |
| * Captures defects for a workflow and includes them in the result of the workflow or its activities. | |
| * | |
| * **Details** | |
| * | |
| * By default, this annotation is set to `true`, meaning defects are captured. | |
| * | |
| * @category annotations | |
| * @since 4.0.0 | |
| */ | |
| export const CaptureDefects = /*#__PURE__*/Context.Reference("effect/workflow/Workflow/CaptureDefects", { | |
| defaultValue: constTrue | |
| }); | |
| /** | |
| * Marks a workflow to suspend when it encounters any error. | |
| * | |
| * **Details** | |
| * | |
| * The suspended execution can later be resumed with the workflow's `resume` method, for example `MyWorkflow.resume(executionId)`. | |
| * | |
| * @category annotations | |
| * @since 4.0.0 | |
| */ | |
| export const SuspendOnFailure = /*#__PURE__*/Context.Reference("effect/workflow/Workflow/SuspendOnFailure", { | |
| defaultValue: constFalse | |
| }); | |
| //# sourceMappingURL=Workflow.js.map |
Xet Storage Details
- Size:
- 14.2 kB
- Xet hash:
- 1d7c0d497fb20700f327d7e731200537b3b110f937055c6c240ffadca4a19ebb
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.