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

1,041 lines 52.5 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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StatefulWorkflow = void 0; const dottie_1 = __importStar(require("dottie")); const workflow = __importStar(require("@temporalio/workflow")); const store_1 = require("../store"); const deep_object_diff_1 = require("deep-object-diff"); const lodash_1 = require("lodash"); const Workflow_1 = require("./Workflow"); const decorators_1 = require("../decorators"); const SchemaManager_1 = require("../store/SchemaManager"); const StateManager_1 = require("../store/StateManager"); const limitRecursion_1 = require("../utils/limitRecursion"); const getCompositeKey_1 = require("../utils/getCompositeKey"); const common_1 = require("@temporalio/common"); const flatten_1 = require("../utils/flatten"); const utils_1 = require("../utils"); const ramda_1 = require("ramda"); const mnemonist_1 = require("mnemonist"); class StatefulWorkflow extends Workflow_1.Workflow { _actionsBound = false; _eventsBound = false; selectorPatternCache = new Map(); getSelectorPattern(selector) { let pattern = this.selectorPatternCache.get(selector); if (!pattern) { pattern = new RegExp('^' + selector.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); this.selectorPatternCache.set(selector, pattern); } return pattern; } _memoProperties = {}; actionRunning = false; continueAsNew = true; shouldContinueAsNew = false; stateManager; schemaManager = SchemaManager_1.SchemaManager.getInstance(); iteration = 0; schema; conditionTimeout = undefined; shouldLoadData() { return typeof this?.loadData === 'function' && this.pendingUpdate; } apiToken; setApiToken(apiToken) { this.log.trace(`setApiToken`); if (apiToken && this.apiToken !== apiToken) { this.log.debug(`Updating apiToken...`); this.apiToken = apiToken; this.pendingUpdate = true; this.forwardSignalToChildren('apiToken', apiToken); } } id; entityName; getState() { return this.state; } get state() { return this.stateManager.state; } async setState(state) { await this.stateManager.setState(state); } get data() { return this.stateManager.query(this.entityName, this.id, true); } set data(data) { if ((0, lodash_1.isObject)(data) && this.data) { Object.assign(this.data, data); } else { this.setState((0, store_1.normalizeEntities)(data, this.entityName)); } } pendingUpdate = true; get pendingChanges() { return this.stateManager.queue; } subscriptions = []; subscriptionHandles = new mnemonist_1.LRUCacheWithDelete(100); managedPaths = {}; apiUrl; ancestorWorkflowIds = []; params; options; ancestorHandles = new mnemonist_1.LRUCacheWithDelete(100); constructor(params, options) { super(params, options); this.params = params; this.options = options; this.options.saveStateToMemo = this.options.saveStateToMemo === true; this.stateManager = StateManager_1.StateManager.getInstance(workflow.workflowInfo().workflowId); if (this.params?.ancestorWorkflowIds) { this.ancestorWorkflowIds = this.params.ancestorWorkflowIds; for (const workflowId of this.ancestorWorkflowIds) { const [entityName, entityId] = workflowId.split(/-(.*)/s).slice(0, 2); this.ancestorHandles.set(workflowId, { entityId, entityName, handle: workflow.getExternalWorkflowHandle(workflowId), isParent: workflow.workflowInfo().parent?.workflowId === workflowId }); } } this.id = this.params?.id; this.entityName = (this.params?.entityName || options?.schemaName); this.schema = this.entityName ? this.schemaManager.getSchema(this.entityName) : options.schema; this.stateManager.on('stateChange', this.stateChanged.bind(this)); this.pendingUpdate = typeof this?.loadData === 'function'; this.pendingIteration = true; const memo = (0, utils_1.unflatten)(workflow.workflowInfo()?.memo ?? {}); if (!(0, lodash_1.isEmpty)(memo?.properties ?? {})) { this._memoProperties = memo.properties; } if (memo?.state && !(0, lodash_1.isEmpty)(memo.state)) { this.stateManager.state = memo.state; } this.apiUrl = this.params?.apiUrl ?? options?.apiUrl; this.apiToken = this.params?.apiToken; } async onSetup() { if (this.params?.subscriptions) { for (const subscription of this.params.subscriptions) { await this.subscribe(subscription); } } if (this.params?.state && !(0, lodash_1.isEmpty)(this.params?.state)) { await this.stateManager.dispatch((0, store_1.setState)(this.params.state), false, workflow.workflowInfo().workflowId); } if (this.params?.data && !(0, lodash_1.isEmpty)(this.params?.data)) { await this.stateManager.dispatch((0, store_1.updateEntity)(this.params.data, this.entityName), false, workflow.workflowInfo().workflowId); } } async executeWorkflow() { this.log.trace(`executeWorkflow`); if (this.schema) this.configureManagedPaths(this.schema); if (this.params?.startDelay) await workflow.sleep(this.params.startDelay); try { while (this.iteration <= this.maxIterations) { if (this.pendingIteration) { this.pendingIteration = false; } await workflow.condition(() => (typeof this.condition === 'function' && this.condition()) || this.pendingIteration || this.pendingUpdate || !!this.pendingChanges.length || this.shouldContinueAsNew || this.status !== 'running', !this.conditionTimeout ? undefined : this.conditionTimeout); this.log.trace(`executeWorkflow: ${this.iteration}`); if (this.status === 'paused') { await this.emitAsync('paused'); await this.forwardSignalToChildren('pause'); await workflow.condition(() => this.status !== 'paused' || this.isInTerminalState()); } if (!this.isInTerminalState() && this.shouldLoadData()) { await this.loadDataAndEnqueue(); } if (!this.isInTerminalState()) { await this.processState(); this.result = await this.execute(); } if (this.pendingUpdate) { this.pendingUpdate = false; } if (!this.isInTerminalState() && this.pendingChanges.length) { this.log.warn(`Pending changes after processing: ${this.pendingChanges.length}`); await workflow.sleep(2500); continue; } if (this.isInTerminalState()) { if (this.status !== 'errored') { return this.result || this.data; } throw this.result; } if (++this.iteration >= this.maxIterations || workflow.workflowInfo().historyLength >= this.maxIterations || (workflow.workflowInfo().continueAsNewSuggested && workflow.workflowInfo().historySize >= 25000000) || this.shouldContinueAsNew) { await this.handleMaxIterations(); break; } await this.upsertToMemo(); } return this.result ?? this.data; } catch (err) { if (err instanceof workflow.ContinueAsNew) { throw err; } await this.handleExecutionError(err, (error) => { throw error; }); throw err; } } update({ data, updates, entityName, action, strategy = '$merge', changeOrigin, sync = true }) { this.log.trace(`update`); if (action) { this.stateManager.dispatch(action, sync, changeOrigin); } else { if (data !== null && !(0, lodash_1.isEmpty)(data)) { updates = (0, store_1.normalizeEntities)(data, entityName === this.entityName ? this.schema : SchemaManager_1.SchemaManager.getInstance().getSchema(entityName)); } if (updates) { this.stateManager.dispatch((0, store_1.updateNormalizedEntities)(updates, strategy), sync, changeOrigin); } else { this.log.error(`Invalid Update: ${JSON.stringify(data, null, 2)}, \n${JSON.stringify(updates, null, 2)}`); } } } delete({ data, action, deletions, entityName, changeOrigin, sync = true }) { this.log.trace(`delete`); if (action) { this.stateManager.dispatch(action, sync, changeOrigin); } else { if (!(0, lodash_1.isEmpty)(data)) { deletions = (0, store_1.normalizeEntities)(data, entityName === this.entityName ? this.schema : SchemaManager_1.SchemaManager.getInstance().getSchema(entityName)); } if (deletions) { this.stateManager.dispatch((0, store_1.deleteNormalizedEntities)(deletions), sync, changeOrigin); } else { this.log.error(`Invalid Delete: ${JSON.stringify(data, null, 2)}, \n${JSON.stringify(deletions, null, 2)}`); } } } async subscribe(subscription) { this.log.trace(`subscribe`); const { workflowId, subscriptionId } = subscription; if (!this.subscriptions.find((sub) => sub.workflowId === workflowId && sub.subscriptionId === subscriptionId)) { this.subscriptions.push(subscription); this.subscriptionHandles.set(workflowId, workflow.getExternalWorkflowHandle(workflowId)); } } async unsubscribe(subscription) { this.log.trace(`unsubscribe`); const { workflowId, subscriptionId } = subscription; const index = this.subscriptions.findIndex((sub) => sub.workflowId === workflowId && sub.subscriptionId === subscriptionId); if (index !== -1) { this.subscriptionHandles.delete(workflowId); this.subscriptions.splice(index, 1); } } async processState() { this.log.debug(`processState`); if (this.actionRunning) { this.log.trace(`Action is running, waiting for all changes to be made before processing...`); await workflow.condition(() => !this.actionRunning); } if (this.stateManager.processing) { this.log.trace(`State manager is already processing, waiting for it to finish...`); await workflow.condition(() => !this.stateManager.processing); } if (this.pendingChanges.length) { await this.stateManager.processChanges(); } } async stateChanged({ newState, previousState, differences, changeOrigins }) { this.log.trace(`stateChanged`); if (!(0, lodash_1.isEmpty)(differences.added) || !(0, lodash_1.isEmpty)(differences.updated) || !(0, lodash_1.isEmpty)(differences.deleted)) { const created = (0, dottie_1.get)(differences.added, `${this.entityName}.${this.id}`, false); const updated = (0, dottie_1.get)(differences.updated, `${this.entityName}.${this.id}`, false); const deleted = (0, dottie_1.get)(differences.deleted, `${this.entityName}.${this.id}`, false); if (created && this.iteration === 0) { await this.emitAsync('created', { changes: created, newState, previousState, changeOrigins }); } else if (created || updated) { await this.emitAsync('updated', { changes: (0, ramda_1.mergeDeepRight)(created || {}, updated || {}), newState, previousState, changeOrigins }); } else if (deleted && this.isItemDeleted(differences, this.entityName, this.id)) { if (!(await this.emitAsync('deleted', { changes: deleted, newState, previousState, changeOrigins }))) { this.status = 'cancelled'; } } await this.processChildState(newState, differences, previousState || {}, changeOrigins); let previousStateForSubscriptions = {}; if (this.iteration !== 0) { previousStateForSubscriptions = previousState || {}; } else if (this.params.state) { previousStateForSubscriptions = this.params.state; } else if (this.params.data) { previousStateForSubscriptions = (0, store_1.normalizeEntities)(this.params.data, this.schema); } await this.processSubscriptions(newState, previousStateForSubscriptions === previousState ? differences : (0, deep_object_diff_1.detailedDiff)(previousStateForSubscriptions, newState), previousStateForSubscriptions, changeOrigins); this.pendingIteration = true; } } async upsertToMemo() { const memo = (workflow.workflowInfo().memo || {}); const flattenedCurrentMemo = (0, flatten_1.flatten)({ status: memo?.status, state: memo?.state ?? {}, properties: memo?.properties ?? {} }); const flattenedNewMemo = (0, flatten_1.flatten)({ status: this.status, state: this.options?.saveStateToMemo ? this.state : {}, properties: this._memoProperties ?? {} }); const updatedMemo = {}; let hasChanges = (0, lodash_1.isEmpty)(memo); for (const [key, newValue] of Object.entries(flattenedNewMemo)) { const currentValue = flattenedCurrentMemo[key]; if (!(0, lodash_1.isEqual)(newValue, currentValue)) { updatedMemo[key] = newValue; hasChanges = true; } } for (const key of Object.keys(flattenedCurrentMemo)) { if (!(key in flattenedNewMemo)) { updatedMemo[key] = undefined; hasChanges = true; } } if (hasChanges) { this.log.info(`Saving state to memo`); workflow.upsertMemo({ ...updatedMemo, iteration: this.iteration, lastUpdated: new Date().toISOString() }); } } async processSubscriptions(newState, differences, previousState, changeOrigins) { this.log.trace(`processSubscriptions`); for (const subscription of this.subscriptions) { const { workflowId, selector, condition, subscriptionId, signalName, sync, strategy } = subscription; if (signalName !== 'update' && signalName !== 'delete') { continue; } const shouldPropagate = this.shouldPropagate(newState, differences, selector, condition, changeOrigins, this.ancestorWorkflowIds); if (!shouldPropagate) { continue; } const { updates, deletions } = this.extractChangesForSelector(differences, selector, newState, previousState); const handle = this.subscriptionHandles.get(workflowId); if (handle) { try { if ((signalName === 'update' && !(0, lodash_1.isEmpty)(updates)) || (signalName === 'delete' && !(0, lodash_1.isEmpty)(deletions))) { this.log.trace(`Sending ${signalName} to workflow: ${workflowId}, Subscription ID: ${subscriptionId}`); this.log.trace(JSON.stringify({ updates, deletions }, null, 2)); await handle.signal(signalName, { ...(signalName === 'update' ? { updates } : { deletions }), changeOrigin: workflow.workflowInfo().workflowId, subscriptionId, sync, strategy }); } } catch (err) { this.log.error(`Failed to signal workflow '${workflowId}': ${err.message}`); } } } } shouldPropagate(newState, differences, selector, condition, changeOrigins, ancestorWorkflowIds = []) { if (changeOrigins) { for (const origin of changeOrigins) { if (origin && ancestorWorkflowIds.includes(origin)) { this.log.trace(`Skipping propagation for selector ${selector} because the change originated from an ancestor workflow (${origin}).`); return false; } } } const selectorRegex = this.getSelectorPattern(selector); for (const diffType of ['added', 'updated']) { const entities = differences[diffType]; if (!entities) continue; for (const [entityName, entityChanges] of Object.entries(entities)) { for (const [entityId, entityData] of Object.entries(entityChanges)) { for (const key in entityData) { if (Object.prototype.hasOwnProperty.call(entityData, key)) { const path = `${entityName}.${entityId}.${key}`; if (selectorRegex.test(path)) { const selectedData = (0, dottie_1.get)(newState, path); if (condition && !condition(selectedData)) { this.log.trace(`Custom condition for selector ${selector} not met.`); continue; } this.log.trace(`Differences detected that match selector ${selector}, propagation allowed.`); return true; } else { const selectorParts = selector.split('.'); const keyParts = path.split('.'); let isParent = true; for (let i = 0; i < selectorParts.length; i++) { if (selectorParts[i] === '*') { continue; } if (selectorParts[i] !== keyParts[i]) { isParent = false; break; } } if (isParent) { const parentKey = selector.split('.').slice(-1)[0]; const selectedData = (0, dottie_1.get)(newState, `${entityName}.${entityId}.${parentKey}`); if (condition && !condition(selectedData)) { this.log.trace(`Custom condition for selector ${selector} not met.`); continue; } this.log.trace(`Differences detected within selector ${selector}, propagation allowed.`); return true; } } } } } } } this.log.trace(`No matching differences found, conditions not met, or ancestry conflicts for selector: ${selector}`); return false; } extractChangesForSelector(differences, selector, newState, previousState) { const updates = {}; const deletions = {}; let selectorRegex = this.getSelectorPattern(selector); for (const diffType of ['added', 'updated', 'deleted']) { const entities = differences[diffType]; if (!entities) continue; for (const [entityName, entityChanges] of Object.entries(entities)) { for (const entityId in entityChanges) { const entityPath = `${entityName}.${entityId}`; if (diffType === 'deleted') { if (!(0, dottie_1.get)(newState, entityPath) && selectorRegex.test(entityPath)) { if (!deletions[entityName]) { deletions[entityName] = {}; } deletions[entityName][entityId] = (0, dottie_1.get)(previousState, entityPath); } } else { const entityData = entityChanges[entityId]; for (const key in entityData) { if (Object.prototype.hasOwnProperty.call(entityData, key)) { const path = `${entityPath}.${key}`; if (selectorRegex.test(path)) { if (!updates[entityName]) { updates[entityName] = {}; } if (!updates[entityName][entityId]) { updates[entityName][entityId] = {}; } updates[entityName][entityId][key] = (0, dottie_1.get)(newState, path); } } } } } } } return { updates, deletions }; } async processChildState(newState, differences, previousState, changeOrigins) { this.log.trace(`processChildState`); const newData = (0, limitRecursion_1.limitRecursion)(this.id, this.entityName, newState, this.stateManager); const oldData = (0, limitRecursion_1.limitRecursion)(this.id, this.entityName, previousState); for (const [_, config] of Object.entries(this.managedPaths)) { const currentValue = (0, dottie_1.get)(newData, config.path); const previousValue = (0, dottie_1.get)(oldData, config.path); if (!config.autoStart && typeof config.condition !== 'function') { continue; } if (Array.isArray(currentValue)) { await this.processArrayItems(config, newState, previousState, differences, changeOrigins, currentValue, previousValue); } else if (currentValue) { await this.processItem(config, newState, previousState, differences, changeOrigins, currentValue, previousValue); } if (Array.isArray(previousValue)) { for (const item of previousValue) { const compositeId = Array.isArray(config.idAttribute) ? (0, getCompositeKey_1.getCompositeKey)(item, config.idAttribute) : item[config.idAttribute]; if (this.isItemDeleted(differences, String(config.entityName), compositeId)) { this.log.trace(`Processing deleted item in ${config.entityName}`); await this.handleDeletion(config, compositeId); } } } else if (previousValue) { const itemId = previousValue[config.idAttribute]; if (this.isItemDeleted(differences, String(config.entityName), itemId)) { this.log.trace(`Processing deleted item in ${config.entityName}`); await this.handleDeletion(config, itemId); } } } } async processItem(config, newState, previousState, differences, changeOrigins, newItem = {}, previousItem = {}) { const compositeId = Array.isArray(config.idAttribute) ? (0, getCompositeKey_1.getCompositeKey)(newItem, config.idAttribute) : newItem[config.idAttribute]; await this.processSingleItem(config, compositeId, newState, previousState, differences, changeOrigins, newItem, previousItem); } isItemDeleted(differences, entityName, itemId) { const deletedEntitiesByName = (0, dottie_1.get)(differences, `deleted.${entityName}`, {}); const keyExistsInDeletions = Object.keys(deletedEntitiesByName).includes(itemId); if (keyExistsInDeletions) { if (deletedEntitiesByName[itemId] === undefined) { return true; } } return false; } async handleDeletion(config, compositeId) { const workflowId = config.includeParentId ? `${config.entityName}-${compositeId}-${this.id}` : `${config.entityName}-${compositeId}`; const handle = this.handles.get(workflowId); if (handle) { await this.cancelChildWorkflow(handle); } } async processArrayItems(config, newState, previousState, differences, changeOrigins, items = [], previousItems = []) { this.log.trace(`processArrayItems`); for (let i = 0; i < items.length; i++) { const newItem = items[i]; const itemId = Array.isArray(config.idAttribute) ? (0, getCompositeKey_1.getCompositeKey)(newItem, config.idAttribute) : newItem[config.idAttribute]; const previousItem = previousItems.find((item) => { const previousItemId = Array.isArray(config.idAttribute) ? (0, getCompositeKey_1.getCompositeKey)(item, config.idAttribute) : item[config.idAttribute]; return previousItemId === itemId; }); await this.processSingleItem(config, itemId, newState, previousState, differences, changeOrigins, newItem, previousItem); if ((i + 1) % 100 === 0 && i < items.length - 1) { this.log.debug(`Processed 100 items, pausing for 1 second before continuing...`); await workflow.sleep(2500); } } } async processSingleItem(config, itemId, newState, previousState, differences, changeOrigins, newItem, previousItem) { this.log.trace(`processSingleItem(${itemId})`); const compositeId = Array.isArray(config.idAttribute) ? (0, getCompositeKey_1.getCompositeKey)(newState[config.entityName][itemId], config.idAttribute) : itemId; if (!itemId) { this.log.warn(`No itemId found for ${config.entityName} with id ${compositeId}`); return; } const workflowId = config.includeParentId ? `${config.entityName}-${compositeId}-${this.id}` : `${config.entityName}-${compositeId}`; if (this.ancestorWorkflowIds.includes(workflowId)) { this.log.warn(`Circular dependency detected for workflowId: ${workflowId}. Skipping child workflow start.`); return; } if (changeOrigins.includes(workflowId)) { this.log.trace(`Skipping recursive update...`); return; } const existingHandle = this.handles.get(workflowId); const hasStateChanged = !(0, lodash_1.isEqual)(previousItem, newItem); if (hasStateChanged || !existingHandle) { if (existingHandle && 'result' in existingHandle) { await this.updateChildWorkflow(config, existingHandle, newItem, newState, previousState, differences); } else if (!existingHandle && !(0, lodash_1.isEmpty)(differences)) { await this.startChildWorkflow(config, itemId, workflowId, newItem, newState); } } } configureManagedPaths(parentSchema) { this.log.trace(`configureManagedPaths`); if (!parentSchema.schema) { throw new Error("The provided schema does not have 'schema' defined."); } const defaultSubscriptionConfig = { enabled: true, sync: false, strategy: '$merge', selector: '*' }; const defaultManagedPathConfig = { autoStart: typeof this.options?.children?.autoStart === 'boolean' ? this.options?.children?.autoStart : true, strategy: '$merge', subscriptions: { update: defaultSubscriptionConfig, delete: defaultSubscriptionConfig } }; const childSchemas = parentSchema.schema; for (const [path, _schema] of Object.entries(childSchemas)) { const schema = _schema instanceof Array ? _schema[0] : _schema; this.managedPaths[path] = { ...defaultManagedPathConfig, path, idAttribute: schema._idAttribute, workflowType: `${schema._key}Workflow`, entityName: schema._key, isMany: _schema instanceof Array, ...(this.managedPaths[path] || {}), subscriptions: { update: { ...defaultSubscriptionConfig, ...(this.managedPaths[path]?.subscriptions?.update || {}) }, delete: { ...defaultSubscriptionConfig, ...(this.managedPaths[path]?.subscriptions?.delete || {}) } } }; } } async startChildWorkflow(config, id, workflowId, newItem, newState) { try { if (id === undefined && typeof newItem === 'string') { id = newItem; newItem = newState[config.entityName][newItem]; } const { workflowType, entityName, cancellationType = workflow.ChildWorkflowCancellationType.WAIT_CANCELLATION_REQUESTED, parentClosePolicy = workflow.ParentClosePolicy.TERMINATE, workflowIdConflictPolicy = workflow.WorkflowIdConflictPolicy.TERMINATE_EXISTING, workflowIdReusePolicy = workflow.WorkflowIdReusePolicy.ALLOW_DUPLICATE, workflowTaskTimeout = 30000, startDelay = 1000, startToCloseTimeout = '30 days', retry = { initialInterval: 1000 * 1, maximumInterval: 1000 * 20, backoffCoefficient: 1.5, maximumAttempts: 30 } } = config; this.log.trace(`startChildWorkflow( path=${config.path}, entityName=${entityName}, id=${id}, workflowType=${workflowType}, data: ${JSON.stringify(newItem, null, 2)} )`); const data = typeof config.processData === 'function' ? config.processData((0, limitRecursion_1.limitRecursion)(id, String(entityName), newState, this.stateManager), (0, limitRecursion_1.limitRecursion)(this.id, this.entityName, newState, this.stateManager)) : newItem; if (typeof config.condition === 'function') { if (!config.condition.apply(this, [newItem, this])) { this.log.trace(`Condition returned false, not starting child.`); return; } } const subscriptions = []; if (config.subscriptions?.update?.enabled) { subscriptions.push({ subscriptionId: `${this.entityName}:${this.id}.${config.path}:${id}`, workflowId: workflow.workflowInfo().workflowId, signalName: 'update', selector: config.subscriptions.update.selector ?? '*', parent: `${this.entityName}:${this.id}`, child: `${config.entityName}:${id}`, ancestorWorkflowIds: [...this.ancestorWorkflowIds], sync: config.subscriptions.update.sync, strategy: config.subscriptions.update.strategy }); } if (config.subscriptions?.delete?.enabled) { subscriptions.push({ subscriptionId: `${this.entityName}:${this.id}.${config.path}:${id}:delete`, workflowId: workflow.workflowInfo().workflowId, signalName: 'delete', selector: config.subscriptions.delete.selector ?? '*', parent: `${this.entityName}:${this.id}`, child: `${config.entityName}:${id}`, ancestorWorkflowIds: [...this.ancestorWorkflowIds], sync: config.subscriptions.delete.sync, strategy: config.subscriptions.delete.strategy }); } const startPayload = { workflowId, cancellationType, parentClosePolicy, workflowIdConflictPolicy, workflowIdReusePolicy, startToCloseTimeout, workflowTaskTimeout, startDelay, args: [ { id, state: (0, store_1.normalizeEntities)(data, SchemaManager_1.SchemaManager.getInstance().getSchema(entityName)), entityName, subscriptions, apiToken: this.apiToken, ancestorWorkflowIds: [...this.ancestorWorkflowIds, workflow.workflowInfo().workflowId], startDelay } ], retry }; const handle = await workflow.startChild(String(workflowType), startPayload); this.handles.set(workflowId, handle); if (this.listenerCount(`child:${entityName}:started`) > 0) { await this.emitAsync(`child:${entityName}:started`, { ...config, workflowId, data, handle }); } Promise.resolve() .then(() => handle.result()) .then(async (result) => { await this.emitAsync(`child:${entityName}:completed`, { ...config, workflowId, result }); }) .catch(async (error) => { if (workflow.isCancellation(error) && this.status !== 'cancelled' && this.status !== 'cancelling' && this.handles.has(workflowId)) { this.log.info(`Restarting child workflow due to cancellation...`); Promise.resolve() .then(() => this.startChildWorkflow(config, id, workflowId, this.stateManager.query(String(entityName), id, false), this.state)) .catch((retryError) => { this.log.error(`Retry failed: ${retryError.message}`); }); } else { this.log.error(`Child workflow error: ${error.message}\n${error.stack}`); await this.emitAsync(`child:${entityName}:errored`, { ...config, workflowId, error }); } }); } catch (err) { if (err instanceof Error) { this.log.error(`Failed to start new child workflow: ${err.message}\n${err.stack}`); } else { this.log.error(`An unknown error occurred while starting a new child workflow`); } throw err; } } async updateChildWorkflow(config, handle, newItem, newState, previousState, differences) { this.log.trace(`updateChildWorkflow`); try { const { entityName } = config; const updateConfig = this.options.children?.update || {}; const deleteConfig = this.options.children?.delete || { enabled: false }; if (updateConfig.enabled === false && deleteConfig.enabled === false) { this.log.debug(`Child updates are disabled for workflowId: ${handle.workflowId}. Skipping child workflow update.`); return; } if (typeof config.condition === 'function') { if (!config.condition.apply(this, [newItem, this])) { this.log.trace(`Condition returned false, not updating child.`); return; } } const { updates, deletions } = this.extractChangesForSelector(differences, '*', newState, previousState); if (!(0, lodash_1.isEmpty)(updates) && (updateConfig.enabled || updateConfig.enabled === undefined)) { this.log.trace(`Signaling update to child workflow: ${handle.workflowId} \n ${JSON.stringify(updates, null, 2)}`); const updateSignalPayload = { updates: Object.entries(updates).reduce((acc, [entityName, entities]) => { acc[entityName] = Object.entries(entities).reduce((entityAcc, [id, entity]) => { entityAcc[id] = { id, ...entity }; return entityAcc; }, {}); return acc; }, {}), changeOrigin: workflow.workflowInfo().workflowId }; if (typeof updateConfig.strategy === 'string' && updateConfig.strategy !== '$merge') { updateSignalPayload.strategy = updateConfig.strategy; } if (typeof updateConfig.sync === 'boolean') { updateSignalPayload.sync = updateConfig.sync; } await handle.signal('update', updateSignalPayload); } if (!(0, lodash_1.isEmpty)(deletions) && (deleteConfig.enabled || deleteConfig.enabled === undefined)) { this.log.trace(`Signaling delete to child workflow: ${handle.workflowId}\n ${JSON.stringify(deletions, null, 2)}`); const deleteSignalPayload = { deletions, changeOrigin: workflow.workflowInfo().workflowId }; if (typeof deleteConfig.sync === 'boolean') { deleteSignalPayload.sync = deleteConfig.sync; } await handle.signal('delete', deleteSignalPayload); } if (this.listenerCount(`child:${entityName}:updated`) > 0) await this.emitAsync(`child:${entityName}:updated`, { ...config, workflowId: handle.workflowId, data: newItem, changeOrigin: workflow.workflowInfo().workflowId, differences }); } catch (err) { if (err instanceof Error) { this.log.error(`Failed to signal existing workflow handle: ${err.message}`); } else { this.log.error(`An unknown error occurred while signaling the child workflow`); } } } async cancelChildWorkflow(handle) { this.log.trace(`Cancelling child workflow: ${handle.workflowId}`); try { if (handle) { this.handles.delete(handle.workflowId); const extHandle = workflow.getExternalWorkflowHandle(handle.workflowId); await extHandle.cancel(); } } catch (error) { this.log.error(`Failed to cancel from workflow handle: ${error.message}`); } } 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 }); const defaultParams = { state: this.state, status: this.status, subscriptions: this.subscriptions, ...Object.keys(this.params).reduce((params, key) => ({ ...params, [key]: this[key] }), {}) }; if (typeof this.onContinue === 'function') { const customParams = await this.onContinue(); await continueFn(customParams || defaultParams); } else { await continueFn(defaultParams); } } async loadDataAndEnqueue() { if (typeof this?.loadData === 'function') { let { data, updates, strategy = '$merge' } = await this.loadData(); if (!data && !updates) { this.log.debug(`No data or updates returned from loadData(), skipping state change...`); return; } if (data && !updates) { updates = (0, store_1.normalizeEntities)(data, this.entityName); } if (updates) { await this.stateManager.dispatch((0, store_1.updateNormalizedEntities)(updates, strategy), false); } } } async bindProperties() { await super.bindProperties(); const properties = this.collectMetadata(decorators_1.PROPERTY_METADATA_KEY, this.constructor.prototype); properties.forEach(({ propertyKey, path, memo }) => { const isStringPath = typeof path === 'string'; const useMemoPath = memo === true && isStringPath; const memoKeyString = typeof memo === 'string'; const resolveMemoKey = () => (useMemoPath ? path : memo); if (memo || memoKeyString) { this._memoProperties[propertyKey] = Reflect.get(this, propertyKey); } if (isStringPath || memoKeyString) { Object.defineProperty(this, propertyKey, { get: () => { if (useMemoPath || memoKeyString) { return this._memoProperties ? this._memoProperties[resolveMemoKey()] : undefined; } else if (isStringPath) { return dottie_1.default.get(this.data || {}, path); } return undefined; }, set: (value) => { if (useMemoPath || memoKeyString) { dottie_1.default.set(this._memoProperties, resolveMemoKey(), value); this.pendingIteration = true; } if (isStringPath) { dottie_1.default.set(this.data || (this.data = {}), path, value); } }, configurable: false, enumerable: true }); } }); } async bindActions() { if (this._actionsBound) { return; } const actions = Reflect.getMetadata(decorators_1.ACTIONS_METADATA_KEY, this.constructor.prototype) || []; const validators = Reflect.getMetadata(decorators_1.VALIDATOR_METADATA_KEY, this.constructor.prototype) || {}; for (const { method } of actions) { const methodName = method; const updateOptions = {}; const validatorMethod = validators[methodName]; if (validatorMethod) { updateOptions.validator = this[validatorMethod].bind(this); } updateOptions.unfinishedPolicy = common_1.HandlerUnfinishedPolicy.ABANDON; workflow.setHandler(workflow.defineUpdate(method), (input) => this.runAction(methodName, input), updateOptions); } this._actionsBound = true; } async bindEventHandlers() { if (this._eventsBound) { return; } const eventHandlers = this.collectMetadata(decorators_1.EVENTS_METADATA_KEY, this.constructor.prototype); eventHandlers.forEach((handler) => { (handler.event.startsWith('state:') ? this.stateManager : this).on(handler.event.replace(/^state:/, ''), async (...args) => { if (typeof this[handler.method] === 'function') { await Promise.resolve().then(() => this[handler.method](...args)); } }); }); this._eventsBound = true; } async runAction(methodName, input) { this.actionRunning = true; let result; let error; try { result = await this[methodName].call(this, input); } catch (err) { error = err; this.log.error(error); } finally { this.actionRunning = false; this.pendingIteration = true; } await workflow.condition(() => !this.stateManager.processing && !this.pendingIteration); if (error !== undefined) { return Promise.reject(!(error instanceof Error) ? new Error(error) : error); } return result !== undefined ? result : this.data; } cleanup() { try { this._actionsBound = false; if (this.stateManager) { this.stateManager.removeListener('stateChange', this.stateChanged.bind(this)); this.stateManager.cleanup(); } this.selectorPatternCache.clear(); this.subscriptionHandles.clear(); this.ancestorHandles.clear(); super.cleanup(); } catch (error) { this.log.error(`Error cleaning up event listeners: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.StatefulWorkflow = StatefulWorkflow; __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Boolean) ], StatefulWorkflow.prototype, "shouldContinueAsNew", void 0); __decorate([ (0, decorators_1.Property)({ set: false, memo: 'apiToken' }), __metadata("design:type", String) ], StatefulWorkflow.prototype, "apiToken", void 0); __decorate([ (0, decorators_1.Signal)('apiToken'), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", void 0) ], StatefulWorkflow.prototype, "setApiToken", null); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", String) ], StatefulWorkflow.prototype, "id", void 0); __decorate([ (0, decorators_1.Property)({ set: false }), __metadata("design:type", String) ], StatefulWorkflow.prototype, "entityName", void 0); __decorate([ (0, decorators_1.Query)('state'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], StatefulWorkflow.prototype, "getState", null); __decorate([ (0, decorators_1.Signal)('state'), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], StatefulWorkflow.prototype, "setState", null); __decorate([ (0, decorators_1.Property)(), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], StatefulWorkflow.prototype, "data", null); __decorate([ (0, decorators_1.Property)(), __metadata("design:type"