/** * @since 1.0.0 */ import type { NonEmptyReadonlyArray } from "effect/Array" import type * as Brand from "effect/Brand" import * as Cause from "effect/Cause" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Encoding from "effect/Encoding" import * as Exit from "effect/Exit" import { dual } from "effect/Function" import * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import type * as Activity from "./Activity.js" import * as Workflow from "./Workflow.js" import type { WorkflowEngine, WorkflowInstance } from "./WorkflowEngine.js" /** * @since 1.0.0 * @category Symbols */ export const TypeId: unique symbol = Symbol.for("@effect/workflow/DurableDeferred") /** * @since 1.0.0 * @category Symbols */ export type TypeId = typeof TypeId /** * @since 1.0.0 * @category Models */ export interface DurableDeferred< Success extends Schema.Schema.Any, Error extends Schema.Schema.All = typeof Schema.Never > { readonly [TypeId]: TypeId readonly name: string readonly successSchema: Success readonly errorSchema: Error readonly exitSchema: Schema.ExitFromSelf readonly withActivityAttempt: Effect.Effect> } /** * @since 1.0.0 * @category Models */ export interface Any { readonly [TypeId]: TypeId readonly name: string readonly successSchema: Schema.Schema.Any readonly errorSchema: Schema.Schema.All readonly exitSchema: Schema.ExitFromSelf } /** * @since 1.0.0 * @category Constructors */ export const make = < Success extends Schema.Schema.Any = typeof Schema.Void, Error extends Schema.Schema.All = typeof Schema.Never >(name: string, options?: { readonly success?: Success | undefined readonly error?: Error | undefined }): DurableDeferred => ({ [TypeId]: TypeId, name, successSchema: options?.success ?? Schema.Void as any, errorSchema: options?.error ?? Schema.Never as any, exitSchema: Schema.ExitFromSelf({ success: options?.success ?? Schema.Void as any, failure: options?.error ?? Schema.Never as any, defect: Schema.Defect }), withActivityAttempt: Effect.gen(function*() { const attempt = yield* CurrentAttempt return make(`${name}/${attempt}`, { success: options?.success, error: options?.error }) }) }) const EngineTag = Context.GenericTag( "@effect/workflow/WorkflowEngine" satisfies typeof WorkflowEngine.key ) const InstanceTag = Context.GenericTag( "@effect/workflow/WorkflowEngine/WorkflowInstance" satisfies typeof WorkflowInstance.key ) const CurrentAttempt = Context.Reference()( "@effect/workflow/Activity/CurrentAttempt" satisfies typeof Activity.CurrentAttempt.key, { defaultValue: () => 1 } ) const await_: ( self: DurableDeferred ) => Effect.Effect< Success["Type"], Error["Type"], WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"] > = Effect.fnUntraced(function*< Success extends Schema.Schema.Any, Error extends Schema.Schema.All >(self: DurableDeferred) { const engine = yield* EngineTag const instance = yield* InstanceTag const exit = yield* Workflow.wrapActivityResult( engine.deferredResult(self), Predicate.isUndefined ) if (exit === undefined) { return yield* Workflow.suspend(instance) } return yield* exit }) export { /** * @since 1.0.0 * @category Combinators */ await_ as await } /** * @since 1.0.0 * @category Combinators */ export const into: { /** * @since 1.0.0 * @category Combinators */ (self: DurableDeferred): (effect: Effect.Effect) => Effect.Effect< Success["Type"], Error["Type"], R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"] > /** * @since 1.0.0 * @category Combinators */ ( effect: Effect.Effect, self: DurableDeferred ): Effect.Effect< Success["Type"], Error["Type"], R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"] > } = dual(2, ( effect: Effect.Effect, self: DurableDeferred ): Effect.Effect< Success["Type"], Error["Type"], R | WorkflowEngine | WorkflowInstance | Success["Context"] | Error["Context"] > => Effect.contextWithEffect((context: Context.Context) => { const engine = Context.get(context, EngineTag) const parentInstance = Context.get(context, InstanceTag) const instance = { ...parentInstance } return Effect.onExit(Effect.provideService(effect, InstanceTag, instance), (exit) => { if (Exit.isFailure(exit) && Cause.isInterrupted(exit.cause)) { const isInterruptedOnly = Cause.isInterruptedOnly(exit.cause) if (isInterruptedOnly && instance.suspended) { parentInstance.suspended = true return Effect.void } else if (!isInterruptedOnly) { exit = Exit.failCause( Cause.filter(exit.cause, (cause) => !Cause.isInterruptType(cause)) ) } } return engine.deferredDone(self, { workflowName: parentInstance.workflow.name, executionId: parentInstance.executionId, deferredName: self.name, exit }) }) })) /** * @since 1.0.0 * @category Racing */ export const raceAll = < const Effects extends NonEmptyReadonlyArray>, SI, SR, EI, ER >(options: { name: string success: Schema.Schema< Effects[number] extends Effect.Effect ? S : never, SI, SR > error: Schema.Schema< Effects[number] extends Effect.Effect ? E : never, EI, ER > effects: Effects }): Effect.Effect< (Effects[number] extends Effect.Effect ? _A : never), (Effects[number] extends Effect.Effect ? _E : never), | (Effects[number] extends Effect.Effect ? R : never) | SR | ER | WorkflowEngine | WorkflowInstance > => { const deferred = make(`raceAll/${options.name}`, { success: options.success, error: options.error }) return Effect.gen(function*() { const engine = yield* EngineTag const exit = yield* Workflow.wrapActivityResult(engine.deferredResult(deferred), Predicate.isUndefined) if (exit) { return yield* exit } return yield* into(Effect.raceAll(options.effects), deferred) }) } /** * @since 1.0.0 * @category Token */ export const TokenTypeId: unique symbol = Symbol.for("@effect/workflow/DurableDeferred/Token") /** * @since 1.0.0 * @category Token */ export type TokenTypeId = typeof TokenTypeId /** * @since 1.0.0 * @category Token */ export type Token = Brand.Branded /** * @since 1.0.0 * @category Token */ export const Token: Schema.brand< typeof Schema.String, typeof TokenTypeId > = Schema.String.pipe( Schema.brand(TokenTypeId) ) /** * @since 1.0.0 * @category Token */ export class TokenParsed extends Schema.Class("@effect/workflow/DurableDeferred/TokenParsed")({ workflowName: Schema.String, executionId: Schema.String, deferredName: Schema.String }) { /** * @since 1.0.0 */ get asToken(): Token { return Encoding.encodeBase64Url(JSON.stringify([this.workflowName, this.executionId, this.deferredName])) as Token } /** * @since 1.0.0 */ static readonly FromString: Schema.Schema< TokenParsed, string > = Schema.StringFromBase64Url.pipe( Schema.compose(Schema.parseJson(Schema.Tuple(Schema.String, Schema.String, Schema.String))), Schema.transform(TokenParsed, { decode: ([workflowName, executionId, deferredName]) => new TokenParsed({ workflowName, executionId, deferredName }), encode: (parsed) => [parsed.workflowName, parsed.executionId, parsed.deferredName] as const }) ) /** * @since 1.0.0 */ static readonly fromString = Schema.decodeSync(TokenParsed.FromString) /** * @since 1.0.0 */ static readonly encode = Schema.encodeSync(TokenParsed.FromString) } /** * @since 1.0.0 * @category Token */ export const token: ( self: DurableDeferred ) => Effect.Effect = Effect.fnUntraced(function*< Success extends Schema.Schema.Any, Error extends Schema.Schema.All >(self: DurableDeferred) { const instance = yield* InstanceTag return tokenFromExecutionId(self, instance) }) /** * @since 1.0.0 * @category Token */ export const tokenFromExecutionId: { /** * @since 1.0.0 * @category Token */ ( options: { readonly workflow: Workflow.Any readonly executionId: string } ): ( self: DurableDeferred ) => Token /** * @since 1.0.0 * @category Token */ ( self: DurableDeferred, options: { readonly workflow: Workflow.Any; readonly executionId: string } ): Token } = dual( 2, ( self: DurableDeferred, options: { readonly workflow: Workflow.Any readonly executionId: string } ): Token => new TokenParsed({ workflowName: options.workflow.name, executionId: options.executionId, deferredName: self.name }).asToken ) /** * @since 1.0.0 * @category Token */ export const tokenFromPayload: { /** * @since 1.0.0 * @category Token */ ( options: { readonly workflow: W readonly payload: Schema.Simplify> } ): ( self: DurableDeferred ) => Effect.Effect /** * @since 1.0.0 * @category Token */ ( self: DurableDeferred, options: { readonly workflow: W readonly payload: Schema.Simplify> } ): Effect.Effect } = dual( 2, ( self: DurableDeferred, options: { readonly workflow: W readonly payload: Schema.Simplify> } ): Effect.Effect => Effect.map(options.workflow.executionId(options.payload), (executionId) => tokenFromExecutionId(self, { workflow: options.workflow, executionId })) ) /** * @since 1.0.0 * @category Combinators */ export const done: { /** * @since 1.0.0 * @category Combinators */ ( options: { readonly token: Token readonly exit: Exit.Exit } ): (self: DurableDeferred) => Effect.Effect< void, never, WorkflowEngine | Success["Context"] | Error["Context"] > /** * @since 1.0.0 * @category Combinators */ ( self: DurableDeferred, options: { readonly token: Token readonly exit: Exit.Exit } ): Effect.Effect } = dual( 2, Effect.fnUntraced(function*( self: DurableDeferred, options: { readonly token: Token readonly exit: Exit.Exit } ) { const engine = yield* EngineTag const token = TokenParsed.fromString(options.token) yield* engine.deferredDone(self, { workflowName: token.workflowName, executionId: token.executionId, deferredName: token.deferredName, exit: options.exit }) }) ) /** * @since 1.0.0 * @category Combinators */ export const succeed: { /** * @since 1.0.0 * @category Combinators */ ( options: { readonly token: Token readonly value: Success["Type"] } ): (self: DurableDeferred) => Effect.Effect /** * @since 1.0.0 * @category Combinators */ ( self: DurableDeferred, options: { readonly token: Token readonly value: Success["Type"] } ): Effect.Effect } = dual( 2, ( self: DurableDeferred, options: { readonly token: Token readonly value: Success["Type"] } ): Effect.Effect => done(self, { token: options.token, exit: Exit.succeed(options.value) }) ) /** * @since 1.0.0 * @category Combinators */ export const fail: { /** * @since 1.0.0 * @category Combinators */ ( options: { readonly token: Token readonly error: Error["Type"] } ): (self: DurableDeferred) => Effect.Effect /** * @since 1.0.0 * @category Combinators */ ( self: DurableDeferred, options: { readonly token: Token readonly error: Error["Type"] } ): Effect.Effect } = dual( 2, ( self: DurableDeferred, options: { readonly token: Token readonly error: Error["Type"] } ): Effect.Effect => done(self, { token: options.token, exit: Exit.fail(options.error) }) ) /** * @since 1.0.0 * @category Combinators */ export const failCause: { /** * @since 1.0.0 * @category Combinators */ ( options: { readonly token: Token readonly cause: Cause.Cause } ): (self: DurableDeferred) => Effect.Effect /** * @since 1.0.0 * @category Combinators */ ( self: DurableDeferred, options: { readonly token: Token readonly cause: Cause.Cause } ): Effect.Effect } = dual( 2, ( self: DurableDeferred, options: { readonly token: Token readonly cause: Cause.Cause } ): Effect.Effect => done(self, { token: options.token, exit: Exit.failCause(options.cause) }) )