symflow
Version:
SymFlow is a powerful workflow and state machine engine for Node.js, inspired by Symfony Workflow. It allows you to define workflows, transition entities between states, and optionally log audit trails.
223 lines • 10.8 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Symflow = void 0;
const events_1 = require("events");
const event_workflow_1 = require("./event-workflow");
const audit_trail_1 = require("./audit-trail");
const workflow_loader_1 = require("./workflow-loader");
class Symflow {
constructor(workflow, emitter) {
var _a, _b;
this.eventHandlers = {};
this.forkSiblingMap = {};
const definition = typeof workflow === 'string' ? (0, workflow_loader_1.loadWorkflowDefinition)(workflow) : workflow;
this.metadata = definition.metadata || {};
this.places = definition.places;
this.transitions = definition.transitions;
this.stateField = definition.stateField;
this.isStateMachine = definition.type === 'state_machine';
this.workflowName = definition.name;
this.auditEnabled =
typeof definition.auditTrail === 'boolean'
? definition.auditTrail
: ((_b = (_a = definition.auditTrail) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : false);
this.emitter = emitter || new events_1.EventEmitter();
if (definition.events) {
for (const [eventType, handlers] of Object.entries(definition.events)) {
this.eventHandlers[eventType] = handlers;
}
}
for (const transition of Object.values(this.transitions)) {
if (Array.isArray(transition.to) && transition.to.length > 1) {
for (const to of transition.to) {
this.forkSiblingMap[to] = transition.to.filter((s) => s !== to);
}
}
}
}
getEmitter() {
return this.emitter;
}
getMetadata() {
return this.metadata;
}
getAvailableTransitions(entity) {
const currentStates = this.getCurrentStates(entity);
return Object.keys(this.transitions).filter((transition) => this.matchFromStates(currentStates, this.transitions[transition].from));
}
getAvailableTransition(state) {
return Object.keys(this.transitions).filter((transition) => this.matchFromStates([state], this.transitions[transition].from));
}
canTransition(entity_1, transition_1) {
return __awaiter(this, arguments, void 0, function* (entity, transition, shouldTriggerGuard = false) {
var _a;
const currentStates = this.getCurrentStates(entity);
const fromState = (_a = this.transitions[transition]) === null || _a === void 0 ? void 0 : _a.from;
if (!this.matchFromStates(currentStates, fromState)) {
return false;
}
if (shouldTriggerGuard) {
return yield this.triggerEvent(event_workflow_1.WorkflowEventType.GUARD, entity, transition, currentStates, this.transitions[transition].to, true);
}
return true;
});
}
on(eventType, handler) {
if (!this.eventHandlers[eventType]) {
this.eventHandlers[eventType] = [];
}
this.eventHandlers[eventType].push(handler);
}
triggerEvent(eventType_1, entity_1, transition_1, fromState_1, toState_1) {
return __awaiter(this, arguments, void 0, function* (eventType, entity, transition, fromState, toState, silent = false) {
var _a, _b;
const metadata = ((_a = this.transitions[transition]) === null || _a === void 0 ? void 0 : _a.metadata) || {};
const eventPayload = { entity, transition, fromState, toState, metadata };
yield audit_trail_1.AuditTrail.logEvent(this.workflowName, {
entityId: entity.id,
eventType,
transition,
fromState,
toState,
metadata,
timestamp: new Date().toISOString(),
}, !silent && this.auditEnabled);
const eventTypeKey = eventType.toLowerCase();
const eventNames = [
`symflow.${eventTypeKey}`,
`symflow.${this.workflowName}.${eventTypeKey}`,
`symflow.${this.workflowName}.${eventTypeKey}.${transition}`,
];
for (const name of eventNames) {
this.emitter.emit(name, eventPayload);
}
let allowTransition = true;
if (eventType === event_workflow_1.WorkflowEventType.GUARD) {
for (const handler of this.eventHandlers[eventType] || []) {
if (handler(eventPayload) === false) {
allowTransition = false;
break;
}
}
}
else {
(_b = this.eventHandlers[eventType]) === null || _b === void 0 ? void 0 : _b.forEach((handler) => handler(eventPayload));
}
return allowTransition;
});
}
applyTransition(entity, transition, newState) {
return __awaiter(this, void 0, void 0, function* () {
const fromState = this.getCurrentStates(entity);
yield this.triggerEvent(event_workflow_1.WorkflowEventType.ANNOUNCE, entity, transition, fromState, newState);
if (!(yield this.triggerEvent(event_workflow_1.WorkflowEventType.GUARD, entity, transition, fromState, newState))) {
throw new Error(`❌ Transition "${transition}" blocked by Guard event.`);
}
yield this.triggerEvent(event_workflow_1.WorkflowEventType.LEAVE, entity, transition, fromState, newState);
yield this.triggerEvent(event_workflow_1.WorkflowEventType.ENTER, entity, transition, fromState, newState);
if (this.isStateMachine) {
entity[this.stateField] = (Array.isArray(newState) ? newState[0] : newState);
}
else {
const toStates = Array.isArray(newState) ? newState : [newState];
const currentStates = this.getCurrentStates(entity);
const fromStates = this.collectRecursiveFromStates(toStates);
const forkSiblings = toStates.flatMap((to) => this.forkSiblingMap[to] || []);
const toRemove = new Set([...fromStates, ...forkSiblings]);
const keptStates = currentStates.filter((state) => !toRemove.has(state));
const nextStates = [...new Set([...keptStates, ...toStates])];
entity[this.stateField] = nextStates;
}
yield this.triggerEvent(event_workflow_1.WorkflowEventType.TRANSITION, entity, transition, fromState, newState);
yield this.triggerEvent(event_workflow_1.WorkflowEventType.COMPLETED, entity, transition, fromState, newState);
yield this.triggerEvent(event_workflow_1.WorkflowEventType.ENTERED, entity, transition, fromState, newState);
});
}
apply(entity, transition) {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield this.canTransition(entity, transition, false))) {
throw new Error(`Transition "${transition}" is not allowed from state "${entity[this.stateField]}".`);
}
yield this.applyTransition(entity, transition, this.transitions[transition].to);
});
}
getCurrentStates(entity) {
return Array.isArray(entity[this.stateField])
? entity[this.stateField]
: [entity[this.stateField]];
}
matchFromStates(currentStates, fromStates) {
if (typeof fromStates === 'string') {
return currentStates.includes(fromStates);
}
if (Array.isArray(fromStates)) {
if (fromStates.length > 1) {
return fromStates.every((state) => currentStates.includes(state));
}
return fromStates.some((state) => currentStates.includes(state));
}
return false;
}
collectRecursiveFromStates(toStates) {
const allFromStates = new Set();
const visited = new Set();
const recurse = (currentTo) => {
if (visited.has(currentTo))
return;
visited.add(currentTo);
for (const transition of Object.values(this.transitions)) {
const transitionTo = Array.isArray(transition.to) ? transition.to : [transition.to];
if (transitionTo.includes(currentTo)) {
const from = Array.isArray(transition.from) ? transition.from : [transition.from];
from.forEach((f) => {
allFromStates.add(f);
recurse(f);
});
}
}
};
toStates.forEach(recurse);
return allFromStates;
}
toGraphviz() {
let dot = `digraph Workflow {\n`;
for (const [state] of Object.entries(this.places)) {
dot += ` "${state}" [label="${state}"];\n`;
}
for (const [transition, { from, to }] of Object.entries(this.transitions)) {
const fromStates = Array.isArray(from) ? from : [from];
const toStates = Array.isArray(to) ? to : [to];
fromStates.forEach((fromState) => {
toStates.forEach((toState) => {
dot += ` "${fromState}" -> "${toState}" [label="${transition}"];\n`;
});
});
}
dot += `}`;
return dot;
}
toMermaid() {
let mermaid = `graph TD;\n`;
for (const [transition, { from, to }] of Object.entries(this.transitions)) {
const fromStates = Array.isArray(from) ? from : [from];
const toStates = Array.isArray(to) ? to : [to];
fromStates.forEach((fromState) => {
toStates.forEach((toState) => {
mermaid += ` ${fromState} -->|${transition}| ${toState};\n`;
});
});
}
return mermaid;
}
}
exports.Symflow = Symflow;
//# sourceMappingURL=symflow.js.map