runflow
Version:
A fast and reliable flow engine for orchestration and more uses in *Node.js*
1 lines • 88.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/types.ts","../src/engine/flow-manager.ts","../src/debug.ts","../src/engine/task-process.ts","../src/resolver-library.ts","../src/engine/flow-state/flow-state.ts","../src/engine/flow-state/flow-ready.ts","../src/engine/flow-state/flow-finished.ts","../src/engine/flow-state/flow-paused.ts","../src/engine/flow-state/flow-pausing.ts","../src/engine/flow-state/flow-running.ts","../src/engine/flow-state/flow-stopped.ts","../src/engine/flow-state/flow-stopping.ts","../src/engine/process-manager.ts","../src/engine/value-queue-manager.ts","../src/engine/task.ts","../src/engine/flow-run-status.ts","../src/engine/flow.ts","../src/engine/specs.ts"],"sourcesContent":["import type { Debugger } from 'debug';\nimport type { Task } from './engine';\nimport type { ValueQueueManager } from './engine/value-queue-manager';\n\nexport enum FlowStateEnum {\n Ready = 'Ready',\n Running = 'Running',\n Finished = 'Finished',\n Pausing = 'Pausing',\n Paused = 'Paused',\n Stopping = 'Stopping',\n Stopped = 'Stopped',\n}\n\nexport enum FlowTransitionEnum {\n Start = 'Start',\n Finished = 'Finished',\n Reset = 'Reset',\n Pause = 'Pause',\n Paused = 'Paused',\n Resume = 'Resume',\n Stop = 'Stop',\n Stopped = 'Stopped',\n}\n\nexport type AnyValue = any; // eslint-disable-line @typescript-eslint/no-explicit-any\n\nexport type TransformTemplate = AnyValue;\n\nexport type OptPromise<T> = T | Promise<T>;\n\nexport interface ValueMap {\n [key: string]: AnyValue;\n}\n\n// @deprecated Use ValueMap instead\nexport type GenericValueMap = ValueMap;\n\nexport interface ITaskResolver {\n exec(\n params: ValueMap,\n context?: ValueMap,\n task?: Task,\n debug?: Debugger,\n log?: LoggerFn,\n ): OptPromise<ValueMap>;\n}\n\nexport class TaskResolver implements ITaskResolver {\n public exec(\n _params: ValueMap,\n _context?: ValueMap,\n _task?: Task,\n _debug?: Debugger,\n _log?: LoggerFn,\n ): OptPromise<ValueMap> {\n return {};\n }\n}\n\nexport type TaskResolverFn = (\n params: ValueMap,\n context?: ValueMap,\n task?: Task,\n debug?: Debugger,\n log?: LoggerFn,\n) => OptPromise<ValueMap>;\n\nexport type TaskResolverClass = typeof TaskResolver;\n\nexport type TaskResolverExecutor = TaskResolverClass | TaskResolverFn;\n\nexport class TaskResolverMap {\n [key: string]: TaskResolverExecutor;\n}\n\nexport interface TaskMap {\n [code: string]: Task;\n}\n\nexport interface TaskRunStatus {\n solvedReqs: ValueQueueManager;\n solvedResults: ValueMap;\n}\n\nexport interface FlowedPlugin {\n resolverLibrary: TaskResolverMap;\n}\n\nexport interface FlowedLogger {\n log(entry: FlowedLogEntry): void;\n}\n\nexport interface FlowedLogEntry {\n timestamp: Date;\n level: string; // 'fatal', 'error', 'warning', 'info', 'debug', 'trace'\n eventType: string;\n message: string;\n objectId?: string; // instance id\n tags?: string[];\n extra?: ValueMap; // free form serializable key-value object\n}\n\nexport type LoggerFn = ({\n n,\n m,\n mp,\n l,\n e,\n}: { n?: number; m: string; mp?: ValueMap; l?: string; e?: string }) => void;\n","import { readFile } from 'fs';\nimport * as http from 'http';\nimport type { IncomingMessage } from 'http';\nimport * as https from 'https';\nimport type {\n FlowedLogEntry,\n FlowedLogger,\n FlowedPlugin,\n OptPromise,\n TaskResolverMap,\n ValueMap,\n} from '../types';\nimport { Flow } from './flow';\nimport type { FlowSpec } from './specs';\n\nexport class FlowManager {\n public static plugins: {\n resolvers: TaskResolverMap;\n } = {\n resolvers: {},\n };\n\n public static logger: FlowedLogger | null = null;\n\n public static run(\n flowSpec: FlowSpec,\n params: ValueMap = {},\n expectedResults: string[] = [],\n resolvers: TaskResolverMap = {},\n context: ValueMap = {},\n options: ValueMap = {},\n ): OptPromise<ValueMap> {\n const flow = new Flow(flowSpec);\n return flow.start(params, expectedResults, resolvers, context, options);\n }\n\n public static runFromString(\n flowSpecJson: string,\n params: ValueMap = {},\n expectedResults: string[] = [],\n resolvers: TaskResolverMap = {},\n context: ValueMap = {},\n options: ValueMap = {},\n ): Promise<ValueMap> {\n return new Promise<ValueMap>((resolveFlow, reject) => {\n try {\n const flowSpec = JSON.parse(flowSpecJson);\n FlowManager.run(flowSpec, params, expectedResults, resolvers, context, options).then(\n resolveFlow,\n reject,\n );\n } catch (error) {\n reject(error);\n }\n });\n }\n\n public static runFromFile(\n flowSpecFilepath: string,\n params: ValueMap = {},\n expectedResults: string[] = [],\n resolvers: TaskResolverMap = {},\n context: ValueMap = {},\n options: ValueMap = {},\n ): Promise<ValueMap> {\n return new Promise<ValueMap>((resolveFlow, reject) => {\n readFile(flowSpecFilepath, 'utf8', (err, fileContents) => {\n if (err) {\n reject(err);\n } else {\n FlowManager.runFromString(\n fileContents,\n params,\n expectedResults,\n resolvers,\n context,\n options,\n ).then(resolveFlow, reject);\n }\n });\n });\n }\n\n public static runFromUrl(\n flowSpecUrl: string,\n params: ValueMap = {},\n expectedResults: string[] = [],\n resolvers: TaskResolverMap = {},\n context: ValueMap = {},\n options: ValueMap = {},\n ): Promise<ValueMap> {\n let client: typeof import('http') | typeof import('https') | null = null;\n // noinspection HttpUrlsUsage\n if (flowSpecUrl.startsWith('http://')) {\n client = http;\n } else if (flowSpecUrl.startsWith('https://')) {\n client = https;\n }\n\n if (client === null) {\n let actualProtocol = null;\n const matchResult = flowSpecUrl.match(/^([a-zA-Z]+):/);\n if (Array.isArray(matchResult) && matchResult.length === 2) {\n actualProtocol = matchResult[1];\n return Promise.reject(\n new Error(\n `Protocol not supported: ${actualProtocol}. Supported protocols are: [http, https]`,\n ),\n );\n } else {\n return Promise.reject(new Error(`Invalid URL: ${flowSpecUrl}`));\n }\n }\n\n return new Promise<ValueMap>((resolveFlow, reject) => {\n client! // eslint-disable-line @typescript-eslint/no-non-null-assertion\n .get(flowSpecUrl, { agent: false }, (res: IncomingMessage) => {\n const { statusCode } = res;\n const contentType = res.headers['content-type'] ?? 'application/json';\n\n let error;\n if (statusCode !== 200) {\n error = new Error(`Request failed with status code: ${statusCode}`);\n } else if (\n !(contentType.startsWith('application/json') || contentType.startsWith('text/plain'))\n ) {\n error = new Error(\n `Invalid content-type: Expected 'application/json' or 'text/plain' but received '${contentType}'`,\n );\n }\n\n if (error) {\n reject(error);\n } else {\n res.setEncoding('utf8');\n let rawData = '';\n res.on('data', (chunk: string) => {\n rawData += chunk;\n });\n res.on('end', () => {\n FlowManager.runFromString(\n rawData,\n params,\n expectedResults,\n resolvers,\n context,\n options,\n ).then(resolveFlow, reject);\n });\n }\n })\n .on('error', (error: Error) => {\n reject(error);\n });\n });\n }\n\n public static installPlugin(plugin: FlowedPlugin): void {\n // Installing plugin resolvers\n if (plugin.resolverLibrary) {\n for (const [name, resolver] of Object.entries(plugin.resolverLibrary)) {\n this.plugins.resolvers[name] = resolver;\n }\n }\n }\n\n public static installLogger(logger: FlowedLogger): void {\n this.logger = logger;\n }\n\n public static log(entry: FlowedLogEntry): void {\n if (FlowManager.logger === null) {\n return;\n }\n FlowManager.logger.log(entry);\n }\n}\n","import debug, { type Debugger } from 'debug';\n\nconst debugs: { [key: string]: Debugger } = {};\n\nexport default (scope: string): Debugger => {\n let d = debugs[scope];\n\n if (typeof d === 'undefined') {\n d = debug(`flowed:${scope}`);\n debugs[scope] = d;\n }\n\n return d;\n};\n","import type { Debugger } from 'debug';\nimport type {\n LoggerFn,\n TaskResolverClass,\n TaskResolverExecutor,\n TaskResolverFn,\n ValueMap,\n} from '../types';\nimport type { ProcessManager } from './process-manager';\nimport type { Task } from './task';\n\nexport class TaskProcess {\n public static nextPid = 1;\n\n protected params!: ValueMap;\n\n /**\n * Process id\n */\n public pid: number;\n\n constructor(\n public manager: ProcessManager,\n public id: number,\n public task: Task,\n protected taskResolverExecutor: TaskResolverExecutor,\n protected context: ValueMap,\n protected automapParams: boolean,\n protected automapResults: boolean,\n protected flowId: number,\n protected debug: Debugger,\n protected log: LoggerFn,\n ) {\n this.pid = TaskProcess.nextPid;\n TaskProcess.nextPid = (TaskProcess.nextPid + 1) % Number.MAX_SAFE_INTEGER;\n }\n\n public getParams(): ValueMap {\n return this.params;\n }\n\n public run(): Promise<ValueMap> {\n this.params = this.task.mapParamsForResolver(\n this.task.runStatus.solvedReqs.popAll(),\n this.automapParams,\n this.flowId,\n this.log,\n );\n\n let resolverFn = this.taskResolverExecutor as TaskResolverFn;\n let resolverThis: TaskResolverClass | undefined = undefined;\n const isClassResolver = this.taskResolverExecutor.prototype?.exec;\n if (isClassResolver) {\n // @todo try to remove type casts in this code section\n const resolverInstance = new (this.taskResolverExecutor as TaskResolverClass)();\n resolverFn = resolverInstance.exec;\n resolverThis = resolverInstance as unknown as TaskResolverClass;\n }\n\n return new Promise((resolve, reject) => {\n const onResolverSuccess = (resolverValue: ValueMap): void => {\n this.task.runStatus.solvedResults = this.task.mapResultsFromResolver(\n resolverValue,\n this.automapResults,\n this.flowId,\n this.log,\n );\n resolve(this.task.runStatus.solvedResults);\n };\n\n const onResolverError = (error: Error): void => {\n reject(error);\n };\n\n let resolverResult;\n\n // @sonar start-ignore Ignore this block because try is required even when not await-ing for the promise\n try {\n resolverResult = resolverFn.call(\n resolverThis,\n this.params,\n this.context,\n this.task,\n this.debug,\n this.log,\n );\n } catch (error) {\n // @todo Add test to get this error here with a sync resolver that throws error after returning the promise\n onResolverError(error as Error);\n }\n // @sonar end-ignore\n\n const resultIsObject = typeof resolverResult === 'object';\n const resultIsPromise = resolverResult?.constructor?.name === 'Promise';\n\n if (!resultIsObject) {\n throw new Error(\n `Expected resolver for task '${\n this.task.code\n }' to return an object or Promise that resolves to object. Returned value is of type '${typeof resolverResult}'.`,\n );\n }\n\n if (resultIsPromise) {\n // Resolver returned a Promise<ValueMap>\n (resolverResult as Promise<ValueMap>).then(onResolverSuccess).catch(onResolverError);\n } else {\n // Resolver returned a ValueMap\n onResolverSuccess(resolverResult as ValueMap);\n }\n });\n }\n}\n","import type { Debugger } from 'debug';\nimport { FlowManager, Task } from './engine';\nimport { TaskProcess } from './engine/task-process';\nimport type { LoggerFn, ValueMap } from './types';\n\n// Do nothing and finish\nexport class NoopResolver {\n public exec(): ValueMap {\n return {};\n }\n}\n\nexport class EchoResolver {\n public exec(params: ValueMap): ValueMap {\n return { out: params.in };\n }\n}\n\nexport class ThrowErrorResolver {\n public exec(params: ValueMap): ValueMap {\n throw new Error(\n typeof params.message !== 'undefined'\n ? params.message\n : 'ThrowErrorResolver resolver has thrown an error',\n );\n }\n}\n\nexport class ConditionalResolver {\n public exec(params: ValueMap): ValueMap {\n return params.condition ? { onTrue: params.trueResult } : { onFalse: params.falseResult };\n }\n}\n\n// Wait for 'ms' milliseconds and finish\nexport class WaitResolver {\n public exec(params: ValueMap): ValueMap {\n return new Promise<ValueMap>((resolve) => {\n setTimeout(() => {\n resolve({ result: params.result });\n }, params.ms);\n });\n }\n}\n\n// Run a flow and finish\nexport class SubFlowResolver {\n public async exec(params: ValueMap, context: ValueMap): Promise<ValueMap> {\n // @todo add test with subflow task with flowContext\n // @todo document $flowed\n\n // If no resolvers specified as parameter, inherit from global scope\n let flowResolvers = params.flowResolvers;\n if (typeof flowResolvers === 'undefined') {\n flowResolvers = context.$flowed.getResolvers();\n }\n\n let flowResult = await FlowManager.run(\n params.flowSpec,\n params.flowParams,\n params.flowExpectedResults,\n flowResolvers,\n context,\n context.$flowed.flow.runStatus.runOptions,\n );\n\n // @todo document param uniqueResult\n if (typeof params.uniqueResult === 'string') {\n flowResult = flowResult[params.uniqueResult];\n }\n\n return { flowResult };\n }\n}\n\n// Run a task multiple times and finishes returning an array with all results.\n// If one execution fails, the repeater resolver ends with an exception (this is valid for both parallel and not parallel modes).\nexport class RepeaterResolver {\n public async exec(\n params: ValueMap,\n context: ValueMap,\n task: Task,\n debug: Debugger,\n log: LoggerFn,\n ): Promise<ValueMap> {\n const resolver = context.$flowed.getResolverByName(params.resolver);\n if (resolver === null) {\n throw new Error(\n `Task resolver '${params.resolver}' for inner flowed::Repeater task has no definition.`,\n );\n }\n\n const innerTask = new Task('task-repeat-model', params.taskSpec);\n\n const resultPromises = [];\n let results = [];\n for (let i = 0; i < params.count; i++) {\n innerTask.resetRunStatus();\n innerTask.supplyReqs(params.taskParams);\n\n // @todo add test with repeater task with taskContext\n\n const process = new TaskProcess(\n context.$flowed.processManager,\n 0,\n innerTask,\n resolver,\n context,\n !!params.resolverAutomapParams,\n !!params.resolverAutomapResults,\n params.flowId,\n debug,\n log,\n );\n\n const result = process.run();\n\n if (params.parallel) {\n resultPromises.push(result);\n } else {\n results.push(await result); // If rejected, exception is not thrown here, it is delegated\n }\n }\n\n if (params.parallel) {\n results = await Promise.all(resultPromises); // If rejected, exception is not thrown here, it is delegated\n }\n\n return { results };\n }\n}\n\nexport class ArrayMapResolver {\n public async exec(\n params: ValueMap,\n context: ValueMap,\n task: Task,\n debug: Debugger,\n log: LoggerFn,\n ): Promise<ValueMap> {\n const resolver = context.$flowed.getResolverByName(params.resolver);\n if (resolver === null) {\n throw new Error(\n `Task resolver '${params.resolver}' for inner flowed::ArrayMap task has no definition.`,\n );\n }\n\n const innerTask = new Task('task-loop-model', params.spec);\n\n const resultPromises = [];\n let results = [];\n for (const taskParams of params.params) {\n innerTask.resetRunStatus();\n innerTask.supplyReqs(taskParams);\n\n // @todo add test with loop task with context\n\n const process = new TaskProcess(\n context.$flowed.processManager,\n 0,\n innerTask,\n resolver,\n context,\n !!params.automapParams,\n !!params.automapResults,\n params.flowId,\n debug,\n log,\n );\n\n const result = process.run();\n\n if (params.parallel) {\n resultPromises.push(result);\n } else {\n results.push(await result); // If rejected, exception is not thrown here, it is delegated\n }\n }\n\n if (params.parallel) {\n results = await Promise.all(resultPromises); // If rejected, exception is not thrown here, it is delegated\n }\n\n return { results };\n }\n}\n\n// @todo document Loop resolver\nexport class LoopResolver {\n public async exec(\n params: ValueMap,\n context: ValueMap,\n task: Task,\n debug: Debugger,\n log: LoggerFn,\n ): Promise<ValueMap> {\n const resolverName = params.subtask.resolver.name;\n const resolver = context.$flowed.getResolverByName(resolverName);\n if (resolver === null) {\n throw new Error(\n `Task resolver '${resolverName}' for inner flowed::Loop task has no definition.`,\n );\n }\n\n const innerTask = new Task('task-loop-model', params.subtask);\n\n const resultPromises = [];\n let outCollection = [];\n for (const item of params.inCollection) {\n const taskParams = { [params.inItemName]: item };\n\n innerTask.resetRunStatus();\n innerTask.supplyReqs(taskParams);\n\n // @todo add test with loop task with context\n\n const process = new TaskProcess(\n context.$flowed.processManager,\n 0,\n innerTask,\n resolver,\n context,\n !!params.automapParams,\n !!params.automapResults,\n params.flowId,\n debug,\n log,\n );\n\n const itemResultPromise = process.run();\n\n if (params.parallel) {\n resultPromises.push(itemResultPromise);\n } else {\n const itemResult = await itemResultPromise;\n outCollection.push(itemResult[params.outItemName]); // If rejected, exception is not thrown here, it is delegated\n }\n }\n\n if (params.parallel) {\n const outCollectionResults = await Promise.all(resultPromises); // If rejected, exception is not thrown here, it is delegated\n outCollection = outCollectionResults.map((itemResult) => itemResult[params.outItemName]);\n }\n\n return { outCollection };\n }\n}\n\nexport class StopResolver {\n public exec(params: ValueMap, context: ValueMap): ValueMap {\n return { promise: context.$flowed.flow.stop() };\n }\n}\n\nexport class PauseResolver {\n public exec(params: ValueMap, context: ValueMap): ValueMap {\n return { promise: context.$flowed.flow.pause() };\n }\n}\n","import type { Debugger } from 'debug';\nimport rawDebug from '../../debug';\nimport {\n ArrayMapResolver,\n ConditionalResolver,\n EchoResolver,\n LoopResolver,\n NoopResolver,\n PauseResolver,\n RepeaterResolver,\n StopResolver,\n SubFlowResolver,\n ThrowErrorResolver,\n WaitResolver,\n} from '../../resolver-library';\nimport {\n type AnyValue,\n type FlowStateEnum,\n FlowTransitionEnum,\n type FlowedLogEntry,\n type OptPromise,\n type TaskResolverExecutor,\n type TaskResolverMap,\n type ValueMap,\n} from '../../types';\nimport { FlowManager } from '../flow-manager';\nimport type { FlowRunStatus, SerializedFlowRunStatus } from '../flow-run-status';\nimport type { Task } from '../task';\nimport type { TaskProcess } from '../task-process';\nimport type { IFlow } from './iflow';\n\nexport abstract class FlowState implements IFlow {\n /**\n * Built-in resolver library.\n * @type {TaskResolverMap}\n */\n protected static builtInResolvers: TaskResolverMap = {\n 'flowed::Noop': NoopResolver,\n 'flowed::Echo': EchoResolver,\n 'flowed::ThrowError': ThrowErrorResolver,\n 'flowed::Conditional': ConditionalResolver,\n 'flowed::Wait': WaitResolver,\n 'flowed::SubFlow': SubFlowResolver,\n 'flowed::Repeater': RepeaterResolver,\n 'flowed::Loop': LoopResolver,\n 'flowed::ArrayMap': ArrayMapResolver,\n 'flowed::Stop': StopResolver,\n 'flowed::Pause': PauseResolver,\n };\n\n protected runStatus: FlowRunStatus;\n\n public constructor(runStatus: FlowRunStatus) {\n this.runStatus = runStatus;\n }\n\n public start(\n params: ValueMap,\n expectedResults: string[],\n resolvers: TaskResolverMap,\n context: ValueMap,\n _options: ValueMap = {},\n ): OptPromise<ValueMap> {\n throw this.createTransitionError(FlowTransitionEnum.Start);\n }\n\n public finished(_error: Error | boolean = false): void {\n throw this.createTransitionError(FlowTransitionEnum.Finished);\n }\n\n public pause(): Promise<ValueMap> {\n throw this.createTransitionError(FlowTransitionEnum.Pause);\n }\n\n public paused(_error: Error | boolean = false): void {\n throw this.createTransitionError(FlowTransitionEnum.Paused);\n }\n\n public resume(): Promise<ValueMap> {\n throw this.createTransitionError(FlowTransitionEnum.Resume);\n }\n\n public stop(): Promise<ValueMap> {\n throw this.createTransitionError(FlowTransitionEnum.Stop);\n }\n\n public stopped(_error: Error | boolean = false): void {\n throw this.createTransitionError(FlowTransitionEnum.Stopped);\n }\n\n public reset(): void {\n throw this.createTransitionError(FlowTransitionEnum.Reset);\n }\n\n public abstract getStateCode(): FlowStateEnum;\n\n public execFinishResolve(): void {\n this.runStatus.finishResolve(this.runStatus.results);\n }\n\n public execFinishReject(error: Error): void {\n this.runStatus.finishReject(error);\n }\n\n public isRunning(): boolean {\n return this.runStatus.processManager.runningCount() > 0;\n }\n\n public setExpectedResults(expectedResults: string[]): void {\n // Check expected results that cannot be fulfilled\n const missingExpected = expectedResults.filter(\n (r) => !this.runStatus.taskProvisions.includes(r),\n );\n if (missingExpected.length > 0) {\n const msg = `The results [${missingExpected.join(', ')}] are not provided by any task`;\n if (this.runStatus.options.throwErrorOnUnsolvableResult) {\n throw new Error(msg);\n } else {\n this.log({ m: msg, l: 'w' });\n }\n }\n\n this.runStatus.expectedResults = [...expectedResults];\n }\n\n public getResults(): ValueMap {\n return this.runStatus.results;\n }\n\n public setResolvers(resolvers: TaskResolverMap): void {\n this.runStatus.resolvers = resolvers;\n }\n\n public setContext(context: ValueMap): void {\n this.runStatus.context = {\n $flowed: {\n getResolverByName: this.getResolverByName.bind(this),\n getResolvers: this.getResolvers.bind(this),\n processManager: this.runStatus.processManager,\n flow: this.runStatus.flow,\n },\n ...context,\n };\n }\n\n public setRunOptions(options: ValueMap): void {\n const defaultRunOptions = {\n debugKey: 'flow',\n instanceId: null, // @todo check if it would be better to move this field into logFields\n logFields: {},\n };\n this.runStatus.runOptions = Object.assign(defaultRunOptions, options);\n }\n\n public supplyParameters(params: ValueMap): void {\n for (const [paramCode, paramValue] of Object.entries(params)) {\n this.runStatus.state.supplyResult(paramCode, paramValue);\n }\n }\n\n public createFinishPromise() {\n this.runStatus.finishPromise = new Promise<ValueMap>((resolve, reject) => {\n this.runStatus.finishResolve = resolve;\n this.runStatus.finishReject = reject;\n });\n }\n\n public getResolverForTask(task: Task): TaskResolverExecutor {\n const name = task.getResolverName();\n\n const resolver = this.getResolverByName(name);\n\n if (resolver === null) {\n throw new Error(\n `Task resolver '${name}' for task '${task.code}' has no definition. Defined custom resolvers are: [${Object.keys(\n this.runStatus.resolvers,\n ).join(', ')}].`,\n );\n }\n\n return resolver;\n }\n\n public getResolverByName(name: string): TaskResolverExecutor | null {\n // Lookup for custom resolvers\n const resolvers = this.runStatus.resolvers;\n const hasCustomResolver = typeof resolvers[name] !== 'undefined';\n if (hasCustomResolver) {\n return resolvers[name];\n }\n\n // Lookup for plugin resolvers\n const hasPluginResolver = typeof FlowManager.plugins.resolvers[name] !== 'undefined';\n if (hasPluginResolver) {\n return FlowManager.plugins.resolvers[name];\n }\n\n // Lookup for built-in resolvers\n const hasBuiltInResolver = typeof FlowState.builtInResolvers[name] !== 'undefined';\n if (hasBuiltInResolver) {\n return FlowState.builtInResolvers[name];\n }\n\n return null;\n }\n\n public getResolvers(): TaskResolverMap {\n const customResolvers = this.runStatus.resolvers;\n const pluginResolvers = FlowManager.plugins.resolvers;\n const builtInResolver = FlowState.builtInResolvers;\n\n return {\n ...builtInResolver,\n ...pluginResolvers,\n ...customResolvers,\n };\n }\n\n public supplyResult(resultName: string, result: AnyValue): void {\n // Checks if the task result is required by other tasks.\n // If it is not, it is likely a flow output value.\n const suppliesSomeTask = typeof this.runStatus.tasksByReq[resultName] !== 'undefined';\n\n if (suppliesSomeTask) {\n const suppliedTasks = this.runStatus.tasksByReq[resultName];\n const suppliedTaskCodes = Object.keys(suppliedTasks);\n for (const taskCode of suppliedTaskCodes) {\n const suppliedTask = suppliedTasks[taskCode];\n\n suppliedTask.supplyReq(resultName, result);\n\n // @todo Possible optimization: supply all results first, then check ready tasks\n // @todo This 'if' could actually be a 'while', in case more than one instance of the same task get ready\n if (suppliedTask.isReadyToRun()) {\n this.runStatus.tasksReady.push(suppliedTask);\n }\n }\n }\n\n // If the result is required as flow output, it is provided\n const isExpectedResult = this.runStatus.expectedResults.indexOf(resultName) > -1;\n if (isExpectedResult) {\n this.runStatus.results[resultName] = result;\n }\n }\n\n public getStateInstance(state: FlowStateEnum): FlowState {\n return this.runStatus.states[state];\n }\n\n public startReadyTasks(): void {\n const readyTasks = this.runStatus.tasksReady;\n this.runStatus.tasksReady = [];\n\n for (const task of readyTasks) {\n const taskResolver = this.runStatus.state.getResolverForTask(task);\n\n const process = this.runStatus.processManager.createProcess(\n task,\n taskResolver,\n this.runStatus.context,\n !!this.runStatus.options.resolverAutomapParams,\n !!this.runStatus.options.resolverAutomapResults,\n this.runStatus.id,\n this.debug as Debugger,\n this.log.bind(this),\n );\n\n const errorHandler = (error: Error): void => {\n this.processFinished(process, error, true);\n };\n\n process\n .run()\n .then(() => {\n this.processFinished(process, false, true);\n }, errorHandler)\n .catch(errorHandler);\n\n this.log({\n n: this.runStatus.id,\n m: `Task '${task.code}(${task.getResolverName()})' started, params: %O`,\n mp: process.getParams(),\n e: 'TS',\n pid: process.pid,\n task: { code: task.code, type: task.getResolverName() },\n });\n }\n }\n\n public setState(newState: FlowStateEnum): void {\n const prevState = this.runStatus.state.getStateCode();\n this.runStatus.state = this.getStateInstance(newState);\n this.log({\n n: this.runStatus.id,\n m: `Changed flow state from '${prevState}' to '${newState}'`,\n l: 'd',\n e: 'FC',\n });\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n throw this.createMethodError('getSerializableState');\n }\n\n protected processFinished(\n process: TaskProcess,\n error: Error | boolean,\n stopFlowExecutionOnError: boolean,\n ): void {\n this.runStatus.processManager.removeProcess(process);\n\n const task = process.task;\n const taskCode = task.code;\n const taskSpec = task.spec;\n const taskProvisions = taskSpec.provides ?? [];\n const taskResults = task.getResults();\n const hasDefaultResult = Object.prototype.hasOwnProperty.call(taskSpec, 'defaultResult');\n\n if (error) {\n this.log({\n n: this.runStatus.id,\n m: `Error in task '${taskCode}', results: %O`,\n mp: taskResults,\n l: 'e',\n e: 'TF',\n pid: process.pid,\n task: { code: task.code, type: task.getResolverName() },\n });\n } else {\n this.log({\n n: this.runStatus.id,\n m: `Finished task '${taskCode}', results: %O`,\n mp: taskResults,\n e: 'TF',\n pid: process.pid,\n task: { code: task.code, type: task.getResolverName() },\n });\n }\n\n for (const resultName of taskProvisions) {\n if (Object.prototype.hasOwnProperty.call(taskResults, resultName)) {\n this.runStatus.state.supplyResult(resultName, taskResults[resultName]);\n } else if (hasDefaultResult) {\n // @todo add defaultResult to repeater task\n this.runStatus.state.supplyResult(resultName, taskSpec.defaultResult);\n } else {\n this.log({\n n: this.runStatus.id,\n m: `Expected value '${resultName}' was not provided by task '${taskCode}' with resolver '${task.getResolverName()}'. Consider using the task field 'defaultResult' to provide values by default.`,\n l: 'w',\n });\n }\n }\n\n this.runStatus.state.postProcessFinished(error, stopFlowExecutionOnError);\n }\n\n protected postProcessFinished(_error: Error | boolean, _stopFlowExecutionOnError: boolean): void {\n // Default empty implementation to be overridden when applies.\n }\n\n protected createTransitionError(transition: string): Error {\n return new Error(\n `Cannot execute transition ${transition} in current state ${this.getStateCode()}.`,\n );\n }\n\n protected createMethodError(method: string): Error {\n return new Error(`Cannot execute method ${method} in current state ${this.getStateCode()}.`);\n }\n\n public debug(formatter: string, ...args: AnyValue[]): void {\n const scope =\n this?.runStatus && typeof this.runStatus.runOptions.debugKey === 'string'\n ? this.runStatus.runOptions.debugKey\n : 'init';\n rawDebug(scope)(formatter, ...args);\n }\n\n public static formatDebugMessage({\n n,\n m,\n l,\n e,\n }: { n?: number; m: string; mp?: ValueMap; l?: string; e?: string }): string {\n const levelIcon = l === 'w' ? '⚠️ ' : '';\n const eventIcons = {\n FS: '▶ ',\n FF: '✔ ',\n TS: ' ‣ ',\n TF: ' ✓ ',\n FC: ' ⓘ ',\n FT: '◼ ',\n FP: '⏸ ',\n };\n let eventIcon = (eventIcons as any)[e ?? ''] ?? ''; // eslint-disable-line @typescript-eslint/no-explicit-any\n if (e === 'TF' && ['e', 'f'].includes(l ?? '')) {\n eventIcon = ' ✗';\n } else if (e === 'FF' && ['e', 'f'].includes(l ?? '')) {\n eventIcon = '✘';\n }\n const icon = levelIcon + eventIcon;\n\n return `[${n}] ${icon}${m}`;\n }\n\n public static createLogEntry(\n {\n n,\n m,\n mp,\n l,\n e,\n pid,\n task,\n }: {\n n?: number;\n m: string;\n mp?: ValueMap;\n l?: string;\n e?: string;\n pid?: number;\n task?: AnyValue;\n },\n flowStatus?: FlowRunStatus,\n ): FlowedLogEntry {\n const formatLevel = (level: string | undefined) => {\n level = level || 'i';\n switch (level) {\n case 'e':\n return 'error';\n case 'w':\n return 'warning';\n case 'i':\n return 'info';\n case 'd':\n return 'debug';\n default:\n throw new Error(`Not supported error level: \"${level}\"`);\n }\n };\n\n const formatEvent = (event: string | undefined) => {\n switch (event) {\n case 'TS':\n return 'Task.Started';\n case 'TF':\n return 'Task.Finished';\n case 'FC':\n return 'Flow.StateChanged';\n case 'FS':\n return 'Flow.Started';\n case 'FF':\n return 'Flow.Finished';\n case 'FT':\n return 'Flow.Stopped';\n case 'FP':\n return 'Flow.Paused';\n default:\n return 'General';\n }\n };\n\n const formatMsg = (templateMsg: string, param: ValueMap | undefined) => {\n if (param) {\n // @todo Take into account that 'param' could have circular references, hence JSON.stringify(param) could fail\n const paramStr = JSON.stringify(param);\n return templateMsg.replace(\n '%O',\n paramStr.length > 100 ? paramStr.slice(0, 97) + '...' : paramStr,\n );\n }\n return templateMsg;\n };\n\n let auditLogEntry: FlowedLogEntry = {\n level: formatLevel(l),\n eventType: formatEvent(e),\n message: formatMsg(m, mp),\n timestamp: new Date(),\n extra: {\n pid,\n task,\n debugId: n,\n values: JSON.stringify(mp),\n },\n };\n\n if (flowStatus) {\n auditLogEntry.objectId = flowStatus.runOptions.instanceId;\n auditLogEntry = Object.assign(flowStatus.runOptions.logFields, auditLogEntry);\n }\n\n return auditLogEntry;\n }\n\n public log({\n n,\n m,\n mp,\n l,\n e,\n pid,\n task,\n }: {\n n?: number;\n m: string;\n mp?: ValueMap;\n l?: string;\n e?: string;\n pid?: number;\n task?: AnyValue;\n }): void {\n this.debug(FlowState.formatDebugMessage({ n, m, mp, l, e }), [mp]);\n FlowManager.log(FlowState.createLogEntry({ n, m, mp, l, e, pid, task }, this.runStatus));\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum, type TaskResolverMap, type ValueMap } from '../../types';\nimport type { SerializedFlowRunStatus } from '../flow-run-status';\n\nexport class FlowReady extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Ready;\n }\n\n public start(\n params: ValueMap,\n expectedResults: string[],\n resolvers: TaskResolverMap,\n context: ValueMap,\n options: ValueMap = {},\n ) {\n this.setRunOptions(options);\n this.log({ n: this.runStatus.id, m: 'Flow started with params: %O', mp: params, e: 'FS' });\n\n this.setState(FlowStateEnum.Running);\n\n this.setExpectedResults([...expectedResults]);\n this.setResolvers(resolvers);\n this.setContext(context);\n this.supplyParameters(params);\n\n this.createFinishPromise();\n\n // Run tasks\n this.startReadyTasks();\n\n // Notify 'flow finished' in advance when:\n // - there are no tasks ready to start (cannot execute anything because requirements are not satisfied), or\n // - there are no tasks at all in the flow spec.\n if (!this.runStatus.state.isRunning()) {\n this.runStatus.state.finished();\n }\n\n return this.runStatus.finishPromise;\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n return this.runStatus.toSerializable();\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum } from '../../types';\nimport type { SerializedFlowRunStatus } from '../flow-run-status';\n\nexport class FlowFinished extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Finished;\n }\n\n public reset(): void {\n this.setState(FlowStateEnum.Ready);\n this.runStatus.initRunStatus(this.runStatus.spec);\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n return this.runStatus.toSerializable();\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum, type ValueMap } from '../../types';\nimport type { SerializedFlowRunStatus } from '../flow-run-status';\n\nexport class FlowPaused extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Paused;\n }\n\n public resume(): Promise<ValueMap> {\n this.setState(FlowStateEnum.Running);\n\n this.createFinishPromise();\n\n this.startReadyTasks();\n\n if (!this.runStatus.state.isRunning()) {\n this.runStatus.state.finished();\n }\n\n return this.runStatus.finishPromise;\n }\n\n public stop(): Promise<ValueMap> {\n this.setState(FlowStateEnum.Stopping);\n\n return Promise.resolve(this.getResults());\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n return this.runStatus.toSerializable();\n }\n}\n","import { FlowStateEnum } from '../../types';\nimport { FlowState } from './flow-state';\n\nexport class FlowPausing extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Pausing;\n }\n\n public paused(error: Error | boolean): void {\n this.setState(FlowStateEnum.Paused);\n\n if (error) {\n this.log({ n: this.runStatus.id, m: 'Flow paused with error.', e: 'FP' });\n\n this.execFinishReject(error as Error);\n } else {\n this.log({ n: this.runStatus.id, m: 'Flow paused.', e: 'FP' });\n\n this.execFinishResolve();\n }\n }\n\n protected postProcessFinished(error: Error | boolean, _stopFlowExecutionOnError: boolean): void {\n this.runStatus.state.paused(error);\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum, type ValueMap } from '../../types';\n\nexport class FlowRunning extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Running;\n }\n\n public pause(): Promise<ValueMap> {\n this.setState(FlowStateEnum.Pausing);\n\n return this.runStatus.finishPromise;\n }\n\n public stop(): Promise<ValueMap> {\n this.setState(FlowStateEnum.Stopping);\n\n return this.runStatus.finishPromise;\n }\n\n public finished(error: Error | boolean = false): void {\n this.setState(FlowStateEnum.Finished);\n\n if (error) {\n this.log({\n n: this.runStatus.id,\n m: 'Flow finished with error. Results: %O',\n mp: this.getResults(),\n l: 'e',\n e: 'FF',\n });\n this.execFinishReject(error as Error);\n } else {\n this.log({\n n: this.runStatus.id,\n m: 'Flow finished with results: %O',\n mp: this.getResults(),\n e: 'FF',\n });\n this.execFinishResolve();\n }\n }\n\n protected postProcessFinished(error: Error | boolean, stopFlowExecutionOnError: boolean): void {\n const stopExecution = error && stopFlowExecutionOnError;\n if (!stopExecution) {\n this.runStatus.state.startReadyTasks();\n }\n\n if (!this.runStatus.state.isRunning()) {\n this.runStatus.state.finished(error);\n }\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum } from '../../types';\nimport type { SerializedFlowRunStatus } from '../flow-run-status';\n\nexport class FlowStopped extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Stopped;\n }\n\n public reset(): void {\n this.setState(FlowStateEnum.Ready);\n this.runStatus.initRunStatus(this.runStatus.spec);\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n return this.runStatus.toSerializable();\n }\n}\n","import { FlowState } from '.';\nimport { FlowStateEnum } from '../../types';\n\nexport class FlowStopping extends FlowState {\n public getStateCode(): FlowStateEnum {\n return FlowStateEnum.Stopping;\n }\n\n public stopped(error: Error | boolean = false): void {\n this.setState(FlowStateEnum.Stopped);\n\n if (error) {\n this.log({ n: this.runStatus.id, m: 'Flow stopped with error.', e: 'FT' });\n this.execFinishReject(error as Error);\n } else {\n this.log({ n: this.runStatus.id, m: 'Flow stopped.', e: 'FT' });\n this.execFinishResolve();\n }\n }\n\n protected postProcessFinished(error: Error | boolean, _stopFlowExecutionOnError: boolean): void {\n this.runStatus.state.stopped(error);\n }\n}\n","import type { Debugger } from 'debug';\nimport type { LoggerFn, TaskResolverExecutor, ValueMap } from '../types';\nimport type { Task } from './task';\nimport { TaskProcess } from './task-process';\n\nexport class ProcessManager {\n public nextProcessId: number;\n\n public processes: TaskProcess[];\n\n public constructor() {\n this.nextProcessId = 1;\n this.processes = [];\n }\n\n public createProcess(\n task: Task,\n taskResolverExecutor: TaskResolverExecutor,\n context: ValueMap,\n automapParams: boolean,\n automapResults: boolean,\n flowId: number,\n debug: Debugger,\n log: LoggerFn,\n ): TaskProcess {\n this.nextProcessId++;\n const process = new TaskProcess(\n this,\n this.nextProcessId,\n task,\n taskResolverExecutor,\n context,\n automapParams,\n automapResults,\n flowId,\n debug,\n log,\n );\n this.processes.push(process);\n\n return process;\n }\n\n public runningCount(): number {\n return this.processes.length;\n }\n\n public removeProcess(process: TaskProcess): void {\n const processIndex = this.processes.findIndex((p) => p.id === process.id);\n this.processes.splice(processIndex, 1);\n }\n}\n","import type { AnyValue, ValueMap } from '../types';\n\nexport type ValueQueue = AnyValue[];\n\nexport interface ValueQueueMap {\n [name: string]: ValueQueue;\n}\n\n// Everything needed to create a ValueQueueManager restoring a previous state\nexport type SerializableValueQueueManager = ValueQueueMap;\n\nexport class ValueQueueManager {\n public static fromSerializable(serializable: ValueQueueManager): ValueQueueManager {\n const queueNames = Object.keys(serializable);\n const instance = new ValueQueueManager(queueNames);\n instance.queues = serializable as unknown as ValueQueueMap;\n instance.nonEmptyQueues = queueNames.reduce((acc, name) => {\n if (instance.queues[name].length > 0) {\n acc.add(name);\n }\n return acc;\n }, new Set<string>());\n return instance;\n }\n\n protected queues: ValueQueueMap;\n\n // This field can be calculated from this.queues\n protected queueNames: string[]; // List of queue names\n\n // This field can be calculated from this.queues\n protected nonEmptyQueues: Set<string>; // List of queue names\n\n public constructor(queueNames: string[]) {\n this.nonEmptyQueues = new Set();\n this.queueNames = [...queueNames];\n this.queues = queueNames.reduce((acc: ValueQueueMap, name) => {\n acc[name] = [];\n return acc;\n }, {});\n }\n\n public push(queueName: string, value: AnyValue): void {\n if (!this.queueNames.includes(queueName)) {\n throw new Error(\n `Queue name ${queueName} does not exist in queue manager. Existing queues are: [${this.queueNames.join(', ')}].`,\n );\n }\n\n this.nonEmptyQueues.add(queueName);\n this.queues[queueName].push(value);\n }\n\n public getEmptyQueueNames(): string[] {\n return this.queueNames.reduce((acc: string[], name: string) => {\n if (this.queues[name].length === 0) {\n acc.push(name);\n }\n return acc;\n }, []);\n }\n\n public popAll(): ValueMap {\n this.validateAllNonEmpty();\n\n return this.queueNames.reduce((acc: ValueMap, name: string) => {\n acc[name] = this.queues[name].shift();\n if (this.queues[name].length === 0) {\n this.nonEmptyQueues.delete(name);\n }\n return acc;\n }, {});\n }\n\n public topAll(): ValueMap {\n this.validateAllNonEmpty();\n\n return this.queueNames.reduce((acc: ValueMap, name: string) => {\n acc[name] = this.queues[name][0];\n return acc;\n }, {});\n }\n\n // For this to work, all user values must be serializable to JSON\n public toSerializable(): SerializableValueQueueManager {\n return JSON.parse(JSON.stringify(this.queues));\n }\n\n public validateAllNonEmpty(): void {\n if (!this.allHaveContent()) {\n throw new Error(`Some of the queues are empty: [${this.getEmptyQueueNames().join(', ')}].`);\n }\n }\n\n public allHaveContent(): boolean {\n return this.nonEmptyQueues.size === this.queueNames.length;\n }\n}\n","import type { AnyValue, LoggerFn, TaskRunStatus, ValueMap } from '../types';\nimport type { SerializedFlowRunStatus } from './flow-run-status';\nimport type { ResolverParamInfoTransform, ResolverParamInfoValue, TaskSpec } from './specs';\nimport { ValueQueueManager } from './value-queue-manager';\n\nconst ST = require('flowed-st');\n\nexport class Task {\n public runStatus!: TaskRunStatus;\n\n public constructor(\n public code: string,\n public spec: TaskSpec,\n ) {\n this.parseSpec();\n }\n\n public getResolverName(): string {\n return (this.spec.resolver ?? { name: 'flowed::Noop' }).name;\n }\n\n public getSerializableState(): SerializedFlowRunStatus {\n const result = JSON.parse(JSON.stringify(this.runStatus));\n result.solvedReqs = this.runStatus.solvedReqs.toSerializable();\n return result;\n }\n\n public setSerializableState(runStatus: TaskRunStatus): void {\n this.runStatus = JSON.parse(JSON.stringify(runStatus));\n this.runStatus.solvedReqs = ValueQueueManager.fromSerializable(runStatus.solvedReqs);\n }\n\n public resetRunStatus(): void {\n const reqs = [...(this.spec.requires ?? [])];\n\n this.runStatus = {\n solvedReqs: new ValueQueueManager(reqs),\n solvedResults: {},\n };\n }\n\n public isReadyToRun(): boolean {\n return this.runStatus.solvedReqs.allHaveContent();\n }\n\n public getResults(): ValueMap {\n return this.runStatus.solvedResults;\n }\n\n public supplyReq(reqName: string, value: AnyValue): void {\n const reqIndex = (this.spec.requires ?? []).indexOf(reqName);\n if (reqIndex === -1) {\n // This can only happen if supplyReq is called manually by the user. The flow will never call with an invalid reqName.\n throw new Error(`Requirement '${reqName}' for task '${this.code}' is not valid.`);\n }\n\n this.runStatus.solvedReqs.push(reqName, value);\n }\n\n public supplyReqs(reqsMap: ValueMap): void {\n for (const [reqName, req] of Object.entries(reqsMap)) {\n this.supplyReq(reqName, req);\n }\n }\n\n public mapParamsForResolver(\n solvedReqs: ValueMap,\n automap: boolean,\n flowId: number,\n log: LoggerFn,\n ): ValueMap {\n const params: ValueMap = {};\n\n let resolverParams = this.spec.resolver?.params ?? {};\n\n if (automap) {\n const requires = this.spec.requires ?? [];\n // When `Object.fromEntries()` is available in ES, use it instead of the following solution\n // @todo Add test with requires = []\n const autoMappedParams = requires\n .map((req) => ({ [req]: req }))\n .reduce((accum, peer) => Object.assign(accum, peer), {});\n log({\n n: flowId,\n m: ` ⓘ Auto-mapped resolver params in task '${this.code}': %O`,\n mp: autoMappedParams,\n l: 'd',\n });\n resolverParams = Object.assign(autoMappedParams, resolverParams);\n }\n\n let paramValue;\n for (const [resolverParamName, paramSolvingInfo] of Object.entries(resolverParams)) {\n // @todo Add test to check the case when a loop round does not set anything and make sure next value (`paramValue`) is undefined by default\n\n if (typeof paramSolvingInfo === 'string') {\n // If it is string, it is a task param name\n paramValue = solvedReqs[paramSolvingInfo];\n } else if (Object.prototype.hasOwnProperty.call(paramSolvingInfo, 'value')) {\n // If it is an object, expect the format { value: <some value> } or { transform: <some template> }\n // Implicit condition: typeof paramSolvingInfo === 'object' && paramSolvingInfo !== null\n // Direct value pre-processor\n paramValue = (paramSolvingInfo as ResolverParamInfoValue).value;\n } else {\n // Template transform pre-processor\n // Implicit condition: paramSolvingInfo.hasOwnProperty('transform')\n const template = (paramSolvingInfo as ResolverParamInfoTransform).transform;\n paramValue = ST.select(solvedReqs).transformWith(template).root();\n }\n\n params[resolverParamName] = paramValue;\n }\n\n return params;\n }\n\n public mapResultsFromResolver(\n solvedResults: ValueMap,\n automap: boolean,\n flowId: number,\n log: LoggerFn,\n ): ValueMap {\n if (typeof solvedResults !== 'object') {\n throw new Error(\n `Expected resolver for task '${\n this.code\n }' to return an object or Promise that resolves to object. Returned value is of type '${typeof solvedResults}'.`,\n );\n }\n\n const results: ValueMap = {};\n\n let resolverResults = this.spec.resolver?.results ?? {};\n\n if (automap) {\n const provides = this.spec.provides ?? [];\n // @todo Add test with provides = []\n const autoMappedResults = provides.reduce(\n (acc: Valu