smart-saga-pattern
Version:
A library implementing the Saga pattern for microservice architecture
159 lines (158 loc) • 5.84 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StructuredSagaExecutor = void 0;
const types_1 = require("../types");
/**
* Executes a Saga definition with structured context
* The context is structured as:
* {
* global: { ... },
* steps: {
* "Step Name": {
* input: { ... },
* output: { ... },
* compensation: { ... }
* }
* }
* }
* @template T Type of the input data
* @template R Type of the result data
*/
class StructuredSagaExecutor {
/**
* Creates a new StructuredSagaExecutor
* @param definition Saga definition to execute
* @param context Initial Saga context
*/
constructor(definition, context) {
this.currentStepIndex = -1;
this.definition = definition;
this.context = { ...context };
// Initialize steps object if it doesn't exist
if (!this.hasStepsObject()) {
this.initializeStepsObject();
}
}
/**
* Checks if the context has a steps object
*/
hasStepsObject() {
return this.context.data && typeof this.context.data === 'object' &&
'steps' in this.context.data && typeof this.context.data.steps === 'object';
}
/**
* Initializes the steps object in the context
*/
initializeStepsObject() {
if (!this.context.data) {
this.context.data = {};
}
this.context.data.steps = {};
}
/**
* Executes the Saga
*/
async execute() {
const steps = this.definition.getSteps();
try {
this.context.status = types_1.SagaStatus.EXECUTING;
for (let i = 0; i < steps.length; i++) {
this.currentStepIndex = i;
const step = steps[i];
try {
// Execute the transaction with the requiresCompensation flag
const requiresCompensation = step.requiresCompensation !== undefined
? step.requiresCompensation
: true;
// Initialize step data if it doesn't exist
this.initializeStepData(step.transaction.getName());
// Execute the transaction
const result = await step.transaction.execute(this.context.data, requiresCompensation);
// Store the result in the step's output field
this.updateStepOutput(step.transaction.getName(), result);
}
catch (error) {
// Transaction failed, start compensation
await this.compensate();
return this.context;
}
}
// All steps completed successfully
this.context.status = types_1.SagaStatus.COMPLETED;
this.context.endTime = Date.now();
return this.context;
}
catch (error) {
// Something went wrong during execution or compensation
this.context.status = types_1.SagaStatus.FAILED;
this.context.error = error instanceof Error ? error.message : String(error);
this.context.endTime = Date.now();
return this.context;
}
}
/**
* Initializes step data in the context
* @param stepName Name of the step
*/
initializeStepData(stepName) {
const steps = this.context.data.steps;
if (!steps[stepName]) {
steps[stepName] = {
input: {},
output: {},
compensation: {}
};
}
}
/**
* Updates step output in the context
* @param stepName Name of the step
* @param output Output data
*/
updateStepOutput(stepName, output) {
const steps = this.context.data.steps;
if (steps[stepName]) {
steps[stepName].output = output;
}
}
/**
* Compensates the Saga
*/
async compensate() {
var _a;
const steps = this.definition.getSteps();
this.context.status = types_1.SagaStatus.COMPENSATING;
try {
// Compensate steps in reverse order
for (let i = this.currentStepIndex; i >= 0; i--) {
const step = steps[i];
// Only compensate if the step requires compensation
if (step.compensation && (step.requiresCompensation !== false)) {
try {
const stepName = step.transaction.getName();
const steps = this.context.data.steps;
// Get the transaction output from the step's output field
const transactionOutput = (_a = steps[stepName]) === null || _a === void 0 ? void 0 : _a.output;
// Execute compensation with transaction output and name
await step.compensation.execute(this.context.data, transactionOutput, step.transaction.getName());
}
catch (error) {
// Compensation failed
this.context.status = types_1.SagaStatus.COMPENSATION_FAILED;
this.context.error = error instanceof Error ? error.message : String(error);
this.context.endTime = Date.now();
throw error;
}
}
}
// All compensations completed successfully
this.context.status = types_1.SagaStatus.COMPENSATED;
this.context.endTime = Date.now();
}
catch (error) {
// Re-throw the error to be caught by the execute method
throw error;
}
}
}
exports.StructuredSagaExecutor = StructuredSagaExecutor;