• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

snatalenko / node-cqrs / 21873137262

10 Feb 2026 04:23PM UTC coverage: 85.173%. First build
21873137262

Pull #31

github

web-flow
Merge 8c5d5890b into 025edb883
Pull Request #31: Multi-saga correlation via `message.sagaOrigins`

657 of 993 branches covered (66.16%)

143 of 158 new or added lines in 17 files covered. (90.51%)

1258 of 1477 relevant lines covered (85.17%)

33.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.59
/src/AbstractSaga.ts
1
import {
28✔
2
        type ICommand,
3
        type ICommandBus,
4
        type Identifier,
5
        type IEvent,
6
        type IEventStore,
7
        type IMutableState,
8
        type ISaga,
9
        type ISagaConstructor,
10
        type ISagaConstructorParams,
11
        isEvent
12
} from './interfaces/index.ts';
13
import { SagaEventHandler } from './SagaEventHandler.ts';
28✔
14
import {
28✔
15
        validateHandlers,
16
        getHandler,
17
        promiseOrSync,
18
        getClassName,
19
        getMessageHandlerNames
20
} from './utils/index.ts';
21

22
/**
23
 * Base class for Saga definition
24
 */
25
export abstract class AbstractSaga implements ISaga {
28✔
26

27
        /**
28
         * Optional list of events that start new saga.
29
         *
30
         * When not defined, saga start is inferred by the absence of `message.sagaOrigins[sagaDescriptor]`.
31
         */
32
        static get startsWith(): string[] | undefined {
33
                return undefined;
10✔
34
        }
35

36
        /** List of event types being handled by Saga, can be overridden in Saga implementation */
37
        static get handles(): string[] {
38
                return getMessageHandlerNames(this);
50✔
39
        }
40

41
        /**
42
         * Convenience helper to create a `SagaEventHandler` for this saga type and subscribe it to
43
         * the provided `eventStore`.
44
         */
45
        static register<T extends AbstractSaga>(
46
                this: ISagaConstructor & (new (options: ISagaConstructorParams) => T),
47
                eventStore: IEventStore,
48
                commandBus: ICommandBus
49
        ): SagaEventHandler {
50
                const handler = new SagaEventHandler({
2✔
51
                        sagaType: this,
52
                        eventStore,
53
                        commandBus
54
                });
55
                handler.subscribe(eventStore);
2✔
56
                return handler;
2✔
57
        }
58

59
        /** Saga ID */
60
        get id(): Identifier {
61
                return this.#id;
2✔
62
        }
63

64
        /** Saga version */
65
        get version(): number {
66
                return this.#version;
6✔
67
        }
68

69
        protected state?: IMutableState | object;
70

71
        #id: Identifier;
72
        #version = 0;
56✔
73
        #messages: ICommand[] = [];
56✔
74
        #handling = false;
56✔
75

76
        /**
77
         * Creates an instance of AbstractSaga
78
         */
79
        constructor(options: ISagaConstructorParams) {
80
                if (!options)
56!
81
                        throw new TypeError('options argument required');
×
82
                if (!options.id)
56!
83
                        throw new TypeError('options.id argument required');
×
84
                if (options.events)
56!
NEW
85
                        throw new TypeError('options.events argument is deprecated');
×
86

87
                this.#id = options.id;
56✔
88

89
                validateHandlers(this, 'startsWith');
56✔
90
                validateHandlers(this, 'handles');
54✔
91
        }
92

93
        /** Modify saga state by applying an event */
94
        mutate(event: IEvent): void {
95
                if (!isEvent(event))
50!
NEW
96
                        throw new TypeError('event argument must be a valid IEvent');
×
97

98
                if (this.state) {
50✔
99
                        const handler = 'mutate' in this.state ?
6✔
100
                                this.state.mutate :
101
                                getHandler(this.state, event.type);
102
                        if (handler)
6✔
103
                                handler.call(this.state, event);
6✔
104
                }
105

106
                this.#version += 1;
50✔
107
        }
108

109
        /** Process saga event and return produced commands */
110
        handle(event: IEvent): ReadonlyArray<ICommand> | Promise<ReadonlyArray<ICommand>> {
111
                if (!isEvent(event))
36!
NEW
112
                        throw new TypeError('event argument must be a valid IEvent');
×
113
                if (this.#handling)
36✔
114
                        throw new Error('Another event is being processed, concurrent handling is not allowed');
2✔
115

116
                const handler = getHandler(this, event.type);
34✔
117
                if (!handler)
34!
118
                        throw new Error(`'${event.type}' handler is not defined or not a function`);
×
119

120
                this.#handling = true;
34✔
121
                this.#messages.length = 0;
34✔
122

123
                try {
34✔
124
                        const r = handler.call(this, event);
34✔
125

126
                        return promiseOrSync(r, () => {
34✔
127
                                this.mutate(event);
34✔
128
                                return this.#messages.splice(0);
34✔
129
                        }, () => {
130
                                this.#handling = false;
34✔
131
                        });
132
                }
133
                catch (err) {
NEW
134
                        this.#handling = false;
×
NEW
135
                        throw err;
×
136
                }
137
        }
138

139
        /** Format a command and put it to the execution queue */
140
        protected enqueue(commandType: string): void;
141
        protected enqueue(commandType: string, aggregateId: Identifier): void;
142
        protected enqueue<T>(commandType: string, aggregateId: Identifier | undefined, payload: T): void;
143
        protected enqueue<T>(commandType: string, aggregateId?: Identifier, payload?: T) {
144
                if (typeof commandType !== 'string' || !commandType.length)
30!
145
                        throw new TypeError('commandType argument must be a non-empty String');
×
146
                if (!['string', 'number', 'undefined'].includes(typeof aggregateId))
30!
147
                        throw new TypeError('aggregateId argument must be either string, number or undefined');
×
148

149
                this.enqueueRaw({
30✔
150
                        aggregateId,
151
                        type: commandType,
152
                        payload
153
                });
154
        }
155

156
        /** Put a command to the execution queue */
157
        protected enqueueRaw(command: ICommand) {
158
                if (typeof command !== 'object' || !command)
30✔
159
                        throw new TypeError('command argument must be an Object');
×
160
                if (typeof command.type !== 'string' || !command.type.length)
30!
161
                        throw new TypeError('command.type argument must be a non-empty String');
×
162

163
                this.#messages.push(command);
30✔
164
        }
165

166
        /** Get human-readable Saga name */
167
        toString(): string {
168
                return `${getClassName(this)} ${this.id} (v${this.version})`;
×
169
        }
170
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc