@fluojs/cqrs 1.1.1 → 2.0.0
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.
- package/README.ko.md +69 -7
- package/README.md +69 -7
- package/dist/buses/command-bus.d.ts +9 -2
- package/dist/buses/command-bus.d.ts.map +1 -1
- package/dist/buses/command-bus.js +54 -24
- package/dist/buses/event-bus.d.ts +7 -7
- package/dist/buses/event-bus.d.ts.map +1 -1
- package/dist/buses/event-bus.js +55 -68
- package/dist/buses/event-handler-discovery.d.ts +12 -0
- package/dist/buses/event-handler-discovery.d.ts.map +1 -0
- package/dist/buses/event-handler-discovery.js +35 -0
- package/dist/buses/publish-drain-tracker.d.ts +38 -0
- package/dist/buses/publish-drain-tracker.d.ts.map +1 -0
- package/dist/buses/publish-drain-tracker.js +86 -0
- package/dist/buses/query-bus.d.ts +9 -2
- package/dist/buses/query-bus.d.ts.map +1 -1
- package/dist/buses/query-bus.js +54 -24
- package/dist/buses/saga-bus.d.ts +10 -6
- package/dist/buses/saga-bus.d.ts.map +1 -1
- package/dist/buses/saga-bus.js +41 -82
- package/dist/buses/saga-discovery.d.ts +12 -0
- package/dist/buses/saga-discovery.d.ts.map +1 -0
- package/dist/buses/saga-discovery.js +39 -0
- package/dist/buses/saga-drain.d.ts +9 -0
- package/dist/buses/saga-drain.d.ts.map +1 -0
- package/dist/buses/saga-drain.js +34 -0
- package/dist/buses/saga-topology.d.ts +15 -0
- package/dist/buses/saga-topology.d.ts.map +1 -0
- package/dist/buses/saga-topology.js +39 -0
- package/dist/discovery.d.ts +16 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +16 -9
- package/dist/dispatch-context.d.ts +33 -0
- package/dist/dispatch-context.d.ts.map +1 -0
- package/dist/dispatch-context.js +42 -0
- package/dist/module.d.ts +0 -8
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +4 -0
- package/dist/types.d.ts +4 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/buses/saga-bus.js
CHANGED
|
@@ -5,14 +5,18 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
|
|
|
5
5
|
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
6
6
|
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
7
|
import { FluoError, Inject, InvariantError } from '@fluojs/core';
|
|
8
|
-
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
8
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
9
9
|
import { CqrsBusBase } from '../discovery.js';
|
|
10
|
-
import { SagaExecutionError
|
|
10
|
+
import { SagaExecutionError } from '../errors.js';
|
|
11
11
|
import { createIsolatedEvent } from '../event-clone.js';
|
|
12
|
-
import { getSagaMetadata } from '../metadata.js';
|
|
13
12
|
import { CQRS_MODULE_OPTIONS } from '../tokens.js';
|
|
14
|
-
|
|
13
|
+
import { discoverSagaDescriptors } from './saga-discovery.js';
|
|
14
|
+
import { drainPendingSagaDispatches } from './saga-drain.js';
|
|
15
|
+
import { enterSagaTopology } from './saga-topology.js';
|
|
15
16
|
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
17
|
+
|
|
18
|
+
/** Private capability authorizing saga dispatch from an already active publish drain. */
|
|
19
|
+
export const CQRS_SAGA_DRAIN_AUTHORIZATION = Symbol('fluo.cqrs.sagaDrainAuthorization');
|
|
16
20
|
function isSaga(value) {
|
|
17
21
|
if (typeof value !== 'object' || value === null) {
|
|
18
22
|
return false;
|
|
@@ -29,13 +33,13 @@ function toErrorMessage(error) {
|
|
|
29
33
|
/**
|
|
30
34
|
* Runtime saga coordinator that discovers `@Saga()` providers and serializes execution per saga token.
|
|
31
35
|
*
|
|
32
|
-
* The service prevents re-entrant dispatch loops within the same
|
|
36
|
+
* The service prevents re-entrant dispatch loops within the same explicit dispatch context and waits for
|
|
33
37
|
* in-flight saga chains during shutdown so lifecycle guarantees remain predictable.
|
|
34
38
|
*/
|
|
35
39
|
let _CqrsSagaLifecycleSer;
|
|
36
40
|
class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
37
41
|
static {
|
|
38
|
-
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
|
|
42
|
+
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
|
|
39
43
|
}
|
|
40
44
|
descriptorsByEvent = new Map();
|
|
41
45
|
discoveryPromise;
|
|
@@ -44,9 +48,13 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
44
48
|
lifecycleState = 'created';
|
|
45
49
|
pendingDispatches = new Set();
|
|
46
50
|
shutdownDrainTimeouts = 0;
|
|
47
|
-
|
|
51
|
+
unregisterShutdownStartCleanup;
|
|
52
|
+
constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
|
|
48
53
|
super(runtimeContainer, compiledModules, logger);
|
|
49
54
|
this.moduleOptions = moduleOptions;
|
|
55
|
+
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
|
|
56
|
+
this.markApplicationShutdownStarted();
|
|
57
|
+
});
|
|
50
58
|
}
|
|
51
59
|
async onApplicationBootstrap() {
|
|
52
60
|
this.lifecycleState = 'discovering';
|
|
@@ -59,7 +67,9 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
async onApplicationShutdown() {
|
|
62
|
-
this.
|
|
70
|
+
this.markApplicationShutdownStarted();
|
|
71
|
+
this.unregisterShutdownStartCleanup?.();
|
|
72
|
+
this.unregisterShutdownStartCleanup = undefined;
|
|
63
73
|
await this.drainActiveSagaWork();
|
|
64
74
|
this.executionChains.clear();
|
|
65
75
|
this.handlerInstances.clear();
|
|
@@ -90,7 +100,8 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
90
100
|
* @param event Event instance that may trigger one or more sagas.
|
|
91
101
|
* @returns A promise that resolves once all matching saga chains for the event complete.
|
|
92
102
|
*/
|
|
93
|
-
async dispatch(event, context) {
|
|
103
|
+
async dispatch(event, context, drainAuthorization) {
|
|
104
|
+
this.assertAcceptingNewWork(drainAuthorization);
|
|
94
105
|
await this.ensureDiscovered();
|
|
95
106
|
const descriptors = this.matchSagaDescriptors(event);
|
|
96
107
|
if (descriptors.length === 0) {
|
|
@@ -98,6 +109,16 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
98
109
|
}
|
|
99
110
|
await Promise.all(descriptors.map(descriptor => this.dispatchWithOrdering(descriptor, event, context)));
|
|
100
111
|
}
|
|
112
|
+
assertAcceptingNewWork(drainAuthorization) {
|
|
113
|
+
if ((this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') && drainAuthorization !== CQRS_SAGA_DRAIN_AUTHORIZATION) {
|
|
114
|
+
throw new InvariantError('CQRS saga bus cannot dispatch after shutdown has started.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
markApplicationShutdownStarted() {
|
|
118
|
+
if (this.lifecycleState !== 'stopped') {
|
|
119
|
+
this.lifecycleState = 'stopping';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
101
122
|
matchSagaDescriptors(event) {
|
|
102
123
|
const descriptors = [];
|
|
103
124
|
for (const [eventType, eventDescriptors] of this.descriptorsByEvent.entries()) {
|
|
@@ -108,22 +129,14 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
108
129
|
return descriptors;
|
|
109
130
|
}
|
|
110
131
|
async dispatchWithOrdering(descriptor, event, activeContext) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (isActiveRoute) {
|
|
115
|
-
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeContext?.path ?? []), routeLabel].join(' -> ')}.`);
|
|
116
|
-
}
|
|
117
|
-
if ((activeContext?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
|
|
118
|
-
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} exceeded the maximum nested saga depth of ${MAX_NESTED_SAGA_DEPTH} while handling ${descriptor.eventType.name}. ` + 'Keep in-process saga graphs acyclic and externally bounded.');
|
|
119
|
-
}
|
|
120
|
-
if (isActiveToken) {
|
|
121
|
-
await this.invokeSaga(descriptor, event, this.createDispatchContext(activeContext, descriptor, routeLabel));
|
|
132
|
+
const topology = enterSagaTopology(activeContext, descriptor);
|
|
133
|
+
if (topology.reentrantToken) {
|
|
134
|
+
await this.invokeSaga(descriptor, event, topology.context);
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
124
137
|
const previous = this.executionChains.get(descriptor.token) ?? Promise.resolve();
|
|
125
138
|
const current = previous.then(async () => {
|
|
126
|
-
await this.invokeSaga(descriptor, event,
|
|
139
|
+
await this.invokeSaga(descriptor, event, topology.context);
|
|
127
140
|
});
|
|
128
141
|
this.executionChains.set(descriptor.token, current.catch(() => undefined));
|
|
129
142
|
this.pendingDispatches.add(current);
|
|
@@ -134,30 +147,16 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
async drainActiveSagaWork() {
|
|
137
|
-
const activeWork = [...this.pendingDispatches, ...this.executionChains.values()];
|
|
138
|
-
if (activeWork.length === 0) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
150
|
const timeoutMs = this.resolveShutdownDrainTimeoutMs();
|
|
142
|
-
const
|
|
151
|
+
const activeWorkCount = this.pendingDispatches.size;
|
|
152
|
+
const drained = await drainPendingSagaDispatches(this.pendingDispatches, timeoutMs);
|
|
143
153
|
if (!drained) {
|
|
144
|
-
this.
|
|
145
|
-
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWork.length)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
|
|
154
|
+
this.reportShutdownDrainTimeout(timeoutMs, activeWorkCount);
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
timeoutId = setTimeout(() => resolve(false), timeoutMs);
|
|
152
|
-
});
|
|
153
|
-
const drain = Promise.allSettled(activeWork).then(() => true);
|
|
154
|
-
try {
|
|
155
|
-
return await Promise.race([drain, timeout]);
|
|
156
|
-
} finally {
|
|
157
|
-
if (timeoutId) {
|
|
158
|
-
clearTimeout(timeoutId);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
157
|
+
reportShutdownDrainTimeout(timeoutMs, activeWorkCount) {
|
|
158
|
+
this.shutdownDrainTimeouts += 1;
|
|
159
|
+
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWorkCount)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
|
|
161
160
|
}
|
|
162
161
|
resolveShutdownDrainTimeoutMs() {
|
|
163
162
|
const timeoutMs = this.moduleOptions.shutdown?.drainTimeoutMs;
|
|
@@ -166,16 +165,6 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
166
165
|
}
|
|
167
166
|
return Math.floor(timeoutMs);
|
|
168
167
|
}
|
|
169
|
-
createDispatchContext(activeContext, descriptor, routeLabel) {
|
|
170
|
-
return {
|
|
171
|
-
activeRoutes: [...(activeContext?.activeRoutes ?? []), {
|
|
172
|
-
eventType: descriptor.eventType,
|
|
173
|
-
token: descriptor.token
|
|
174
|
-
}],
|
|
175
|
-
depth: (activeContext?.depth ?? 0) + 1,
|
|
176
|
-
path: [...(activeContext?.path ?? []), routeLabel]
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
168
|
async invokeSaga(descriptor, event, context) {
|
|
180
169
|
const instance = await this.resolveHandlerInstance(descriptor.token);
|
|
181
170
|
if (!isSaga(instance)) {
|
|
@@ -216,37 +205,7 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
216
205
|
}
|
|
217
206
|
}
|
|
218
207
|
discoverSagaDescriptors() {
|
|
219
|
-
|
|
220
|
-
const seenByTarget = new WeakMap();
|
|
221
|
-
for (const candidate of this.discoveryCandidates()) {
|
|
222
|
-
const metadata = getSagaMetadata(candidate.targetType);
|
|
223
|
-
if (!metadata) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (candidate.scope !== 'singleton') {
|
|
227
|
-
this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @Saga() but is registered with ${candidate.scope} scope. Sagas are registered only for singleton providers.`, 'CqrsSagaLifecycleService');
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
|
|
231
|
-
for (const eventType of metadata.eventTypes) {
|
|
232
|
-
if (seenEventTypes.has(eventType)) {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
seenEventTypes.add(eventType);
|
|
236
|
-
const descriptors = descriptorsByEvent.get(eventType) ?? [];
|
|
237
|
-
if (!descriptors.some(descriptor => descriptor.targetType === candidate.targetType)) {
|
|
238
|
-
descriptors.push({
|
|
239
|
-
eventType,
|
|
240
|
-
moduleName: candidate.moduleName,
|
|
241
|
-
targetType: candidate.targetType,
|
|
242
|
-
token: candidate.token
|
|
243
|
-
});
|
|
244
|
-
descriptorsByEvent.set(eventType, descriptors);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
seenByTarget.set(candidate.targetType, seenEventTypes);
|
|
248
|
-
}
|
|
249
|
-
return descriptorsByEvent;
|
|
208
|
+
return discoverSagaDescriptors(this.discoveryCandidates(), this.logger);
|
|
250
209
|
}
|
|
251
210
|
static {
|
|
252
211
|
_initClass();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApplicationLogger } from '@fluojs/runtime';
|
|
2
|
+
import type { DiscoveryCandidate } from '../discovery.js';
|
|
3
|
+
import type { CqrsEventType, SagaDescriptor } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Discovers singleton saga registrations by provider-token identity.
|
|
6
|
+
*
|
|
7
|
+
* @param candidates Provider registrations compiled from the application module graph.
|
|
8
|
+
* @param logger Application logger used for non-singleton discovery warnings.
|
|
9
|
+
* @returns Saga descriptors grouped by event type in discovery order.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverSagaDescriptors(candidates: readonly DiscoveryCandidate[], logger: ApplicationLogger): Map<CqrsEventType, SagaDescriptor[]>;
|
|
12
|
+
//# sourceMappingURL=saga-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saga-discovery.d.ts","sourceRoot":"","sources":["../../src/buses/saga-discovery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,MAAM,EAAE,iBAAiB,GACxB,GAAG,CAAC,aAAa,EAAE,cAAc,EAAE,CAAC,CAyCtC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getSagaMetadata } from '../metadata.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discovers singleton saga registrations by provider-token identity.
|
|
4
|
+
*
|
|
5
|
+
* @param candidates Provider registrations compiled from the application module graph.
|
|
6
|
+
* @param logger Application logger used for non-singleton discovery warnings.
|
|
7
|
+
* @returns Saga descriptors grouped by event type in discovery order.
|
|
8
|
+
*/
|
|
9
|
+
export function discoverSagaDescriptors(candidates, logger) {
|
|
10
|
+
const descriptorsByEvent = new Map();
|
|
11
|
+
const seenEventTypesByToken = new Map();
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
const metadata = getSagaMetadata(candidate.targetType);
|
|
14
|
+
if (!metadata) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (candidate.scope !== 'singleton') {
|
|
18
|
+
logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @Saga() but is registered with ${candidate.scope} scope. Sagas are registered only for singleton providers.`, 'CqrsSagaLifecycleService');
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const seenEventTypes = seenEventTypesByToken.get(candidate.token) ?? new Set();
|
|
22
|
+
for (const eventType of metadata.eventTypes) {
|
|
23
|
+
if (seenEventTypes.has(eventType)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seenEventTypes.add(eventType);
|
|
27
|
+
const descriptors = descriptorsByEvent.get(eventType) ?? [];
|
|
28
|
+
descriptors.push({
|
|
29
|
+
eventType,
|
|
30
|
+
moduleName: candidate.moduleName,
|
|
31
|
+
targetType: candidate.targetType,
|
|
32
|
+
token: candidate.token
|
|
33
|
+
});
|
|
34
|
+
descriptorsByEvent.set(eventType, descriptors);
|
|
35
|
+
}
|
|
36
|
+
seenEventTypesByToken.set(candidate.token, seenEventTypes);
|
|
37
|
+
}
|
|
38
|
+
return descriptorsByEvent;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waits until a mutable set of saga tasks becomes quiescent within one deadline.
|
|
3
|
+
*
|
|
4
|
+
* @param pendingDispatches Live saga task set owned by the lifecycle service.
|
|
5
|
+
* @param timeoutMs Maximum drain duration in milliseconds.
|
|
6
|
+
* @returns `true` when all current and late-added tasks settle before the deadline.
|
|
7
|
+
*/
|
|
8
|
+
export declare function drainPendingSagaDispatches(pendingDispatches: ReadonlySet<Promise<void>>, timeoutMs: number): Promise<boolean>;
|
|
9
|
+
//# sourceMappingURL=saga-drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saga-drain.d.ts","sourceRoot":"","sources":["../../src/buses/saga-drain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC9C,iBAAiB,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAgBlB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waits until a mutable set of saga tasks becomes quiescent within one deadline.
|
|
3
|
+
*
|
|
4
|
+
* @param pendingDispatches Live saga task set owned by the lifecycle service.
|
|
5
|
+
* @param timeoutMs Maximum drain duration in milliseconds.
|
|
6
|
+
* @returns `true` when all current and late-added tasks settle before the deadline.
|
|
7
|
+
*/
|
|
8
|
+
export async function drainPendingSagaDispatches(pendingDispatches, timeoutMs) {
|
|
9
|
+
const deadline = Date.now() + timeoutMs;
|
|
10
|
+
while (pendingDispatches.size > 0) {
|
|
11
|
+
const remainingTimeoutMs = deadline - Date.now();
|
|
12
|
+
if (remainingTimeoutMs <= 0) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (!(await awaitSagaTasks([...pendingDispatches], remainingTimeoutMs))) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
async function awaitSagaTasks(activeWork, timeoutMs) {
|
|
22
|
+
let timeoutId;
|
|
23
|
+
const timeout = new Promise(resolve => {
|
|
24
|
+
timeoutId = setTimeout(() => resolve(false), timeoutMs);
|
|
25
|
+
});
|
|
26
|
+
const drain = Promise.allSettled(activeWork).then(() => true);
|
|
27
|
+
try {
|
|
28
|
+
return await Promise.race([drain, timeout]);
|
|
29
|
+
} finally {
|
|
30
|
+
if (timeoutId) {
|
|
31
|
+
clearTimeout(timeoutId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CqrsDispatchContext, SagaDescriptor } from '../types.js';
|
|
2
|
+
/** Result of entering one guarded saga route. */
|
|
3
|
+
export interface SagaTopologyEntry {
|
|
4
|
+
readonly context: CqrsDispatchContext;
|
|
5
|
+
readonly reentrantToken: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates and enters one saga route using private immutable context state.
|
|
9
|
+
*
|
|
10
|
+
* @param context Opaque context passed through the active CQRS pipeline.
|
|
11
|
+
* @param descriptor Saga route selected for the current event.
|
|
12
|
+
* @returns The next opaque context and whether the same provider token is already active.
|
|
13
|
+
*/
|
|
14
|
+
export declare function enterSagaTopology(context: CqrsDispatchContext | undefined, descriptor: SagaDescriptor): SagaTopologyEntry;
|
|
15
|
+
//# sourceMappingURL=saga-topology.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saga-topology.d.ts","sourceRoot":"","sources":["../../src/buses/saga-topology.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIvE,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,GAAG,SAAS,EACxC,UAAU,EAAE,cAAc,GACzB,iBAAiB,CAiCnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createInternalCqrsDispatchContext, getInternalCqrsDispatchContextState } from '../dispatch-context.js';
|
|
2
|
+
import { SagaTopologyError } from '../errors.js';
|
|
3
|
+
const MAX_NESTED_SAGA_DEPTH = 32;
|
|
4
|
+
|
|
5
|
+
/** Result of entering one guarded saga route. */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates and enters one saga route using private immutable context state.
|
|
9
|
+
*
|
|
10
|
+
* @param context Opaque context passed through the active CQRS pipeline.
|
|
11
|
+
* @param descriptor Saga route selected for the current event.
|
|
12
|
+
* @returns The next opaque context and whether the same provider token is already active.
|
|
13
|
+
*/
|
|
14
|
+
export function enterSagaTopology(context, descriptor) {
|
|
15
|
+
const internalState = getInternalCqrsDispatchContextState(context);
|
|
16
|
+
const activeTopology = internalState?.sagaTopology;
|
|
17
|
+
const routeLabel = `${descriptor.targetType.name}(${descriptor.eventType.name})`;
|
|
18
|
+
const reenteredRoute = activeTopology?.activeRoutes.some(route => route.token === descriptor.token && route.eventType === descriptor.eventType);
|
|
19
|
+
if (reenteredRoute) {
|
|
20
|
+
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeTopology?.path ?? []), routeLabel].join(' -> ')}.`);
|
|
21
|
+
}
|
|
22
|
+
if ((activeTopology?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
|
|
23
|
+
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} exceeded the maximum nested saga depth of ${MAX_NESTED_SAGA_DEPTH} while handling ${descriptor.eventType.name}. ` + 'Keep in-process saga graphs acyclic and externally bounded.');
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
context: createInternalCqrsDispatchContext({
|
|
27
|
+
publishDrainToken: internalState?.publishDrainToken,
|
|
28
|
+
sagaTopology: {
|
|
29
|
+
activeRoutes: [...(activeTopology?.activeRoutes ?? []), {
|
|
30
|
+
eventType: descriptor.eventType,
|
|
31
|
+
token: descriptor.token
|
|
32
|
+
}],
|
|
33
|
+
depth: (activeTopology?.depth ?? 0) + 1,
|
|
34
|
+
path: [...(activeTopology?.path ?? []), routeLabel]
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
reentrantToken: activeTopology?.activeRoutes.some(route => route.token === descriptor.token) ?? false
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/discovery.d.ts
CHANGED
|
@@ -22,10 +22,26 @@ export interface DiscoveryCandidate {
|
|
|
22
22
|
export declare function createDuplicateHandlerMessage(kind: 'command' | 'query' | 'event', messageType: Function, first: {
|
|
23
23
|
moduleName: string;
|
|
24
24
|
targetType: Function;
|
|
25
|
+
token: Token;
|
|
25
26
|
}, second: {
|
|
26
27
|
moduleName: string;
|
|
27
28
|
targetType: Function;
|
|
29
|
+
token: Token;
|
|
28
30
|
}): string;
|
|
31
|
+
/**
|
|
32
|
+
* Checks whether two discovered handler candidates refer to the same provider registration.
|
|
33
|
+
*
|
|
34
|
+
* @param first The first handler registration.
|
|
35
|
+
* @param second The second handler registration.
|
|
36
|
+
* @returns Whether both target type and provider token match.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isSameHandlerRegistration(first: {
|
|
39
|
+
targetType: Function;
|
|
40
|
+
token: Token;
|
|
41
|
+
}, second: {
|
|
42
|
+
targetType: Function;
|
|
43
|
+
token: Token;
|
|
44
|
+
}): boolean;
|
|
29
45
|
/**
|
|
30
46
|
* Represents the cqrs bus base.
|
|
31
47
|
*/
|
package/dist/discovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;IAC7C,UAAU,EAAE,QAAQ,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;CACd;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,EACnC,WAAW,EAAE,QAAQ,EACrB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EACjE,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACjE,MAAM,CAER;AAMD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE;IAAE,UAAU,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAC7C,MAAM,EAAE;IAAE,UAAU,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC7C,OAAO,CAET;AAED;;GAEG;AACH,8BAAsB,WAAW;IAI7B,SAAS,CAAC,QAAQ,CAAC,gBAAgB,EAAE,SAAS;IAC9C,SAAS,CAAC,QAAQ,CAAC,eAAe,EAAE,SAAS,cAAc,EAAE;IAC7D,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB;IAL9C,SAAS,CAAC,QAAQ,CAAC,gBAAgB,+BAAsC;gBAGpD,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB;IAG9C,SAAS,CAAC,mBAAmB,IAAI,kBAAkB,EAAE;cA6BrC,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;cAgBnD,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;CAiBvE"}
|
package/dist/discovery.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatTokenName } from '@fluojs/core';
|
|
1
2
|
import { getClassDiMetadata } from '@fluojs/core/internal';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -27,7 +28,21 @@ function isClassProvider(provider) {
|
|
|
27
28
|
* @returns The create duplicate handler message result.
|
|
28
29
|
*/
|
|
29
30
|
export function createDuplicateHandlerMessage(kind, messageType, first, second) {
|
|
30
|
-
return `Duplicate ${kind} handler for ${messageType.name} was discovered in ${first
|
|
31
|
+
return `Duplicate ${kind} handler for ${messageType.name} was discovered in ${describeHandlerRegistration(first)} and ${describeHandlerRegistration(second)}.`;
|
|
32
|
+
}
|
|
33
|
+
function describeHandlerRegistration(registration) {
|
|
34
|
+
return `${registration.moduleName}.${registration.targetType.name} [token: ${formatTokenName(registration.token)}]`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks whether two discovered handler candidates refer to the same provider registration.
|
|
39
|
+
*
|
|
40
|
+
* @param first The first handler registration.
|
|
41
|
+
* @param second The second handler registration.
|
|
42
|
+
* @returns Whether both target type and provider token match.
|
|
43
|
+
*/
|
|
44
|
+
export function isSameHandlerRegistration(first, second) {
|
|
45
|
+
return first.targetType === second.targetType && first.token === second.token;
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
/**
|
|
@@ -62,14 +77,6 @@ export class CqrsBusBase {
|
|
|
62
77
|
});
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
|
-
for (const controller of compiledModule.definition.controllers ?? []) {
|
|
66
|
-
candidates.push({
|
|
67
|
-
moduleName: compiledModule.type.name,
|
|
68
|
-
scope: scopeFromProvider(controller),
|
|
69
|
-
targetType: controller,
|
|
70
|
-
token: controller
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
80
|
}
|
|
74
81
|
return candidates;
|
|
75
82
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Token } from '@fluojs/core';
|
|
2
|
+
import type { CqrsDispatchContext, CqrsEventType } from './types.js';
|
|
3
|
+
/** One active saga route retained in private CQRS dispatch state. */
|
|
4
|
+
export interface CqrsDispatchRoute {
|
|
5
|
+
readonly eventType: CqrsEventType;
|
|
6
|
+
readonly token: Token;
|
|
7
|
+
}
|
|
8
|
+
/** Private saga topology state associated with an opaque dispatch context. */
|
|
9
|
+
export interface CqrsSagaTopologyState {
|
|
10
|
+
readonly activeRoutes: readonly CqrsDispatchRoute[];
|
|
11
|
+
readonly depth: number;
|
|
12
|
+
readonly path: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/** Private state carried by an internally created dispatch context. */
|
|
15
|
+
export interface InternalCqrsDispatchContextState {
|
|
16
|
+
readonly publishDrainToken: symbol | undefined;
|
|
17
|
+
readonly sagaTopology: CqrsSagaTopologyState | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates an opaque immutable dispatch context and retains its state in a private weak map.
|
|
21
|
+
*
|
|
22
|
+
* @param state Internal publish-drain and saga-topology state.
|
|
23
|
+
* @returns A frozen fieldless context safe to pass through application handlers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createInternalCqrsDispatchContext(state: InternalCqrsDispatchContextState): CqrsDispatchContext;
|
|
26
|
+
/**
|
|
27
|
+
* Reads private state only for context values created by CQRS internals.
|
|
28
|
+
*
|
|
29
|
+
* @param context Optional public dispatch context received from a handler or saga.
|
|
30
|
+
* @returns The immutable internal state, or `undefined` for caller-created values.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getInternalCqrsDispatchContextState(context: CqrsDispatchContext | undefined): InternalCqrsDispatchContextState | undefined;
|
|
33
|
+
//# sourceMappingURL=dispatch-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-context.d.ts","sourceRoot":"","sources":["../src/dispatch-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAErE,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB;AAED,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,uEAAuE;AACvE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,YAAY,EAAE,qBAAqB,GAAG,SAAS,CAAC;CAC1D;AAcD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,gCAAgC,GAAG,mBAAmB,CAU9G;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,mBAAmB,GAAG,SAAS,GACvC,gCAAgC,GAAG,SAAS,CAE9C"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** One active saga route retained in private CQRS dispatch state. */
|
|
2
|
+
|
|
3
|
+
/** Private saga topology state associated with an opaque dispatch context. */
|
|
4
|
+
|
|
5
|
+
/** Private state carried by an internally created dispatch context. */
|
|
6
|
+
|
|
7
|
+
const internalContextStates = new WeakMap();
|
|
8
|
+
function freezeSagaTopology(state) {
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
activeRoutes: Object.freeze(state.activeRoutes.map(route => Object.freeze({
|
|
11
|
+
eventType: route.eventType,
|
|
12
|
+
token: route.token
|
|
13
|
+
}))),
|
|
14
|
+
depth: state.depth,
|
|
15
|
+
path: Object.freeze([...state.path])
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates an opaque immutable dispatch context and retains its state in a private weak map.
|
|
21
|
+
*
|
|
22
|
+
* @param state Internal publish-drain and saga-topology state.
|
|
23
|
+
* @returns A frozen fieldless context safe to pass through application handlers.
|
|
24
|
+
*/
|
|
25
|
+
export function createInternalCqrsDispatchContext(state) {
|
|
26
|
+
const context = Object.freeze({});
|
|
27
|
+
internalContextStates.set(context, Object.freeze({
|
|
28
|
+
publishDrainToken: state.publishDrainToken,
|
|
29
|
+
sagaTopology: state.sagaTopology ? freezeSagaTopology(state.sagaTopology) : undefined
|
|
30
|
+
}));
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reads private state only for context values created by CQRS internals.
|
|
36
|
+
*
|
|
37
|
+
* @param context Optional public dispatch context received from a handler or saga.
|
|
38
|
+
* @returns The immutable internal state, or `undefined` for caller-created values.
|
|
39
|
+
*/
|
|
40
|
+
export function getInternalCqrsDispatchContextState(context) {
|
|
41
|
+
return context ? internalContextStates.get(context) : undefined;
|
|
42
|
+
}
|
package/dist/module.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Provider } from '@fluojs/di';
|
|
2
1
|
import { type EventBusModuleOptions } from '@fluojs/event-bus';
|
|
3
2
|
import { type ModuleType } from '@fluojs/runtime';
|
|
4
3
|
import type { CommandHandlerClass, EventHandlerClass, QueryHandlerClass, SagaClass } from './types.js';
|
|
@@ -16,13 +15,6 @@ export interface CqrsModuleOptions {
|
|
|
16
15
|
drainTimeoutMs?: number;
|
|
17
16
|
};
|
|
18
17
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Creates the providers required for CQRS buses, compatibility aliases, and optional handler registration.
|
|
21
|
-
*
|
|
22
|
-
* @param options CQRS module options including eager handler classes and event-bus configuration.
|
|
23
|
-
* @returns Providers for the command, query, event, and saga runtimes plus compatibility tokens.
|
|
24
|
-
*/
|
|
25
|
-
export declare function createCqrsProviders(options?: CqrsModuleOptions): Provider[];
|
|
26
18
|
/** Runtime module entrypoint for CQRS bus registration and handler discovery. */
|
|
27
19
|
export declare class CqrsModule {
|
|
28
20
|
/**
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAOhE,OAAO,KAAK,EACV,mBAAmB,EAEnB,iBAAiB,EAIjB,iBAAiB,EACjB,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACjD,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,aAAa,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,KAAK,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B,8GAA8G;IAC9G,QAAQ,CAAC,EAAE;QACT,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAyGD,iFAAiF;AACjF,qBAAa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAiB5D"}
|
package/dist/module.js
CHANGED
|
@@ -24,6 +24,13 @@ function collectOptionHandlerProviders(options) {
|
|
|
24
24
|
}
|
|
25
25
|
return providers;
|
|
26
26
|
}
|
|
27
|
+
function resolveDelegatedEventBusOptions(options) {
|
|
28
|
+
const eventBusOptions = options.eventBus ?? {};
|
|
29
|
+
return {
|
|
30
|
+
...eventBusOptions,
|
|
31
|
+
global: eventBusOptions.global ?? options.global ?? true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
27
34
|
function assertCommandBusService(service) {
|
|
28
35
|
if (!(service instanceof CommandBusLifecycleService)) {
|
|
29
36
|
throw new TypeError('CQRS command bus alias expected CommandBusLifecycleService.');
|
|
@@ -46,7 +53,7 @@ function assertCqrsEventBusService(service) {
|
|
|
46
53
|
* @param options CQRS module options including eager handler classes and event-bus configuration.
|
|
47
54
|
* @returns Providers for the command, query, event, and saga runtimes plus compatibility tokens.
|
|
48
55
|
*/
|
|
49
|
-
|
|
56
|
+
function createCqrsProviders(options = {}) {
|
|
50
57
|
return [{
|
|
51
58
|
provide: CQRS_MODULE_OPTIONS,
|
|
52
59
|
useValue: options
|
|
@@ -94,7 +101,7 @@ export class CqrsModule {
|
|
|
94
101
|
return defineModule(CqrsModuleDefinition, {
|
|
95
102
|
exports: [CommandBusLifecycleService, QueryBusLifecycleService, CqrsEventBusService, COMMAND_BUS, QUERY_BUS, EVENT_BUS],
|
|
96
103
|
global: options.global ?? true,
|
|
97
|
-
imports: [EventBusModule.forRoot(options
|
|
104
|
+
imports: [EventBusModule.forRoot(resolveDelegatedEventBusOptions(options))],
|
|
98
105
|
providers: createCqrsProviders(options)
|
|
99
106
|
});
|
|
100
107
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":""}
|
package/dist/types.d.ts
CHANGED
|
@@ -56,19 +56,12 @@ export interface ISaga<TEvent extends IEvent = IEvent> {
|
|
|
56
56
|
/**
|
|
57
57
|
* Opaque dispatch context used to preserve saga topology guards across nested CQRS calls.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* CQRS passes this value to command handlers, query handlers, event handlers, and sagas when a
|
|
60
|
+
* nested dispatch chain is active. Application code should pass the value through unchanged to
|
|
61
|
+
* nested `execute(...)`, `publish(...)`, or `publishAll(...)` calls. The context intentionally
|
|
62
|
+
* exposes no public topology fields, and caller-shaped objects cannot supply trusted runtime state.
|
|
61
63
|
*/
|
|
62
64
|
export interface CqrsDispatchContext {
|
|
63
|
-
/** Saga routes currently active in the in-process CQRS dispatch chain. */
|
|
64
|
-
readonly activeRoutes: readonly Readonly<{
|
|
65
|
-
eventType: CqrsEventType;
|
|
66
|
-
token: Token;
|
|
67
|
-
}>[];
|
|
68
|
-
/** Current nested saga depth for in-process topology guarding. */
|
|
69
|
-
readonly depth: number;
|
|
70
|
-
/** Human-readable saga route labels used when reporting topology failures. */
|
|
71
|
-
readonly path: readonly string[];
|
|
72
65
|
}
|
|
73
66
|
/** Constructor type used to identify a command message class. */
|
|
74
67
|
export interface CommandType<TCommand extends ICommand = ICommand> {
|