UNPKG

yoonite-saga

Version:

> Orchestration de workflows transactionnels avec gestion de compensation (pattern Saga)

137 lines 4.73 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SagaProcessor = void 0; const class_transformer_1 = require("class-transformer"); const class_validator_1 = require("class-validator"); class SagaProcessor { constructor(services = {}) { this.steps = []; this.history = []; this.services = {}; this.ctx = {}; this.currentStep = 0; this.toCompensate = []; this.errors = []; this.currentExecution = null; this.errorFormatter = (err) => err; this.services = services; } add(workflow) { this.steps = [...this.steps, ...workflow]; return this; } handleExceptions(formatter) { this.errorFormatter = formatter; } async start() { this.debug("Run workflow"); await this.runStep(); return this.formatResponse(); } async runStep() { const currentStep = this.steps[this.currentStep]; const { name, condition, validate, invokes, withCompensation } = currentStep; try { if (this.checkCondition(condition)) { if (validate) await this.validate(validate); if (withCompensation) this.toCompensate.push(withCompensation); this.debug("\tStart step:", name); await invokes.reduce((acc, curr) => acc.then(() => this.runInvoke(curr)), Promise.resolve()); } await this.makeStepForward(this.currentStep + 1); } catch (err) { this.errors.push(this.errorFormatter(err)); this.endLog("error"); await this.compensate(); } } async runInvoke(invoke) { const { name, condition, action, withCompensation } = invoke; if (!this.checkCondition(condition)) return; if (withCompensation) this.toCompensate.push(withCompensation); this.debug("\t\tRun invoke:", name); this.startLog(name); const response = await action(this.ctx, this.services); this.ctx = Object.assign(Object.assign({}, this.ctx), response); this.endLog("success"); } async validate(dto) { if (!dto) return; if (typeof dto !== "function") { throw new Error("DTO for validation must be a class constructor"); } const objInstance = (0, class_transformer_1.plainToInstance)(dto, this.ctx || {}); const errors = (await (0, class_validator_1.validate)(objInstance)) || []; if (errors.length > 0) { errors.map(({ property, constraints = {} }) => { this.errors.push(new Error(`Validation failed on "${property}" : ` + Object.values(constraints).join(", "))); }); throw new Error("Stop saga"); } } checkCondition(condition) { if (!condition) return true; return condition(Object.assign({}, this.ctx)); } async makeStepForward(index) { if (index >= this.steps.length) { return this.ctx; } this.currentStep = index; return this.runStep(); } async compensate() { await this.toCompensate.reverse().reduce(async (acc, cur) => acc .then(() => cur(this.ctx, this.services)) .then((response = {}) => { this.ctx = Object.assign(Object.assign({}, this.ctx), response); }) .catch((err) => { this.errors.push(err); return; }), Promise.resolve()); } async formatResponse() { const hasErrors = this.errors.length > 0; return { state: hasErrors ? "failed" : "success", context: this.ctx, errors: this.errors, history: this.history, }; } startLog(name) { const currentStep = this.steps[this.currentStep]; const execution = { step: this.currentStep, name: currentStep.name, invoke: name, startAt: new Date(), endAt: 0, state: "pending", }; this.currentExecution = execution; } endLog(state) { if (!this.currentExecution) return; this.currentExecution.state = state; this.currentExecution.endAt = new Date(); this.history.push(Object.assign({}, this.currentExecution)); } debug(...params) { const currentStep = this.steps[this.currentStep]; if (currentStep.debug) console.log(...params); } } exports.SagaProcessor = SagaProcessor; //# sourceMappingURL=saga-processor.js.map