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,040 lines • 53.1 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);
};
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");
const sanitizePropertyKey_1 = require("../utils/sanitizePropertyKey");
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;
isContinueable = true;
continueAsNew = 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.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.pendingChanges.length ||
this.continueAsNew ||
this.status !== 'running', this.conditionTimeout ?? undefined);
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 >= this.maxHistorySize) ||
this.continueAsNew) {
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, option }) => {
const isStringPath = typeof path === 'string';
const useMemoPath = memo === true && isStringPath;
const memoKeyString = typeof memo === 'string';
const optionKey = typeof option === 'string';
const resolveMemoKey = () => {
const key = useMemoPath ? path : memo;
return typeof key === 'string' ? (0, sanitizePropertyKey_1.sanitizePropertyKey)(key) : key;
};
if (memo || memoKeyString) {
this._memoProperties[resolveMemoKey()] = Reflect.get(this, propertyKey);
}
if (isStringPath || memoKeyString) {
Object.defineProperty(this, propertyKey, {
get: () => {
if (useMemoPath || memoKeyString) {
return dottie_1.default.get(this._memoProperties, resolveMemoKey());
}
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
});
if (optionKey && option in this.options) {
this[propertyKey] = this.options[option];
}
}
});
}
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;
this.actionRunning = false;
this.pendingIteration = false;
this.pendingUpdate = false;
this.iteration = 0;
this.selectorPatternCache.clear();
this.subscriptionHandles.clear();
this.ancestorHandles.clear();
this.ancestorWorkflowIds.length = 0;
this._memoProperties = {};
this.subscriptions.length = 0;
this.stateManager.cleanup();
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, "continueAsNew", 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", []),
_