@fluojs/cqrs 1.1.2 → 3.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 +29 -9
- package/README.md +29 -9
- package/dist/buses/command-bus.d.ts +19 -2
- package/dist/buses/command-bus.d.ts.map +1 -1
- package/dist/buses/command-bus.js +68 -24
- package/dist/buses/event-bus.d.ts +16 -8
- package/dist/buses/event-bus.d.ts.map +1 -1
- package/dist/buses/event-bus.js +84 -74
- 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 +89 -0
- package/dist/buses/query-bus.d.ts +19 -2
- package/dist/buses/query-bus.d.ts.map +1 -1
- package/dist/buses/query-bus.js +68 -24
- package/dist/buses/saga-bus.d.ts +21 -9
- package/dist/buses/saga-bus.d.ts.map +1 -1
- package/dist/buses/saga-bus.js +113 -112
- package/dist/buses/saga-continuation.d.ts +14 -0
- package/dist/buses/saga-continuation.d.ts.map +1 -0
- package/dist/buses/saga-continuation.js +24 -0
- 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 +18 -0
- package/dist/buses/saga-topology.d.ts.map +1 -0
- package/dist/buses/saga-topology.js +46 -0
- package/dist/buses/shutdown-deadline.d.ts +23 -0
- package/dist/buses/shutdown-deadline.d.ts.map +1 -0
- package/dist/buses/shutdown-deadline.js +31 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +15 -1
- package/dist/discovery.d.ts +18 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +95 -12
- package/dist/dispatch-context.d.ts +35 -0
- package/dist/dispatch-context.d.ts.map +1 -0
- package/dist/dispatch-context.js +43 -0
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -2
- package/dist/module.d.ts +4 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +24 -4
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +10 -6
- 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 +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/buses/saga-bus.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { OnApplicationBootstrap, OnApplicationShutdown } from '@fluojs/runtime';
|
|
1
|
+
import type { OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
|
|
2
2
|
import { CqrsBusBase } from '../discovery.js';
|
|
3
3
|
import type { CqrsModuleOptions } from '../module.js';
|
|
4
4
|
import type { CqrsDispatchContext, IEvent } from '../types.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { type SagaDispatchOptions } from './saga-continuation.js';
|
|
6
|
+
import { CqrsShutdownDeadline } from './shutdown-deadline.js';
|
|
7
|
+
/** Private capability authorizing saga dispatch from an already active publish drain. */
|
|
8
|
+
export declare const CQRS_SAGA_DRAIN_AUTHORIZATION: unique symbol;
|
|
8
9
|
/**
|
|
9
10
|
* Runtime saga coordinator that discovers `@Saga()` providers and serializes execution per saga token.
|
|
10
11
|
*
|
|
@@ -13,14 +14,17 @@ interface SagaDispatchOptions {
|
|
|
13
14
|
*/
|
|
14
15
|
export declare class CqrsSagaLifecycleService extends CqrsBusBase implements OnApplicationBootstrap, OnApplicationShutdown {
|
|
15
16
|
private readonly moduleOptions;
|
|
17
|
+
private readonly shutdownDeadline;
|
|
16
18
|
private descriptorsByEvent;
|
|
17
19
|
private discoveryPromise;
|
|
18
20
|
private discovered;
|
|
19
21
|
private readonly executionChains;
|
|
20
22
|
private lifecycleState;
|
|
21
23
|
private readonly pendingDispatches;
|
|
24
|
+
private authorizedPipelineLeaseCount;
|
|
22
25
|
private shutdownDrainTimeouts;
|
|
23
|
-
|
|
26
|
+
private unregisterShutdownStartCleanup;
|
|
27
|
+
constructor(runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions, registerRuntimeCleanup?: RuntimeCleanupRegistration, shutdownDeadline?: CqrsShutdownDeadline);
|
|
24
28
|
onApplicationBootstrap(): Promise<void>;
|
|
25
29
|
onApplicationShutdown(): Promise<void>;
|
|
26
30
|
/**
|
|
@@ -41,18 +45,26 @@ export declare class CqrsSagaLifecycleService extends CqrsBusBase implements OnA
|
|
|
41
45
|
* @param event Event instance that may trigger one or more sagas.
|
|
42
46
|
* @returns A promise that resolves once all matching saga chains for the event complete.
|
|
43
47
|
*/
|
|
44
|
-
dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext, options?: SagaDispatchOptions): Promise<void>;
|
|
48
|
+
dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext, options?: SagaDispatchOptions<typeof CQRS_SAGA_DRAIN_AUTHORIZATION>): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Retains the discovered saga graph for one active CQRS publish pipeline.
|
|
51
|
+
*
|
|
52
|
+
* @returns A release function that must run after the authorized pipeline settles.
|
|
53
|
+
*/
|
|
54
|
+
acquireAuthorizedPipelineLease(): () => void;
|
|
55
|
+
private runDispatch;
|
|
45
56
|
private assertAcceptingNewWork;
|
|
57
|
+
private markApplicationShutdownStarted;
|
|
46
58
|
private matchSagaDescriptors;
|
|
47
59
|
private dispatchWithOrdering;
|
|
60
|
+
private trackPendingDispatch;
|
|
61
|
+
private clearStoppedSagaGraphIfQuiescent;
|
|
48
62
|
private drainActiveSagaWork;
|
|
49
|
-
private
|
|
63
|
+
private reportShutdownDrainTimeout;
|
|
50
64
|
private resolveShutdownDrainTimeoutMs;
|
|
51
|
-
private createDispatchContext;
|
|
52
65
|
private invokeSaga;
|
|
53
66
|
private ensureDiscovered;
|
|
54
67
|
private discoverHandlers;
|
|
55
68
|
private discoverSagaDescriptors;
|
|
56
69
|
}
|
|
57
|
-
export {};
|
|
58
70
|
//# sourceMappingURL=saga-bus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saga-bus.d.ts","sourceRoot":"","sources":["../../src/buses/saga-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"saga-bus.d.ts","sourceRoot":"","sources":["../../src/buses/saga-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGjH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,EAAyB,MAAM,aAAa,CAAC;AACrG,OAAO,EAA8D,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG9H,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAK9D,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,EAAE,OAAO,MAAmD,CAAC;AAkBvG;;;;;GAKG;AACH,qBAQa,wBAAyB,SAAQ,WAAY,YAAW,sBAAsB,EAAE,qBAAqB;IAe9G,OAAO,CAAC,QAAQ,CAAC,aAAa;IAE9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAhBnC,OAAO,CAAC,kBAAkB,CAA8C;IACxE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmC;IACnE,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA4B;IAC9D,OAAO,CAAC,4BAA4B,CAAK;IACzC,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,8BAA8B,CAA2B;gBAG/D,gBAAgB,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EAC9D,eAAe,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EAC7D,MAAM,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,EACnC,aAAa,GAAE,iBAAsB,EACtD,sBAAsB,GAAE,0BAAkD,EACzD,gBAAgB,GAAE,oBAAiD;IAShF,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5C;;;;OAIG;IACH,kBAAkB,IAAI;QACpB,UAAU,EAAE,OAAO,CAAC;QACpB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,cAAc,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;QACxF,eAAe,EAAE,MAAM,CAAC;QACxB,qBAAqB,EAAE,MAAM,CAAC;KAC/B;IAcD;;;;;OAKG;IACG,QAAQ,CAAC,MAAM,SAAS,MAAM,EAClC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,mBAAmB,EAC7B,OAAO,GAAE,mBAAmB,CAAC,OAAO,6BAA6B,CAAM,GACtE,OAAO,CAAC,IAAI,CAAC;IAKhB;;;;OAIG;IACH,8BAA8B,IAAI,MAAM,IAAI;YAe9B,WAAW;IA2CzB,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,8BAA8B;IAQtC,OAAO,CAAC,oBAAoB;YAYd,oBAAoB;YAwBpB,oBAAoB;IAWlC,OAAO,CAAC,gCAAgC;YAgB1B,mBAAmB;IAUjC,OAAO,CAAC,0BAA0B;IAQlC,OAAO,CAAC,6BAA6B;YAUvB,UAAU;YAoBV,gBAAgB;YAchB,gBAAgB;IAiB9B,OAAO,CAAC,uBAAuB;CAGhC"}
|
package/dist/buses/saga-bus.js
CHANGED
|
@@ -5,15 +5,20 @@ 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 { drainSagaContinuations, runSerializedSagaContinuationTasks } from './saga-continuation.js';
|
|
14
|
+
import { discoverSagaDescriptors } from './saga-discovery.js';
|
|
15
|
+
import { drainPendingSagaDispatches } from './saga-drain.js';
|
|
16
|
+
import { CqrsShutdownDeadline } from './shutdown-deadline.js';
|
|
17
|
+
import { enterSagaTopology } from './saga-topology.js';
|
|
15
18
|
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
16
|
-
|
|
19
|
+
|
|
20
|
+
/** Private capability authorizing saga dispatch from an already active publish drain. */
|
|
21
|
+
export const CQRS_SAGA_DRAIN_AUTHORIZATION = Symbol('fluo.cqrs.sagaDrainAuthorization');
|
|
17
22
|
function isSaga(value) {
|
|
18
23
|
if (typeof value !== 'object' || value === null) {
|
|
19
24
|
return false;
|
|
@@ -26,12 +31,6 @@ function toErrorMessage(error) {
|
|
|
26
31
|
}
|
|
27
32
|
return String(error);
|
|
28
33
|
}
|
|
29
|
-
function isCqrsDispatchContextState(context) {
|
|
30
|
-
if (typeof context !== 'object' || context === null) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
return context[cqrsDispatchContextStateBrand] === true;
|
|
34
|
-
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* Runtime saga coordinator that discovers `@Saga()` providers and serializes execution per saga token.
|
|
@@ -42,7 +41,7 @@ function isCqrsDispatchContextState(context) {
|
|
|
42
41
|
let _CqrsSagaLifecycleSer;
|
|
43
42
|
class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
44
43
|
static {
|
|
45
|
-
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
|
|
44
|
+
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION, CqrsShutdownDeadline)], [], 0, void 0, CqrsBusBase).c;
|
|
46
45
|
}
|
|
47
46
|
descriptorsByEvent = new Map();
|
|
48
47
|
discoveryPromise;
|
|
@@ -50,10 +49,16 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
50
49
|
executionChains = new Map();
|
|
51
50
|
lifecycleState = 'created';
|
|
52
51
|
pendingDispatches = new Set();
|
|
52
|
+
authorizedPipelineLeaseCount = 0;
|
|
53
53
|
shutdownDrainTimeouts = 0;
|
|
54
|
-
|
|
54
|
+
unregisterShutdownStartCleanup;
|
|
55
|
+
constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined, shutdownDeadline = new CqrsShutdownDeadline()) {
|
|
55
56
|
super(runtimeContainer, compiledModules, logger);
|
|
56
57
|
this.moduleOptions = moduleOptions;
|
|
58
|
+
this.shutdownDeadline = shutdownDeadline;
|
|
59
|
+
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
|
|
60
|
+
this.markApplicationShutdownStarted();
|
|
61
|
+
});
|
|
57
62
|
}
|
|
58
63
|
async onApplicationBootstrap() {
|
|
59
64
|
this.lifecycleState = 'discovering';
|
|
@@ -66,14 +71,12 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
async onApplicationShutdown() {
|
|
69
|
-
this.
|
|
74
|
+
this.markApplicationShutdownStarted();
|
|
75
|
+
this.unregisterShutdownStartCleanup?.();
|
|
76
|
+
this.unregisterShutdownStartCleanup = undefined;
|
|
70
77
|
await this.drainActiveSagaWork();
|
|
71
|
-
this.executionChains.clear();
|
|
72
|
-
this.handlerInstances.clear();
|
|
73
|
-
this.descriptorsByEvent.clear();
|
|
74
|
-
this.discovered = false;
|
|
75
|
-
this.discoveryPromise = undefined;
|
|
76
78
|
this.lifecycleState = 'stopped';
|
|
79
|
+
this.clearStoppedSagaGraphIfQuiescent();
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/**
|
|
@@ -82,11 +85,12 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
82
85
|
* @returns Current discovery state, in-flight execution count, lifecycle state, and discovered saga count.
|
|
83
86
|
*/
|
|
84
87
|
getRuntimeSnapshot() {
|
|
88
|
+
const stopped = this.lifecycleState === 'stopped';
|
|
85
89
|
return {
|
|
86
|
-
discovered: this.discovered,
|
|
90
|
+
discovered: stopped ? false : this.discovered,
|
|
87
91
|
inFlightSagaExecutions: this.pendingDispatches.size,
|
|
88
92
|
lifecycleState: this.lifecycleState,
|
|
89
|
-
sagasDiscovered: new Set(Array.from(this.descriptorsByEvent.values()).flatMap(descriptors => descriptors.map(d => d.token))).size,
|
|
93
|
+
sagasDiscovered: stopped ? 0 : new Set(Array.from(this.descriptorsByEvent.values()).flatMap(descriptors => descriptors.map(d => d.token))).size,
|
|
90
94
|
shutdownDrainTimeouts: this.shutdownDrainTimeouts
|
|
91
95
|
};
|
|
92
96
|
}
|
|
@@ -98,22 +102,66 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
98
102
|
* @returns A promise that resolves once all matching saga chains for the event complete.
|
|
99
103
|
*/
|
|
100
104
|
async dispatch(event, context, options = {}) {
|
|
101
|
-
this.assertAcceptingNewWork(options);
|
|
105
|
+
this.assertAcceptingNewWork(options.drainAuthorization);
|
|
106
|
+
return this.trackPendingDispatch(this.runDispatch(event, context, options));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retains the discovered saga graph for one active CQRS publish pipeline.
|
|
111
|
+
*
|
|
112
|
+
* @returns A release function that must run after the authorized pipeline settles.
|
|
113
|
+
*/
|
|
114
|
+
acquireAuthorizedPipelineLease() {
|
|
115
|
+
this.authorizedPipelineLeaseCount += 1;
|
|
116
|
+
let released = false;
|
|
117
|
+
return () => {
|
|
118
|
+
if (released) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
released = true;
|
|
122
|
+
this.authorizedPipelineLeaseCount -= 1;
|
|
123
|
+
this.clearStoppedSagaGraphIfQuiescent();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async runDispatch(event, context, options) {
|
|
102
127
|
await this.ensureDiscovered();
|
|
103
|
-
const
|
|
104
|
-
|
|
128
|
+
const entries = this.matchSagaDescriptors(event).map(descriptor => ({
|
|
129
|
+
descriptor,
|
|
130
|
+
topology: enterSagaTopology(context, descriptor)
|
|
131
|
+
}));
|
|
132
|
+
if (entries.length === 0) {
|
|
133
|
+
await options.afterSagas?.();
|
|
105
134
|
return;
|
|
106
135
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
const continuationEntry = entries.find(entry => entry.topology.reentrantToken);
|
|
137
|
+
if (continuationEntry) {
|
|
138
|
+
const continuationTasks = entries.map(entry => {
|
|
139
|
+
const scheduledDispatch = entry.topology.reentrantToken ? this.dispatchWithOrdering(entry.descriptor, event, entry.topology) : undefined;
|
|
140
|
+
return {
|
|
141
|
+
run: () => scheduledDispatch ?? this.dispatchWithOrdering(entry.descriptor, event, entry.topology),
|
|
142
|
+
token: entry.descriptor.token
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
continuationEntry.topology.continuationScope.queue.push(async () => {
|
|
146
|
+
await runSerializedSagaContinuationTasks(continuationTasks);
|
|
147
|
+
await options.afterSagas?.();
|
|
148
|
+
});
|
|
111
149
|
return;
|
|
112
150
|
}
|
|
113
|
-
|
|
151
|
+
await Promise.all(entries.map(entry => this.dispatchWithOrdering(entry.descriptor, event, entry.topology)));
|
|
152
|
+
await options.afterSagas?.();
|
|
153
|
+
}
|
|
154
|
+
assertAcceptingNewWork(drainAuthorization) {
|
|
155
|
+
if ((this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') && drainAuthorization !== CQRS_SAGA_DRAIN_AUTHORIZATION) {
|
|
114
156
|
throw new InvariantError('CQRS saga bus cannot dispatch after shutdown has started.');
|
|
115
157
|
}
|
|
116
158
|
}
|
|
159
|
+
markApplicationShutdownStarted() {
|
|
160
|
+
this.shutdownDeadline.start(this.resolveShutdownDrainTimeoutMs());
|
|
161
|
+
if (this.lifecycleState !== 'stopped') {
|
|
162
|
+
this.lifecycleState = 'stopping';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
117
165
|
matchSagaDescriptors(event) {
|
|
118
166
|
const descriptors = [];
|
|
119
167
|
for (const [eventType, eventDescriptors] of this.descriptorsByEvent.entries()) {
|
|
@@ -123,58 +171,52 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
123
171
|
}
|
|
124
172
|
return descriptors;
|
|
125
173
|
}
|
|
126
|
-
async dispatchWithOrdering(descriptor, event,
|
|
127
|
-
const activeState = isCqrsDispatchContextState(activeContext) ? activeContext : undefined;
|
|
128
|
-
const routeLabel = `${descriptor.targetType.name}(${descriptor.eventType.name})`;
|
|
129
|
-
const isActiveRoute = activeState?.activeRoutes.some(route => route.token === descriptor.token && route.eventType === descriptor.eventType);
|
|
130
|
-
const isActiveToken = activeState?.activeRoutes.some(route => route.token === descriptor.token) ?? false;
|
|
131
|
-
if (isActiveRoute) {
|
|
132
|
-
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeState?.path ?? []), routeLabel].join(' -> ')}.`);
|
|
133
|
-
}
|
|
134
|
-
if ((activeState?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
|
|
135
|
-
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.');
|
|
136
|
-
}
|
|
137
|
-
if (isActiveToken) {
|
|
138
|
-
await this.invokeSaga(descriptor, event, this.createDispatchContext(activeState, descriptor, routeLabel));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
174
|
+
async dispatchWithOrdering(descriptor, event, topology) {
|
|
141
175
|
const previous = this.executionChains.get(descriptor.token) ?? Promise.resolve();
|
|
142
|
-
const current = previous.then(async () =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.executionChains.set(descriptor.token, current.catch(() => undefined));
|
|
146
|
-
this.pendingDispatches.add(current);
|
|
176
|
+
const current = previous.then(async () => this.invokeSaga(descriptor, event, topology.context));
|
|
177
|
+
const settled = current.catch(() => undefined);
|
|
178
|
+
this.executionChains.set(descriptor.token, settled);
|
|
147
179
|
try {
|
|
148
180
|
await current;
|
|
181
|
+
if (topology.ownsContinuationScope) {
|
|
182
|
+
await drainSagaContinuations(topology.continuationScope);
|
|
183
|
+
}
|
|
149
184
|
} finally {
|
|
150
|
-
this.
|
|
185
|
+
if (this.executionChains.get(descriptor.token) === settled) {
|
|
186
|
+
this.executionChains.delete(descriptor.token);
|
|
187
|
+
}
|
|
151
188
|
}
|
|
152
189
|
}
|
|
153
|
-
async
|
|
154
|
-
|
|
155
|
-
|
|
190
|
+
async trackPendingDispatch(dispatch) {
|
|
191
|
+
this.pendingDispatches.add(dispatch);
|
|
192
|
+
try {
|
|
193
|
+
await dispatch;
|
|
194
|
+
} finally {
|
|
195
|
+
this.pendingDispatches.delete(dispatch);
|
|
196
|
+
this.clearStoppedSagaGraphIfQuiescent();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
clearStoppedSagaGraphIfQuiescent() {
|
|
200
|
+
if (this.lifecycleState !== 'stopped' || this.pendingDispatches.size > 0 || this.authorizedPipelineLeaseCount > 0) {
|
|
156
201
|
return;
|
|
157
202
|
}
|
|
158
|
-
|
|
159
|
-
|
|
203
|
+
this.executionChains.clear();
|
|
204
|
+
this.handlerInstances.clear();
|
|
205
|
+
this.descriptorsByEvent.clear();
|
|
206
|
+
this.discovered = false;
|
|
207
|
+
this.discoveryPromise = undefined;
|
|
208
|
+
}
|
|
209
|
+
async drainActiveSagaWork() {
|
|
210
|
+
const timeoutMs = this.shutdownDeadline.remainingTimeoutMs() ?? this.resolveShutdownDrainTimeoutMs();
|
|
211
|
+
const activeWorkCount = this.pendingDispatches.size;
|
|
212
|
+
const drained = await drainPendingSagaDispatches(this.pendingDispatches, timeoutMs);
|
|
160
213
|
if (!drained) {
|
|
161
|
-
this.
|
|
162
|
-
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWork.length)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
|
|
214
|
+
this.reportShutdownDrainTimeout(timeoutMs, activeWorkCount);
|
|
163
215
|
}
|
|
164
216
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
timeoutId = setTimeout(() => resolve(false), timeoutMs);
|
|
169
|
-
});
|
|
170
|
-
const drain = Promise.allSettled(activeWork).then(() => true);
|
|
171
|
-
try {
|
|
172
|
-
return await Promise.race([drain, timeout]);
|
|
173
|
-
} finally {
|
|
174
|
-
if (timeoutId) {
|
|
175
|
-
clearTimeout(timeoutId);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
217
|
+
reportShutdownDrainTimeout(timeoutMs, activeWorkCount) {
|
|
218
|
+
this.shutdownDrainTimeouts += 1;
|
|
219
|
+
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWorkCount)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
|
|
178
220
|
}
|
|
179
221
|
resolveShutdownDrainTimeoutMs() {
|
|
180
222
|
const timeoutMs = this.moduleOptions.shutdown?.drainTimeoutMs;
|
|
@@ -183,17 +225,6 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
183
225
|
}
|
|
184
226
|
return Math.floor(timeoutMs);
|
|
185
227
|
}
|
|
186
|
-
createDispatchContext(activeContext, descriptor, routeLabel) {
|
|
187
|
-
return {
|
|
188
|
-
[cqrsDispatchContextStateBrand]: true,
|
|
189
|
-
activeRoutes: [...(activeContext?.activeRoutes ?? []), {
|
|
190
|
-
eventType: descriptor.eventType,
|
|
191
|
-
token: descriptor.token
|
|
192
|
-
}],
|
|
193
|
-
depth: (activeContext?.depth ?? 0) + 1,
|
|
194
|
-
path: [...(activeContext?.path ?? []), routeLabel]
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
228
|
async invokeSaga(descriptor, event, context) {
|
|
198
229
|
const instance = await this.resolveHandlerInstance(descriptor.token);
|
|
199
230
|
if (!isSaga(instance)) {
|
|
@@ -234,37 +265,7 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
|
|
|
234
265
|
}
|
|
235
266
|
}
|
|
236
267
|
discoverSagaDescriptors() {
|
|
237
|
-
|
|
238
|
-
const seenByTarget = new WeakMap();
|
|
239
|
-
for (const candidate of this.discoveryCandidates()) {
|
|
240
|
-
const metadata = getSagaMetadata(candidate.targetType);
|
|
241
|
-
if (!metadata) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
if (candidate.scope !== 'singleton') {
|
|
245
|
-
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');
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
|
|
249
|
-
for (const eventType of metadata.eventTypes) {
|
|
250
|
-
if (seenEventTypes.has(eventType)) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
seenEventTypes.add(eventType);
|
|
254
|
-
const descriptors = descriptorsByEvent.get(eventType) ?? [];
|
|
255
|
-
if (!descriptors.some(descriptor => descriptor.targetType === candidate.targetType)) {
|
|
256
|
-
descriptors.push({
|
|
257
|
-
eventType,
|
|
258
|
-
moduleName: candidate.moduleName,
|
|
259
|
-
targetType: candidate.targetType,
|
|
260
|
-
token: candidate.token
|
|
261
|
-
});
|
|
262
|
-
descriptorsByEvent.set(eventType, descriptors);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
seenByTarget.set(candidate.targetType, seenEventTypes);
|
|
266
|
-
}
|
|
267
|
-
return descriptorsByEvent;
|
|
268
|
+
return discoverSagaDescriptors(this.discoveryCandidates(), this.logger);
|
|
268
269
|
}
|
|
269
270
|
static {
|
|
270
271
|
_initClass();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SagaContinuationScope {
|
|
2
|
+
readonly queue: Array<() => Promise<void>>;
|
|
3
|
+
}
|
|
4
|
+
export interface SagaDispatchOptions<TDrainAuthorization extends symbol> {
|
|
5
|
+
readonly afterSagas?: () => Promise<void>;
|
|
6
|
+
readonly drainAuthorization?: TDrainAuthorization;
|
|
7
|
+
}
|
|
8
|
+
export interface SagaContinuationTask<TToken> {
|
|
9
|
+
readonly run: () => Promise<void>;
|
|
10
|
+
readonly token: TToken;
|
|
11
|
+
}
|
|
12
|
+
export declare function runSerializedSagaContinuationTasks<TToken>(tasks: readonly SagaContinuationTask<TToken>[]): Promise<void>;
|
|
13
|
+
export declare function drainSagaContinuations(scope: SagaContinuationScope): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=saga-continuation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saga-continuation.d.ts","sourceRoot":"","sources":["../../src/buses/saga-continuation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB,CAAC,mBAAmB,SAAS,MAAM;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,mBAAmB,CAAC;CACnD;AAED,MAAM,WAAW,oBAAoB,CAAC,MAAM;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,kCAAkC,CAAC,MAAM,EAC7D,KAAK,EAAE,SAAS,oBAAoB,CAAC,MAAM,CAAC,EAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYxF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function runSerializedSagaContinuationTasks(tasks) {
|
|
2
|
+
const completions = [];
|
|
3
|
+
const settledChains = new Map();
|
|
4
|
+
for (const task of tasks) {
|
|
5
|
+
const previous = settledChains.get(task.token) ?? Promise.resolve();
|
|
6
|
+
const current = previous.then(task.run);
|
|
7
|
+
completions.push(current);
|
|
8
|
+
settledChains.set(task.token, current.catch(() => undefined));
|
|
9
|
+
}
|
|
10
|
+
await Promise.all(settledChains.values());
|
|
11
|
+
await Promise.all(completions);
|
|
12
|
+
}
|
|
13
|
+
export async function drainSagaContinuations(scope) {
|
|
14
|
+
try {
|
|
15
|
+
while (scope.queue.length > 0) {
|
|
16
|
+
const continuation = scope.queue.shift();
|
|
17
|
+
if (continuation) {
|
|
18
|
+
await continuation();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
scope.queue.length = 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
import type { CqrsDispatchContext, SagaDescriptor } from '../types.js';
|
|
2
|
+
import type { SagaContinuationScope } from './saga-continuation.js';
|
|
3
|
+
/** Result of entering one guarded saga route. */
|
|
4
|
+
export interface SagaTopologyEntry {
|
|
5
|
+
readonly continuationScope: SagaContinuationScope;
|
|
6
|
+
readonly context: CqrsDispatchContext;
|
|
7
|
+
readonly ownsContinuationScope: boolean;
|
|
8
|
+
readonly reentrantToken: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates and enters one saga route using private immutable context state.
|
|
12
|
+
*
|
|
13
|
+
* @param context Opaque context passed through the active CQRS pipeline.
|
|
14
|
+
* @param descriptor Saga route selected for the current event.
|
|
15
|
+
* @returns The next opaque context.
|
|
16
|
+
*/
|
|
17
|
+
export declare function enterSagaTopology(context: CqrsDispatchContext | undefined, descriptor: SagaDescriptor): SagaTopologyEntry;
|
|
18
|
+
//# 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;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAIpE,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC;IAClD,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IACxC,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,CAuCnB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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.
|
|
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
|
+
const reentrantToken = activeTopology?.activeRoutes.some(route => route.token === descriptor.token) ?? false;
|
|
20
|
+
if (reenteredRoute) {
|
|
21
|
+
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeTopology?.path ?? []), routeLabel].join(' -> ')}.`);
|
|
22
|
+
}
|
|
23
|
+
if ((activeTopology?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
|
|
24
|
+
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.');
|
|
25
|
+
}
|
|
26
|
+
const continuationScope = internalState?.sagaContinuationScope ?? {
|
|
27
|
+
queue: []
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
continuationScope,
|
|
31
|
+
context: createInternalCqrsDispatchContext({
|
|
32
|
+
publishDrainToken: internalState?.publishDrainToken,
|
|
33
|
+
sagaContinuationScope: continuationScope,
|
|
34
|
+
sagaTopology: {
|
|
35
|
+
activeRoutes: [...(activeTopology?.activeRoutes ?? []), {
|
|
36
|
+
eventType: descriptor.eventType,
|
|
37
|
+
token: descriptor.token
|
|
38
|
+
}],
|
|
39
|
+
depth: (activeTopology?.depth ?? 0) + 1,
|
|
40
|
+
path: [...(activeTopology?.path ?? []), routeLabel]
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
ownsContinuationScope: internalState?.sagaContinuationScope === undefined,
|
|
44
|
+
reentrantToken
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Coordinates one absolute CQRS shutdown deadline across lifecycle drain hooks. */
|
|
2
|
+
export declare class CqrsShutdownDeadline {
|
|
3
|
+
private deadlineMs;
|
|
4
|
+
/**
|
|
5
|
+
* Starts the shared deadline once.
|
|
6
|
+
*
|
|
7
|
+
* @param timeoutMs Configured CQRS shutdown drain duration.
|
|
8
|
+
*/
|
|
9
|
+
start(timeoutMs: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the remaining duration from the shared deadline.
|
|
12
|
+
*
|
|
13
|
+
* @returns Remaining duration in milliseconds, or `undefined` before shutdown starts.
|
|
14
|
+
*/
|
|
15
|
+
remainingTimeoutMs(): number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the absolute timestamp for the shared deadline.
|
|
18
|
+
*
|
|
19
|
+
* @returns Absolute deadline in milliseconds, or `undefined` before shutdown starts.
|
|
20
|
+
*/
|
|
21
|
+
deadlineAtMs(): number | undefined;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=shutdown-deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown-deadline.d.ts","sourceRoot":"","sources":["../../src/buses/shutdown-deadline.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,UAAU,CAAqB;IAEvC;;;;OAIG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B;;;;OAIG;IACH,kBAAkB,IAAI,MAAM,GAAG,SAAS;IAIxC;;;;OAIG;IACH,YAAY,IAAI,MAAM,GAAG,SAAS;CAGnC"}
|