import { Message, MessageAttributes } from '@node-ts/bus-messages' import { ClassConstructor } from '../util' import { WorkflowAlreadyHandlesMessage, WorkflowAlreadyStartedByMessage } from './error' import { MessageWorkflowMapping } from './message-workflow-mapping' import { WorkflowState, WorkflowStatus } from './workflow-state' export type WorkflowHandler< TMessage extends Message, TMessageAttributes extends MessageAttributes, WorkflowStateType extends WorkflowState > = ( message?: TMessage, attributes?: TMessageAttributes, workflowState?: WorkflowStateType ) => | void | Partial | Promise> export type WhenHandler< WorkflowStateType extends WorkflowState, WorkflowType extends Workflow > = ( workflow: WorkflowType ) => WorkflowHandler type KeyOfType = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] export type OnWhenHandler< WorkflowStateType extends WorkflowState = WorkflowState, WorkflowType extends Workflow = Workflow > = { workflowCtor: ClassConstructor> workflowHandler: KeyOfType customLookup: MessageWorkflowMapping | undefined } /** * A workflow configuration that describes how to map incoming messages to handlers within the workflow. */ export class WorkflowMapper< WorkflowStateType extends WorkflowState, WorkflowType extends Workflow > { readonly onStartedBy = new Map< ClassConstructor, { workflowCtor: ClassConstructor> workflowHandler: KeyOfType } >() readonly onWhen = new Map< ClassConstructor, OnWhenHandler >() private workflowStateType: ClassConstructor | undefined constructor( private readonly workflow: ClassConstructor> ) {} get workflowStateCtor(): ClassConstructor | undefined { return this.workflowStateType } withState(workflowStateType: ClassConstructor): this { this.workflowStateType = workflowStateType return this } startedBy( message: ClassConstructor, workflowHandler: KeyOfType // workflowHandler: (workflow: WorkflowType) => WorkflowHandler ): this { if (this.onStartedBy.has(message)) { throw new WorkflowAlreadyStartedByMessage(this.workflow.name, message) } this.onStartedBy.set(message, { workflowHandler, workflowCtor: this.workflow }) return this } when( message: ClassConstructor, workflowHandler: KeyOfType, customLookup?: MessageWorkflowMapping ): this { if (this.onWhen.has(message)) { throw new WorkflowAlreadyHandlesMessage(this.workflow.name, message) } this.onWhen.set(message, { workflowHandler, workflowCtor: this.workflow, customLookup: customLookup as MessageWorkflowMapping< Message, WorkflowState > }) return this } } export abstract class Workflow { abstract configureWorkflow( mapper: WorkflowMapper ): void /** * Ends the workflow and optionally sets any final state. After this is returned, * the workflow instance will no longer be activated for subsequent messages. */ protected completeWorkflow(workflowState?: Partial) { return { ...workflowState, $status: WorkflowStatus.Complete } } /** * Prevents a new workflow from starting, and prevents the persistence of * the workflow state. This should only be used in `startedBy` workflow handlers. */ protected discardWorkflow() { return { $status: WorkflowStatus.Discard } } }