@ferrox-node/cqrs 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ export declare class CqrsEngine {
2
+ private handlers;
3
+ registerCommandHandler(type: string, handler: (cmd: any) => Promise<any>): void;
4
+ executeCommand(cmd: any): Promise<any>;
5
+ }
6
+ export declare class SagaOrchestrator {
7
+ private steps;
8
+ addStep(step: any): void;
9
+ execute(): Promise<{
10
+ success: boolean;
11
+ }>;
12
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SagaOrchestrator = exports.CqrsEngine = void 0;
4
+ class CqrsEngine {
5
+ handlers = new Map();
6
+ registerCommandHandler(type, handler) {
7
+ this.handlers.set(type, handler);
8
+ }
9
+ async executeCommand(cmd) {
10
+ const handler = this.handlers.get(cmd.type);
11
+ if (!handler)
12
+ throw new Error('No handler');
13
+ return handler(cmd);
14
+ }
15
+ }
16
+ exports.CqrsEngine = CqrsEngine;
17
+ class SagaOrchestrator {
18
+ steps = [];
19
+ addStep(step) { this.steps.push(step); }
20
+ async execute() { return { success: true }; }
21
+ }
22
+ exports.SagaOrchestrator = SagaOrchestrator;
@@ -0,0 +1,20 @@
1
+ export interface SagaTransaction {
2
+ id: string;
3
+ payload: any;
4
+ }
5
+ export interface SagaStep {
6
+ name: string;
7
+ execute: (tx: SagaTransaction) => Promise<any>;
8
+ compensate: (tx: SagaTransaction, executeResult?: any) => Promise<void>;
9
+ }
10
+ export declare class AcidSagaCoordinator {
11
+ private steps;
12
+ /**
13
+ * Adds a step to the Distributed Transaction (SAGA)
14
+ */
15
+ addStep(name: string, execute: (tx: SagaTransaction) => Promise<any>, compensate: (tx: SagaTransaction, res?: any) => Promise<void>): this;
16
+ /**
17
+ * Executes the transaction. If any step fails, triggers the compensation (rollback) flow in reverse order.
18
+ */
19
+ execute(tx: SagaTransaction): Promise<void>;
20
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AcidSagaCoordinator = void 0;
4
+ const logger_1 = require("@node-yalc/logger");
5
+ const logger = (0, logger_1.AppLoggerFactory)('SagaCoordinator');
6
+ class AcidSagaCoordinator {
7
+ steps = [];
8
+ /**
9
+ * Adds a step to the Distributed Transaction (SAGA)
10
+ */
11
+ addStep(name, execute, compensate) {
12
+ this.steps.push({ name, execute, compensate });
13
+ return this;
14
+ }
15
+ /**
16
+ * Executes the transaction. If any step fails, triggers the compensation (rollback) flow in reverse order.
17
+ */
18
+ async execute(tx) {
19
+ const executedSteps = [];
20
+ logger.log(`[SAGA] Starting ACID Transaction ${tx.id} with ${this.steps.length} steps`);
21
+ for (const step of this.steps) {
22
+ try {
23
+ logger?.debug?.(`[SAGA] Executing step: ${step.name}`);
24
+ const result = await step.execute(tx);
25
+ executedSteps.push({ step, result });
26
+ }
27
+ catch (error) {
28
+ logger.error(`[SAGA] Transaction ${tx.id} failed at step '${step.name}'. Initiating compensation...`);
29
+ // Rollback sequentially in reverse
30
+ for (let i = executedSteps.length - 1; i >= 0; i--) {
31
+ const { step: rollbackStep, result } = executedSteps[i];
32
+ try {
33
+ logger.warn(`[SAGA] Compensating step: ${rollbackStep.name}`);
34
+ await rollbackStep.compensate(tx, result);
35
+ }
36
+ catch (compError) {
37
+ logger.error(`[SAGA] CRITICAL: Compensation failed for step '${rollbackStep.name}': ${compError.message}`);
38
+ // In a real CQRS, you would send this to a Dead Letter Queue or trigger manual intervention alerts
39
+ }
40
+ }
41
+ throw new Error(`SAGA Transaction ${tx.id} aborted due to failure in step '${step.name}': ${error.message}`);
42
+ }
43
+ }
44
+ logger.log(`[SAGA] Transaction ${tx.id} completed successfully`);
45
+ }
46
+ }
47
+ exports.AcidSagaCoordinator = AcidSagaCoordinator;
@@ -0,0 +1,2 @@
1
+ export * from './cqrs/cqrs-saga';
2
+ export * from './cqrs/cqrs-engine';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./cqrs/cqrs-saga"), exports);
18
+ __exportStar(require("./cqrs/cqrs-engine"), exports);
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@ferrox-node/cqrs",
3
+ "version": "1.1.1",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "scripts": {
7
+ "build": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "@ferrox-node/core": "*"
11
+ },
12
+ "devDependencies": {
13
+ "typescript": "^5.0.0"
14
+ }
15
+ }
Binary file
@@ -0,0 +1,62 @@
1
+ import { AppLoggerFactory } from '@node-yalc/logger';
2
+
3
+ const logger = AppLoggerFactory('SagaCoordinator');
4
+
5
+ export interface SagaTransaction {
6
+ id: string;
7
+ payload: any;
8
+ }
9
+
10
+ export interface SagaStep {
11
+ name: string;
12
+ execute: (tx: SagaTransaction) => Promise<any>;
13
+ compensate: (tx: SagaTransaction, executeResult?: any) => Promise<void>;
14
+ }
15
+
16
+ export class AcidSagaCoordinator {
17
+ private steps: SagaStep[] = [];
18
+
19
+ /**
20
+ * Adds a step to the Distributed Transaction (SAGA)
21
+ */
22
+ addStep(name: string, execute: (tx: SagaTransaction) => Promise<any>, compensate: (tx: SagaTransaction, res?: any) => Promise<void>): this {
23
+ this.steps.push({ name, execute, compensate });
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Executes the transaction. If any step fails, triggers the compensation (rollback) flow in reverse order.
29
+ */
30
+ async execute(tx: SagaTransaction): Promise<void> {
31
+ const executedSteps: { step: SagaStep; result: any }[] = [];
32
+
33
+ logger.log(`[SAGA] Starting ACID Transaction ${tx.id} with ${this.steps.length} steps`);
34
+
35
+ for (const step of this.steps) {
36
+ try {
37
+ logger?.debug?.(`[SAGA] Executing step: ${step.name}`);
38
+ const result = await step.execute(tx);
39
+ executedSteps.push({ step, result });
40
+ } catch (error: any) {
41
+ logger.error(`[SAGA] Transaction ${tx.id} failed at step '${step.name}'. Initiating compensation...`);
42
+
43
+ // Rollback sequentially in reverse
44
+ for (let i = executedSteps.length - 1; i >= 0; i--) {
45
+ const { step: rollbackStep, result } = executedSteps[i];
46
+ try {
47
+ logger.warn(`[SAGA] Compensating step: ${rollbackStep.name}`);
48
+ await rollbackStep.compensate(tx, result);
49
+ } catch (compError: any) {
50
+ logger.error(`[SAGA] CRITICAL: Compensation failed for step '${rollbackStep.name}': ${compError.message}`);
51
+ // In a real CQRS, you would send this to a Dead Letter Queue or trigger manual intervention alerts
52
+ }
53
+ }
54
+
55
+ throw new Error(`SAGA Transaction ${tx.id} aborted due to failure in step '${step.name}': ${error.message}`);
56
+ }
57
+ }
58
+
59
+ logger.log(`[SAGA] Transaction ${tx.id} completed successfully`);
60
+ }
61
+ }
62
+
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './cqrs/cqrs-saga';
2
+ export * from './cqrs/cqrs-engine';
@@ -0,0 +1,76 @@
1
+ import { AcidSagaCoordinator } from '../src/cqrs/cqrs-saga';
2
+
3
+ describe('AcidSagaCoordinator', () => {
4
+ it('should execute all steps successfully in order', async () => {
5
+ const saga = new AcidSagaCoordinator();
6
+ const trace: string[] = [];
7
+
8
+ saga.addStep(
9
+ 'step1',
10
+ async () => { trace.push('step1-exec'); },
11
+ async () => { trace.push('step1-comp'); }
12
+ );
13
+ saga.addStep(
14
+ 'step2',
15
+ async () => { trace.push('step2-exec'); },
16
+ async () => { trace.push('step2-comp'); }
17
+ );
18
+
19
+ await saga.execute({ id: 'tx-1', payload: {} });
20
+
21
+ expect(trace).toEqual(['step1-exec', 'step2-exec']);
22
+ });
23
+
24
+ it('should compensate backward in case of step failure', async () => {
25
+ const saga = new AcidSagaCoordinator();
26
+ const trace: string[] = [];
27
+
28
+ saga.addStep(
29
+ 'ChargeCard',
30
+ async () => { trace.push('charged'); return { amount: 100 }; },
31
+ async () => { trace.push('refunded'); }
32
+ );
33
+ saga.addStep(
34
+ 'BookInventory',
35
+ async () => { trace.push('booked'); },
36
+ async () => { trace.push('unbooked'); }
37
+ );
38
+ saga.addStep(
39
+ 'ShipItem',
40
+ async () => { throw new Error('Warehouse offline'); },
41
+ async () => { trace.push('unshipped'); }
42
+ );
43
+
44
+ await expect(saga.execute({ id: 'tx-2', payload: {} })).rejects.toThrow('Warehouse offline');
45
+
46
+ // Expected: It charged, booked, then failed to ship.
47
+ // So it must UNbook first, then REFUND.
48
+ expect(trace).toEqual(['charged', 'booked', 'unbooked', 'refunded']);
49
+ });
50
+
51
+ it('should continue compensation even if a compensation step fails', async () => {
52
+ const saga = new AcidSagaCoordinator();
53
+ const trace: string[] = [];
54
+
55
+ saga.addStep(
56
+ 'Step1',
57
+ async () => { trace.push('s1'); },
58
+ async () => { trace.push('c1'); }
59
+ );
60
+ saga.addStep(
61
+ 'Step2',
62
+ async () => { trace.push('s2'); },
63
+ async () => { throw new Error('Compensation error'); }
64
+ );
65
+ saga.addStep(
66
+ 'Step3',
67
+ async () => { throw new Error('Execution error'); },
68
+ async () => { trace.push('c3'); }
69
+ );
70
+
71
+ await expect(saga.execute({ id: 'tx-3', payload: {} })).rejects.toThrow('Execution error');
72
+ // Step1 and Step2 executed. Step3 failed.
73
+ // Compensation: Step2 throws error, Step1 continues compensation!
74
+ expect(trace).toEqual(['s1', 's2', 'c1']);
75
+ });
76
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../core/tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "baseUrl": ".",
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ]
11
+ }
12
+
13
+
14
+
15
+
16
+
17
+
18
+