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
JavaScript
"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);