UNPKG

chrono-forge

Version:

A comprehensive framework for building resilient Temporal workflows, advanced state management, and real-time streaming activities in TypeScript. Designed for a seamless developer experience with powerful abstractions, dynamic orchestration, and full cont

718 lines (713 loc) 28.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Workflow = void 0; exports.Temporal = Temporal; require("reflect-metadata"); const workflow = __importStar(require("@temporalio/workflow")); const eventemitter3_1 = __importDefault(require("eventemitter3")); const decorators_1 = require("../decorators"); const mnemonist_1 = require("mnemonist"); const DSLInterpreter_1 = require("./DSLInterpreter"); function Temporal(options) { return function (constructor) { const { name: optionalName, taskQueue, activities, ...extraOptions } = options || {}; const workflowName = optionalName ?? constructor.name; if (!(constructor.prototype instanceof Workflow)) { throw new Error(`Workflows must extend the Workflow class, any class that extends Workflow is supported. ${constructor.name} does not extend Workflow.`); } const construct = new Function('workflow', 'constructor', 'extraOptions', 'activities', ` return async function ${workflowName}(...args) { const instance = new constructor(args[0], { ...extraOptions, activities }); try { instance.workflowType = '${workflowName}'; await instance.bindEventHandlers(); await instance.emitAsync('setup'); await instance.emitAsync('hooks'); await instance.emitAsync('init'); const executionMethod = instance.isContinueable ? 'executeWorkflow' : 'execute'; let result; try { result = await instance[executionMethod](...args); } finally { try { instance.cleanup(); } catch (cleanupError) { workflow.log.error('Error during workflow cleanup: ' + (cleanupError instanceof Error ? cleanupError.message : String(cleanupError))); } } return result; } catch (e) { if (workflow.isCancellation(e)) { await workflow.CancellationScope.nonCancellable(async () => { await workflow.condition(() => instance.status === 'cancelled'); }); } throw e; } } `)(workflow, constructor, extraOptions, activities); return construct; }; } class Workflow extends eventemitter3_1.default { args; options; workflowType; handles = new mnemonist_1.LRUCacheWithDelete(2000); _eventsBound = false; _hooksBound = false; _signalsBound = false; _queriesBound = false; _propertiesBound = false; _stepsBound = false; dsl; createLogger(level) { return (message, ...args) => workflow.log[level](`[${this.constructor.name}]:[${workflow.workflowInfo().workflowId}]: ${message}`, ...args); } log = { debug: this.createLogger('debug'), info: this.createLogger('info'), warn: this.createLogger('warn'), error: this.createLogger('error'), trace: this.createLogger('trace') }; result; queryHandlers = {}; signalHandlers = {}; stepHandlers = {}; startDelay = undefined; isContinueable = false; continueAsNew = false; maxIterations = 10000; maxHistorySize = 26214400; iteration = 0; pendingIteration = false; status = 'running'; pendingUpdate = true; pause() { if (this.status !== 'paused') { this.log.info(`Pausing...`); this.status = 'paused'; workflow.upsertMemo({ status: this.status, lastUpdated: new Date().toISOString() }); this.forwardSignalToChildren('pause'); } else { this.log.info(`Already paused!`); } } resume() { if (this.status !== 'running') { this.log.info(`Resuming...`); this.status = 'running'; workflow.upsertMemo({ status: this.status, lastUpdated: new Date().toISOString() }); this.forwardSignalToChildren('resume'); } else { this.log.info(`Already running...`); } } cancel() { if (this.status !== 'cancelling') { this.log.info(`Cancelling...`); this.status = 'cancelling'; workflow.upsertMemo({ status: this.status, lastUpdated: new Date().toISOString() }); this.forwardSignalToChildren('cancel'); } else { this.log.info(`Already cancelling...`); } } constructor(args, options) { super(); this.args = args; this.options = options; this.args = args; this.options = options; if (args && typeof args === 'object') { this.status = this.args?.status ?? 'running'; this.dsl = this.args?.dsl ?? undefined; this.startDelay = this.args?.startDelay ?? undefined; } } conditionTimeout = undefined; isInTerminalState() { return ['complete', 'completed', 'cancel', 'cancelling', 'cancelled', 'error', 'erroring', 'errored'].includes(this.status); } interpreter; currentGeneration; async execute(...args) { if (this.dsl && this.interpreter) { try { if (this.isContinueable) { if (!this.currentGeneration) { const next = await this.interpreter.next(); if (next.done) { this.status = 'completed'; return this.result; } this.currentGeneration = next.value; } const result = await this.currentGeneration.execute(); const next = await this.interpreter.next(); if (next.done) { this.currentGeneration = undefined; this.status = 'completed'; } else { this.currentGeneration = next.value; } return result; } else { let result; let next; do { if (!this.currentGeneration) { next = await this.interpreter.next(); if (next.done) { this.status = 'completed'; return this.result; } this.currentGeneration = next.value; } result = await this.currentGeneration.execute(); next = await this.interpreter.next(); if (next.done) { this.currentGeneration = undefined; this.status = 'completed'; } else { this.currentGeneration = next.value; } if (this.status !== 'running') { break; } } while (this.currentGeneration); return result; } } catch (error) { this.log.error(`Error executing generation ${this.currentGeneration?.nodeId}: ${error}`); throw error; } } return undefined; } hasDSLNodesToExecute() { return Boolean(this.dsl && this.interpreter && this.currentGeneration); } async executeWorkflow(...args) { this.log.trace(`executeWorkflow`); if (this.startDelay) await workflow.sleep(this.startDelay); try { while (this.iteration <= this.maxIterations) { if (this.pendingIteration) { this.pendingIteration = false; } await workflow.condition(() => (typeof this.condition === 'function' && this.condition()) || this.hasDSLNodesToExecute() || this.pendingIteration || this.pendingUpdate || this.continueAsNew || this.status !== 'running', this.conditionTimeout ?? undefined); if (this.status === 'paused') { await this.emitAsync('paused'); await this.forwardSignalToChildren('pause'); await workflow.condition(() => this.status !== 'paused'); } if (this.status === 'cancelled') { break; } else { this.result = await Promise.resolve().then(() => this.execute()); } if (this.isInTerminalState()) { if (this.status !== 'errored') { return this.result; } throw this.result; } if (++this.iteration >= this.maxIterations || (workflow.workflowInfo().continueAsNewSuggested && workflow.workflowInfo().historySize >= this.maxHistorySize) || this.continueAsNew) { await this.handleMaxIterations(); break; } if (this.pendingUpdate) { this.pendingUpdate = false; } if (this.pendingIteration) { this.pendingIteration = false; } } return this.result; } catch (err) { await this.handleExecutionError(err, (error) => { throw error; }); throw err; } } async handleMaxIterations() { await workflow.condition(() => workflow.allHandlersFinished(), '30 seconds'); const continueFn = workflow.makeContinueAsNewFunc({ workflowType: String(this.workflowType), memo: workflow.workflowInfo().memo, searchAttributes: workflow.workflowInfo().searchAttributes }); if (typeof this.onContinue === 'function') { const customArgs = await this.onContinue(); await continueFn(customArgs || this.args); } else { throw new Error(`No onContinue method found in ${this.constructor.name}. Cannot continue as new.`); } } async handleExecutionError(err, reject) { this.log.error(`Error encountered: ${err?.message}: ${err?.stack}`); if (workflow.isCancellation(err)) { this.log.debug(`Cancelling...`); await workflow.CancellationScope.nonCancellable(async () => { this.status = 'cancelling'; await Promise.all(Array.from(this.handles.values()).flatMap((handles) => Object.values(handles).map((handle) => async () => { try { const extHandle = workflow.getExternalWorkflowHandle(handle.workflowId); await extHandle.cancel(); } catch (error) { this.log.error(`Failed to cancel child workflow:`, error); } }))); this.status = 'cancelled'; }); reject(err); } else if (!(err instanceof workflow.ContinueAsNew)) { this.log.error(`Handling non-cancellation error: ${err?.message} \n ${err.stack}`); reject(err); } } emit(event, ...args) { throw new Error('Workflows need to be deterministic, emitAsync should be used instead!'); } async emitAsync(event, ...args) { const listeners = this.listeners(event); for (const listener of listeners) { try { await Promise.resolve().then(() => listener.apply(this, args)); } catch (error) { console.error(`Error in listener for event '${event}':`, error); } } return listeners.length > 0; } cleanup() { try { this.removeAllListeners(); this.queryHandlers = {}; this.signalHandlers = {}; this.stepHandlers = {}; this.interpreter = undefined; this.currentGeneration = undefined; this._eventsBound = false; this._hooksBound = false; this._signalsBound = false; this._queriesBound = false; this._propertiesBound = false; this._stepsBound = false; } catch (error) { this.log.error(`Error cleaning up event listeners: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async forwardSignalToChildren(signalName, ...args) { for (const handle of this.handles.values()) { try { await Promise.resolve().then(() => handle.signal(signalName, ...args)); } catch (error) { this.log.error(`Failed to forward signal '${signalName}' to child workflow:`, error); } } } async bindEventHandlers() { if (this._eventsBound) { return; } const eventHandlers = this.collectMetadata(decorators_1.EVENTS_METADATA_KEY, this.constructor.prototype); eventHandlers.forEach((handler) => { this.on(handler.event, async (...args) => { const method = this[handler.method]; if (typeof method === 'function') { await Promise.resolve().then(() => method.apply(this, args)); } }); }); this._eventsBound = true; } async bindHooks() { if (this._hooksBound) { return; } const hooks = this.collectHookMetadata(this.constructor.prototype); if (!hooks) return; for (const methodName of Object.keys(hooks)) { const originalMethod = this[methodName]; if (typeof originalMethod === 'function') { this[methodName] = this.createHookedMethod(methodName, originalMethod, hooks[methodName]); } } this._hooksBound = true; } createHookedMethod(methodName, originalMethod, hookConfig) { return async (...args) => { const { before = [], after = [] } = hookConfig; for (const beforeHook of before) { if (typeof this[beforeHook] === 'function') { this.log.trace(`[HOOK]:(${beforeHook}):before(${methodName})`); await Promise.resolve().then(() => this[beforeHook].call(this, ...args)); } } let result; try { result = await Promise.resolve().then(() => originalMethod.call(this, ...args)); } catch (err) { throw err instanceof Error ? err : new Error(String(err)); } for (const afterHook of after) { if (typeof this[afterHook] === 'function') { this.log.trace(`[HOOK]:(${afterHook}):after(${methodName})`); await Promise.resolve().then(() => this[afterHook].call(this, ...args)); } } return result; }; } async bindProperties() { if (this._propertiesBound) { return; } const properties = this.collectMetadata(decorators_1.PROPERTY_METADATA_KEY, this.constructor.prototype); properties.forEach(({ propertyKey, get: g, set: s, queryName, signalName }) => { if (g) { const getter = () => this[propertyKey]; this.queryHandlers[queryName] = getter.bind(this); workflow.setHandler(workflow.defineQuery(typeof g === 'string' ? g : queryName), getter); } if (s) { const setter = (value) => (this[propertyKey] = value); this.signalHandlers[signalName] = setter.bind(this); workflow.setHandler(workflow.defineSignal(typeof g === 'string' ? s : signalName), setter); } }); this._propertiesBound = true; } bindQueries() { if (this._queriesBound) { return; } const queries = this.collectMetadata(decorators_1.QUERY_METADATA_KEY, this.constructor.prototype); for (const [queryName, queryMethod, options] of queries) { if (typeof this[queryMethod] === 'function') { const originalHandler = this[queryMethod].bind(this); const wrappedHandler = (...args) => { try { this.log.trace(`[QUERY]:(${queryName})`); this.emitAsync(queryName).catch((err) => this.log.error(`[QUERY]:(${queryName}):emit error`, err)); return originalHandler(...args); } catch (err) { if (options?.onError) { try { return options.onError(err); } catch (errorHandlerErr) { this.log.error(`[QUERY]:(${queryName}):error handler threw`, errorHandlerErr); throw errorHandlerErr; } } else { this.log.error(`[QUERY]:(${queryName}):error`, err); throw err; } } }; this.queryHandlers[queryName] = wrappedHandler; workflow.setHandler(workflow.defineQuery(queryName), wrappedHandler); } } this._queriesBound = true; } bindSignals() { if (this._signalsBound) { return; } const signals = this.collectMetadata(decorators_1.SIGNAL_METADATA_KEY, this.constructor.prototype); for (const [signalName, signalMethod, options] of signals) { if (typeof this[signalMethod] === 'function') { const originalHandler = this[signalMethod].bind(this); const wrappedHandler = async (...args) => { try { this.log.trace(`[SIGNAL]:(${signalName})`); await this.emitAsync(signalName); return await originalHandler(...args); } catch (err) { if (options?.onError) { try { const result = options.onError(err); if (result instanceof Promise) { return await result; } return result; } catch (errorHandlerErr) { this.log.error(`[SIGNAL]:(${signalName}):error handler threw`, errorHandlerErr); throw errorHandlerErr; } } else { this.log.error(`[SIGNAL]:(${signalName}):error`, err); throw err; } } }; this.signalHandlers[signalName] = wrappedHandler; workflow.setHandler(workflow.defineSignal(signalName), wrappedHandler); } } this._signalsBound = true; } bindSteps() { if (this._stepsBound) { return; } const steps = this.collectMetadata(decorators_1.STEP_METADATA_KEY, this.constructor.prototype); if (steps.length) { for (const step of steps) { if (typeof this[step.method] === 'function') { const handler = this[step.method].bind(this); this.stepHandlers[step.name] = async (...args) => { return await handler(...args); }; } } if (!this.dsl) { this.dsl = (0, DSLInterpreter_1.convertStepsToDSL)(steps, {}, this); } } this._stepsBound = true; } initDSL() { if (this.dsl && !this.interpreter) { Object.defineProperty(this.dsl.variables, 'workflow', { get: () => this, set: () => { throw new Error('workflow is read-only'); }, enumerable: false, configurable: false }); this.interpreter = (0, DSLInterpreter_1.DSLInterpreter)(this.dsl, this.options?.activities, this.stepHandlers); } } collectMetadata(metadataKey, target) { const collectedMetadata = []; const seen = new Set(); let currentProto = target; while (currentProto && currentProto !== Object.prototype) { const metadata = Reflect.getMetadata(metadataKey, currentProto) ?? []; for (const item of metadata) { const itemString = JSON.stringify(item); if (!seen.has(itemString)) { seen.add(itemString); collectedMetadata.push(item); } } currentProto = Object.getPrototypeOf(currentProto); } return collectedMetadata; } collectHookMetadata(target) { const collectedMetadata = {}; const protoChain = []; let currentProto = target; while (currentProto && currentProto !== Workflow.prototype) { protoChain.unshift(currentProto); currentProto = Object.getPrototypeOf(currentProto); } for (const proto of protoChain) { const metadata = Reflect.getOwnMetadata(decorators_1.HOOKS_METADATA_KEY, proto) ?? {}; for (const key of Object.keys(metadata)) { if (!collectedMetadata[key]) { collectedMetadata[key] = { before: [], after: [] }; } collectedMetadata[key].before.push(...(metadata[key].before ?? [])); collectedMetadata[key].after.push(...(metadata[key].after ?? [])); } } return collectedMetadata; } } exports.Workflow = Workflow; __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object) ], Workflow.prototype, "dsl", void 0); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object) ], Workflow.prototype, "startDelay", void 0); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", Object) ], Workflow.prototype, "isContinueable", void 0); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object) ], Workflow.prototype, "continueAsNew", void 0); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", Object) ], Workflow.prototype, "maxIterations", void 0); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", Object) ], Workflow.prototype, "maxHistorySize", void 0); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", Object) ], Workflow.prototype, "iteration", void 0); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Boolean) ], Workflow.prototype, "pendingIteration", void 0); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", String) ], Workflow.prototype, "status", void 0); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object) ], Workflow.prototype, "pendingUpdate", void 0); __decorate([ (0, decorators_1.Signal)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "pause", null); __decorate([ (0, decorators_1.Signal)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "resume", null); __decorate([ (0, decorators_1.Signal)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "cancel", null); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object) ], Workflow.prototype, "conditionTimeout", void 0); __decorate([ (0, decorators_1.On)('hooks'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Workflow.prototype, "bindHooks", null); __decorate([ (0, decorators_1.On)('init'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Workflow.prototype, "bindProperties", null); __decorate([ (0, decorators_1.On)('init'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "bindQueries", null); __decorate([ (0, decorators_1.On)('init'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "bindSignals", null); __decorate([ (0, decorators_1.On)('init'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "bindSteps", null); __decorate([ (0, decorators_1.On)('init'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], Workflow.prototype, "initDSL", null);