yoonite-saga
Version:
> Orchestration de workflows transactionnels avec gestion de compensation (pattern Saga)
137 lines • 4.73 kB
JavaScript
"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