@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.
Files changed (44) hide show
  1. package/README.ko.md +69 -7
  2. package/README.md +69 -7
  3. package/dist/buses/command-bus.d.ts +9 -2
  4. package/dist/buses/command-bus.d.ts.map +1 -1
  5. package/dist/buses/command-bus.js +54 -24
  6. package/dist/buses/event-bus.d.ts +7 -7
  7. package/dist/buses/event-bus.d.ts.map +1 -1
  8. package/dist/buses/event-bus.js +55 -68
  9. package/dist/buses/event-handler-discovery.d.ts +12 -0
  10. package/dist/buses/event-handler-discovery.d.ts.map +1 -0
  11. package/dist/buses/event-handler-discovery.js +35 -0
  12. package/dist/buses/publish-drain-tracker.d.ts +38 -0
  13. package/dist/buses/publish-drain-tracker.d.ts.map +1 -0
  14. package/dist/buses/publish-drain-tracker.js +86 -0
  15. package/dist/buses/query-bus.d.ts +9 -2
  16. package/dist/buses/query-bus.d.ts.map +1 -1
  17. package/dist/buses/query-bus.js +54 -24
  18. package/dist/buses/saga-bus.d.ts +10 -6
  19. package/dist/buses/saga-bus.d.ts.map +1 -1
  20. package/dist/buses/saga-bus.js +41 -82
  21. package/dist/buses/saga-discovery.d.ts +12 -0
  22. package/dist/buses/saga-discovery.d.ts.map +1 -0
  23. package/dist/buses/saga-discovery.js +39 -0
  24. package/dist/buses/saga-drain.d.ts +9 -0
  25. package/dist/buses/saga-drain.d.ts.map +1 -0
  26. package/dist/buses/saga-drain.js +34 -0
  27. package/dist/buses/saga-topology.d.ts +15 -0
  28. package/dist/buses/saga-topology.d.ts.map +1 -0
  29. package/dist/buses/saga-topology.js +39 -0
  30. package/dist/discovery.d.ts +16 -0
  31. package/dist/discovery.d.ts.map +1 -1
  32. package/dist/discovery.js +16 -9
  33. package/dist/dispatch-context.d.ts +33 -0
  34. package/dist/dispatch-context.d.ts.map +1 -0
  35. package/dist/dispatch-context.js +42 -0
  36. package/dist/module.d.ts +0 -8
  37. package/dist/module.d.ts.map +1 -1
  38. package/dist/module.js +9 -2
  39. package/dist/test-setup.d.ts +2 -0
  40. package/dist/test-setup.d.ts.map +1 -0
  41. package/dist/test-setup.js +4 -0
  42. package/dist/types.d.ts +4 -11
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +5 -5
@@ -6,13 +6,15 @@ function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.descrip
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 { Inject, InvariantError } from '@fluojs/core';
8
8
  import { EVENT_BUS as FLUO_EVENT_BUS } from '@fluojs/event-bus';
9
- import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
9
+ import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
10
10
  import { CqrsBusBase } from '../discovery.js';
11
+ import { createInternalCqrsDispatchContext, getInternalCqrsDispatchContextState } from '../dispatch-context.js';
11
12
  import { createIsolatedEvent } from '../event-clone.js';
12
- import { getEventHandlerMetadata } from '../metadata.js';
13
13
  import { createCqrsPlatformStatusSnapshot } from '../status.js';
14
14
  import { CQRS_MODULE_OPTIONS } from '../tokens.js';
15
- import { CqrsSagaLifecycleService } from './saga-bus.js';
15
+ import { discoverEventHandlerDescriptors } from './event-handler-discovery.js';
16
+ import { CqrsPublishDrainTracker } from './publish-drain-tracker.js';
17
+ import { CQRS_SAGA_DRAIN_AUTHORIZATION, CqrsSagaLifecycleService } from './saga-bus.js';
16
18
  const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
17
19
  function isEventHandler(value) {
18
20
  if (typeof value !== 'object' || value === null) {
@@ -30,19 +32,23 @@ function isEventHandler(value) {
30
32
  let _CqrsEventBusService;
31
33
  class CqrsEventBusService extends CqrsBusBase {
32
34
  static {
33
- [_CqrsEventBusService, _initClass] = _applyDecs(this, [Inject(FLUO_EVENT_BUS, CqrsSagaLifecycleService, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
35
+ [_CqrsEventBusService, _initClass] = _applyDecs(this, [Inject(FLUO_EVENT_BUS, CqrsSagaLifecycleService, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
34
36
  }
35
37
  descriptors = [];
36
38
  discoveryPromise;
37
39
  discovered = false;
38
- activePublishPipelines = new Set();
39
- shutdownDrainTimeouts = 0;
40
+ publishDrainTracker;
40
41
  lifecycleState = 'created';
41
- constructor(eventBus, sagaService, runtimeContainer, compiledModules, logger, moduleOptions = {}) {
42
+ unregisterShutdownStartCleanup;
43
+ constructor(eventBus, sagaService, runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
42
44
  super(runtimeContainer, compiledModules, logger);
43
45
  this.eventBus = eventBus;
44
46
  this.sagaService = sagaService;
45
47
  this.moduleOptions = moduleOptions;
48
+ this.publishDrainTracker = new CqrsPublishDrainTracker(logger);
49
+ this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
50
+ this.markApplicationShutdownStarted();
51
+ });
46
52
  }
47
53
  async onApplicationBootstrap() {
48
54
  this.lifecycleState = 'discovering';
@@ -55,9 +61,11 @@ class CqrsEventBusService extends CqrsBusBase {
55
61
  }
56
62
  }
57
63
  async onApplicationShutdown() {
58
- this.lifecycleState = 'stopping';
59
- if (this.activePublishPipelines.size > 0) {
60
- await this.drainActivePublishPipelines();
64
+ this.markApplicationShutdownStarted();
65
+ this.unregisterShutdownStartCleanup?.();
66
+ this.unregisterShutdownStartCleanup = undefined;
67
+ if (this.publishDrainTracker.hasActivePipelines) {
68
+ await this.publishDrainTracker.drain(this.resolveShutdownDrainTimeoutMs());
61
69
  }
62
70
  this.lifecycleState = 'stopped';
63
71
  }
@@ -77,7 +85,7 @@ class CqrsEventBusService extends CqrsBusBase {
77
85
  sagaShutdownDrainTimeouts: sagaSnapshot.shutdownDrainTimeouts,
78
86
  sagasDiscovered: sagaSnapshot.sagasDiscovered,
79
87
  shutdownDrainTimeoutMs: this.resolveShutdownDrainTimeoutMs(),
80
- shutdownDrainTimeouts: this.shutdownDrainTimeouts
88
+ shutdownDrainTimeouts: this.publishDrainTracker.shutdownDrainTimeouts
81
89
  });
82
90
  }
83
91
 
@@ -91,7 +99,9 @@ class CqrsEventBusService extends CqrsBusBase {
91
99
  * @throws {InvariantError} When a discovered provider does not implement `handle(event)`.
92
100
  */
93
101
  async publish(event, context) {
94
- await this.trackPublishPipeline(this.runPublishPipeline(event, context));
102
+ this.assertAcceptingNewWork('publish', context);
103
+ const publishContext = this.createPublishContext(context);
104
+ await this.publishDrainTracker.track(this.runPublishPipeline(event, publishContext.context), publishContext.drainToken);
95
105
  }
96
106
 
97
107
  /**
@@ -102,7 +112,9 @@ class CqrsEventBusService extends CqrsBusBase {
102
112
  * @returns A promise that resolves once all events are published.
103
113
  */
104
114
  async publishAll(events, context) {
105
- await this.trackPublishPipeline(this.runPublishAllPipeline(events, context));
115
+ this.assertAcceptingNewWork('publishAll', context);
116
+ const publishContext = this.createPublishContext(context);
117
+ await this.publishDrainTracker.track(this.runPublishAllPipeline(events, publishContext.context), publishContext.drainToken);
106
118
  }
107
119
  async runPublishPipeline(event, context) {
108
120
  await this.ensureDiscovered();
@@ -113,44 +125,46 @@ class CqrsEventBusService extends CqrsBusBase {
113
125
  }
114
126
  await instance.handle(createIsolatedEvent(descriptor.eventType, event), context);
115
127
  }
116
- await this.sagaService.dispatch(event, context);
128
+ await this.sagaService.dispatch(event, context, CQRS_SAGA_DRAIN_AUTHORIZATION);
117
129
  await this.eventBus.publish(event);
118
130
  }
119
131
  async runPublishAllPipeline(events, context) {
120
132
  for (const event of events) {
121
- await this.publish(event, context);
133
+ await this.runPublishPipeline(event, context);
122
134
  }
123
135
  }
124
- async trackPublishPipeline(pipeline) {
125
- this.activePublishPipelines.add(pipeline);
126
- try {
127
- await pipeline;
128
- } finally {
129
- this.activePublishPipelines.delete(pipeline);
136
+ assertAcceptingNewWork(operation, context) {
137
+ if (this.lifecycleState === 'stopped') {
138
+ throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
139
+ }
140
+ if (this.lifecycleState === 'stopping') {
141
+ const drainToken = getInternalCqrsDispatchContextState(context)?.publishDrainToken;
142
+ if (!drainToken || !this.publishDrainTracker.isActive(drainToken)) {
143
+ throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
144
+ }
130
145
  }
131
146
  }
132
- async drainActivePublishPipelines() {
133
- const activePipelines = Array.from(this.activePublishPipelines);
134
- const timeoutMs = this.resolveShutdownDrainTimeoutMs();
135
- const drained = await this.awaitShutdownDrain(activePipelines, timeoutMs);
136
- if (!drained) {
137
- this.shutdownDrainTimeouts += 1;
138
- this.logger.warn(`CQRS event shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activePipelines.length)} active publish pipeline(s); continuing shutdown.`, 'CqrsEventBusService');
147
+ markApplicationShutdownStarted() {
148
+ if (this.lifecycleState !== 'stopped') {
149
+ this.lifecycleState = 'stopping';
139
150
  }
140
151
  }
141
- async awaitShutdownDrain(activePipelines, timeoutMs) {
142
- let timeoutId;
143
- const timeout = new Promise(resolve => {
144
- timeoutId = setTimeout(() => resolve(false), timeoutMs);
145
- });
146
- const drain = Promise.allSettled(activePipelines).then(() => true);
147
- try {
148
- return await Promise.race([drain, timeout]);
149
- } finally {
150
- if (timeoutId) {
151
- clearTimeout(timeoutId);
152
- }
152
+ createPublishContext(context) {
153
+ const internalState = getInternalCqrsDispatchContextState(context);
154
+ const drainToken = internalState?.publishDrainToken ?? Symbol('fluo.cqrs.activePublishDrain');
155
+ if (context && internalState?.publishDrainToken) {
156
+ return {
157
+ context,
158
+ drainToken
159
+ };
153
160
  }
161
+ return {
162
+ context: createInternalCqrsDispatchContext({
163
+ publishDrainToken: drainToken,
164
+ sagaTopology: internalState?.sagaTopology
165
+ }),
166
+ drainToken
167
+ };
154
168
  }
155
169
  resolveShutdownDrainTimeoutMs() {
156
170
  const timeoutMs = this.moduleOptions.shutdown?.drainTimeoutMs;
@@ -186,34 +200,7 @@ class CqrsEventBusService extends CqrsBusBase {
186
200
  }
187
201
  }
188
202
  discoverEventDescriptors() {
189
- const descriptors = [];
190
- const seenByTarget = new WeakMap();
191
- for (const candidate of this.discoveryCandidates()) {
192
- const metadata = getEventHandlerMetadata(candidate.targetType);
193
- if (!metadata) {
194
- continue;
195
- }
196
- if (candidate.scope !== 'singleton') {
197
- this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @EventHandler() but is registered with ${candidate.scope} scope. Event handlers are registered only for singleton providers.`, 'CqrsEventBusService');
198
- continue;
199
- }
200
- const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
201
- if (seenEventTypes.has(metadata.eventType)) {
202
- continue;
203
- }
204
- seenEventTypes.add(metadata.eventType);
205
- seenByTarget.set(candidate.targetType, seenEventTypes);
206
- const alreadyRegistered = descriptors.some(descriptor => descriptor.eventType === metadata.eventType && descriptor.targetType === candidate.targetType);
207
- if (!alreadyRegistered) {
208
- descriptors.push({
209
- eventType: metadata.eventType,
210
- moduleName: candidate.moduleName,
211
- targetType: candidate.targetType,
212
- token: candidate.token
213
- });
214
- }
215
- }
216
- return descriptors;
203
+ return discoverEventHandlerDescriptors(this.discoveryCandidates(), this.logger);
217
204
  }
218
205
  static {
219
206
  _initClass();
@@ -0,0 +1,12 @@
1
+ import type { ApplicationLogger } from '@fluojs/runtime';
2
+ import type { DiscoveryCandidate } from '../discovery.js';
3
+ import type { EventHandlerDescriptor } from '../types.js';
4
+ /**
5
+ * Discovers singleton event-handler 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 Event-handler descriptors in discovery order.
10
+ */
11
+ export declare function discoverEventHandlerDescriptors(candidates: readonly DiscoveryCandidate[], logger: ApplicationLogger): EventHandlerDescriptor[];
12
+ //# sourceMappingURL=event-handler-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-handler-discovery.d.ts","sourceRoot":"","sources":["../../src/buses/event-handler-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,EAAiB,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,MAAM,EAAE,iBAAiB,GACxB,sBAAsB,EAAE,CAoC1B"}
@@ -0,0 +1,35 @@
1
+ import { getEventHandlerMetadata } from '../metadata.js';
2
+ /**
3
+ * Discovers singleton event-handler 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 Event-handler descriptors in discovery order.
8
+ */
9
+ export function discoverEventHandlerDescriptors(candidates, logger) {
10
+ const descriptors = [];
11
+ const seenEventTypesByToken = new Map();
12
+ for (const candidate of candidates) {
13
+ const metadata = getEventHandlerMetadata(candidate.targetType);
14
+ if (!metadata) {
15
+ continue;
16
+ }
17
+ if (candidate.scope !== 'singleton') {
18
+ logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @EventHandler() but is registered with ${candidate.scope} scope. Event handlers are registered only for singleton providers.`, 'CqrsEventBusService');
19
+ continue;
20
+ }
21
+ const seenEventTypes = seenEventTypesByToken.get(candidate.token) ?? new Set();
22
+ if (seenEventTypes.has(metadata.eventType)) {
23
+ continue;
24
+ }
25
+ seenEventTypes.add(metadata.eventType);
26
+ seenEventTypesByToken.set(candidate.token, seenEventTypes);
27
+ descriptors.push({
28
+ eventType: metadata.eventType,
29
+ moduleName: candidate.moduleName,
30
+ targetType: candidate.targetType,
31
+ token: candidate.token
32
+ });
33
+ }
34
+ return descriptors;
35
+ }
@@ -0,0 +1,38 @@
1
+ import type { ApplicationLogger } from '@fluojs/runtime';
2
+ /** Tracks active CQRS publish pipelines and their shutdown-drain capabilities. */
3
+ export declare class CqrsPublishDrainTracker {
4
+ private readonly logger;
5
+ private readonly activePipelines;
6
+ private readonly activeTokens;
7
+ private drainTimeouts;
8
+ constructor(logger: ApplicationLogger);
9
+ /** Current count of bounded shutdown-drain timeouts. */
10
+ get shutdownDrainTimeouts(): number;
11
+ /** Whether at least one publish pipeline remains active. */
12
+ get hasActivePipelines(): boolean;
13
+ /**
14
+ * Returns whether a private publish capability belongs to active work.
15
+ *
16
+ * @param token Private publish-drain token from an opaque context.
17
+ * @returns `true` while at least one associated pipeline is active.
18
+ */
19
+ isActive(token: symbol): boolean;
20
+ /**
21
+ * Tracks one publish pipeline and releases its private capability on settlement.
22
+ *
23
+ * @param pipeline Publish work to track.
24
+ * @param token Private drain token associated with the pipeline.
25
+ * @returns A promise that mirrors the tracked pipeline.
26
+ */
27
+ track(pipeline: Promise<void>, token: symbol): Promise<void>;
28
+ /**
29
+ * Waits for publish quiescence within the configured shutdown bound.
30
+ *
31
+ * @param timeoutMs Maximum drain duration in milliseconds.
32
+ * @returns A promise that resolves after quiescence or timeout reporting.
33
+ */
34
+ drain(timeoutMs: number): Promise<void>;
35
+ private awaitDrain;
36
+ private waitForQuiescence;
37
+ }
38
+ //# sourceMappingURL=publish-drain-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-drain-tracker.d.ts","sourceRoot":"","sources":["../../src/buses/publish-drain-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,kFAAkF;AAClF,qBAAa,uBAAuB;IAKtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA4B;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAC1D,OAAO,CAAC,aAAa,CAAK;gBAEG,MAAM,EAAE,iBAAiB;IAEtD,wDAAwD;IACxD,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAED,4DAA4D;IAC5D,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlE;;;;;OAKG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAc/B,UAAU;YAgBV,iBAAiB;CAKhC"}
@@ -0,0 +1,86 @@
1
+ /** Tracks active CQRS publish pipelines and their shutdown-drain capabilities. */
2
+ export class CqrsPublishDrainTracker {
3
+ activePipelines = new Set();
4
+ activeTokens = new Map();
5
+ drainTimeouts = 0;
6
+ constructor(logger) {
7
+ this.logger = logger;
8
+ }
9
+
10
+ /** Current count of bounded shutdown-drain timeouts. */
11
+ get shutdownDrainTimeouts() {
12
+ return this.drainTimeouts;
13
+ }
14
+
15
+ /** Whether at least one publish pipeline remains active. */
16
+ get hasActivePipelines() {
17
+ return this.activePipelines.size > 0;
18
+ }
19
+
20
+ /**
21
+ * Returns whether a private publish capability belongs to active work.
22
+ *
23
+ * @param token Private publish-drain token from an opaque context.
24
+ * @returns `true` while at least one associated pipeline is active.
25
+ */
26
+ isActive(token) {
27
+ return (this.activeTokens.get(token) ?? 0) > 0;
28
+ }
29
+
30
+ /**
31
+ * Tracks one publish pipeline and releases its private capability on settlement.
32
+ *
33
+ * @param pipeline Publish work to track.
34
+ * @param token Private drain token associated with the pipeline.
35
+ * @returns A promise that mirrors the tracked pipeline.
36
+ */
37
+ async track(pipeline, token) {
38
+ this.activePipelines.add(pipeline);
39
+ this.activeTokens.set(token, (this.activeTokens.get(token) ?? 0) + 1);
40
+ try {
41
+ await pipeline;
42
+ } finally {
43
+ this.activePipelines.delete(pipeline);
44
+ const activeCount = this.activeTokens.get(token) ?? 0;
45
+ if (activeCount <= 1) {
46
+ this.activeTokens.delete(token);
47
+ } else {
48
+ this.activeTokens.set(token, activeCount - 1);
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Waits for publish quiescence within the configured shutdown bound.
55
+ *
56
+ * @param timeoutMs Maximum drain duration in milliseconds.
57
+ * @returns A promise that resolves after quiescence or timeout reporting.
58
+ */
59
+ async drain(timeoutMs) {
60
+ const drained = await this.awaitDrain(timeoutMs);
61
+ if (drained) {
62
+ return;
63
+ }
64
+ this.drainTimeouts += 1;
65
+ this.logger.warn(`CQRS event shutdown drain exceeded ${String(timeoutMs)}ms with ${String(this.activePipelines.size)} active publish pipeline(s); continuing shutdown.`, 'CqrsEventBusService');
66
+ }
67
+ async awaitDrain(timeoutMs) {
68
+ let timeoutId;
69
+ const timeout = new Promise(resolve => {
70
+ timeoutId = setTimeout(() => resolve(false), timeoutMs);
71
+ });
72
+ const drain = this.waitForQuiescence().then(() => true);
73
+ try {
74
+ return await Promise.race([drain, timeout]);
75
+ } finally {
76
+ if (timeoutId) {
77
+ clearTimeout(timeoutId);
78
+ }
79
+ }
80
+ }
81
+ async waitForQuiescence() {
82
+ while (this.activePipelines.size > 0) {
83
+ await Promise.allSettled([...this.activePipelines]);
84
+ }
85
+ }
86
+ }
@@ -1,4 +1,5 @@
1
- import type { OnApplicationBootstrap } from '@fluojs/runtime';
1
+ import type { Container } from '@fluojs/di';
2
+ import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
2
3
  import { CqrsBusBase } from '../discovery.js';
3
4
  import type { CqrsDispatchContext, IQuery, QueryBus } from '../types.js';
4
5
  /**
@@ -7,11 +8,15 @@ import type { CqrsDispatchContext, IQuery, QueryBus } from '../types.js';
7
8
  * The query bus resolves singleton handlers only, warns on unsupported scopes,
8
9
  * and preserves the one-query-to-one-handler contract used by the CQRS surface.
9
10
  */
10
- export declare class QueryBusLifecycleService extends CqrsBusBase implements QueryBus, OnApplicationBootstrap {
11
+ export declare class QueryBusLifecycleService extends CqrsBusBase implements QueryBus, OnApplicationBootstrap, OnApplicationShutdown {
11
12
  private descriptors;
12
13
  private discoveryPromise;
13
14
  private discovered;
15
+ private lifecycleState;
16
+ private unregisterShutdownStartCleanup;
17
+ constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, registerRuntimeCleanup?: RuntimeCleanupRegistration);
14
18
  onApplicationBootstrap(): Promise<void>;
19
+ onApplicationShutdown(): Promise<void>;
15
20
  /**
16
21
  * Executes one query by dispatching it to the discovered handler for its constructor.
17
22
  *
@@ -23,6 +28,8 @@ export declare class QueryBusLifecycleService extends CqrsBusBase implements Que
23
28
  * @throws {InvariantError} When the resolved provider does not implement `execute(query)`.
24
29
  */
25
30
  execute<TQuery extends IQuery<TResult>, TResult = unknown>(query: TQuery, context?: CqrsDispatchContext): Promise<TResult>;
31
+ private assertAcceptingNewWork;
32
+ private markApplicationShutdownStarted;
26
33
  private ensureDiscovered;
27
34
  private discoverHandlers;
28
35
  private discoverQueryDescriptors;
@@ -1 +1 @@
1
- {"version":3,"file":"query-bus.d.ts","sourceRoot":"","sources":["../../src/buses/query-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAiC,MAAM,iBAAiB,CAAC;AAG7E,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EAEN,QAAQ,EAGT,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,wBAAyB,SAAQ,WAAY,YAAW,QAAQ,EAAE,sBAAsB;IACnG,OAAO,CAAC,WAAW,CAAgD;IACnE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAErB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7C;;;;;;;;;OASG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;YAmBlH,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CAmDjC"}
1
+ {"version":3,"file":"query-bus.d.ts","sourceRoot":"","sources":["../../src/buses/query-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAA4D,MAAM,iBAAiB,CAAC;AAGxG,OAAO,KAAK,EACV,mBAAmB,EACnB,MAAM,EAEN,QAAQ,EAGT,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,wBAAyB,SAAQ,WAAY,YAAW,QAAQ,EAAE,sBAAsB,EAAE,qBAAqB;IAC1H,OAAO,CAAC,WAAW,CAAgD;IACnE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,8BAA8B,CAA2B;gBAG/D,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,sBAAsB,GAAE,0BAAkD;IAStE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5C;;;;;;;;;OASG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBhI,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,8BAA8B;YAMxB,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CA2CjC"}
@@ -5,8 +5,8 @@ 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 { Inject, InvariantError } from '@fluojs/core';
8
- import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
9
- import { CqrsBusBase, createDuplicateHandlerMessage } from '../discovery.js';
8
+ import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
9
+ import { CqrsBusBase, createDuplicateHandlerMessage, isSameHandlerRegistration } from '../discovery.js';
10
10
  import { DuplicateQueryHandlerError, QueryHandlerNotFoundException } from '../errors.js';
11
11
  import { getQueryHandlerMetadata } from '../metadata.js';
12
12
  function isQueryHandler(value) {
@@ -25,13 +25,38 @@ function isQueryHandler(value) {
25
25
  let _QueryBusLifecycleSer;
26
26
  class QueryBusLifecycleService extends CqrsBusBase {
27
27
  static {
28
- [_QueryBusLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], [], 0, void 0, CqrsBusBase).c;
28
+ [_QueryBusLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
29
29
  }
30
30
  descriptors = new Map();
31
31
  discoveryPromise;
32
32
  discovered = false;
33
+ lifecycleState = 'created';
34
+ unregisterShutdownStartCleanup;
35
+ constructor(runtimeContainer, compiledModules, logger, registerRuntimeCleanup = () => () => undefined) {
36
+ super(runtimeContainer, compiledModules, logger);
37
+ this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
38
+ this.markApplicationShutdownStarted();
39
+ });
40
+ }
33
41
  async onApplicationBootstrap() {
34
- await this.ensureDiscovered();
42
+ this.lifecycleState = 'discovering';
43
+ try {
44
+ await this.ensureDiscovered();
45
+ this.lifecycleState = 'ready';
46
+ } catch (error) {
47
+ this.lifecycleState = 'failed';
48
+ throw error;
49
+ }
50
+ }
51
+ async onApplicationShutdown() {
52
+ this.markApplicationShutdownStarted();
53
+ this.unregisterShutdownStartCleanup?.();
54
+ this.unregisterShutdownStartCleanup = undefined;
55
+ this.descriptors.clear();
56
+ this.handlerInstances.clear();
57
+ this.discovered = false;
58
+ this.discoveryPromise = undefined;
59
+ this.lifecycleState = 'stopped';
35
60
  }
36
61
 
37
62
  /**
@@ -45,6 +70,7 @@ class QueryBusLifecycleService extends CqrsBusBase {
45
70
  * @throws {InvariantError} When the resolved provider does not implement `execute(query)`.
46
71
  */
47
72
  async execute(query, context) {
73
+ this.assertAcceptingNewWork('execute');
48
74
  await this.ensureDiscovered();
49
75
  const queryType = query.constructor;
50
76
  const descriptor = this.descriptors.get(queryType);
@@ -57,6 +83,16 @@ class QueryBusLifecycleService extends CqrsBusBase {
57
83
  }
58
84
  return await instance.execute(query, context);
59
85
  }
86
+ assertAcceptingNewWork(operation) {
87
+ if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
88
+ throw new InvariantError(`CQRS query bus cannot ${operation} after shutdown has started.`);
89
+ }
90
+ }
91
+ markApplicationShutdownStarted() {
92
+ if (this.lifecycleState !== 'stopped') {
93
+ this.lifecycleState = 'stopping';
94
+ }
95
+ }
60
96
  async ensureDiscovered() {
61
97
  if (this.discovered) {
62
98
  return;
@@ -82,7 +118,6 @@ class QueryBusLifecycleService extends CqrsBusBase {
82
118
  }
83
119
  discoverQueryDescriptors() {
84
120
  const descriptors = new Map();
85
- const seenByTarget = new WeakMap();
86
121
  for (const candidate of this.discoveryCandidates()) {
87
122
  const metadata = getQueryHandlerMetadata(candidate.targetType);
88
123
  if (!metadata) {
@@ -92,27 +127,22 @@ class QueryBusLifecycleService extends CqrsBusBase {
92
127
  this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @QueryHandler() but is registered with ${candidate.scope} scope. Query handlers are registered only for singleton providers.`, 'QueryBusLifecycleService');
93
128
  continue;
94
129
  }
95
- const seenQueryTypes = seenByTarget.get(candidate.targetType) ?? new Set();
96
- if (seenQueryTypes.has(metadata.queryType)) {
97
- continue;
98
- }
99
- seenQueryTypes.add(metadata.queryType);
100
- seenByTarget.set(candidate.targetType, seenQueryTypes);
101
130
  const existing = descriptors.get(metadata.queryType);
102
- if (existing && existing.targetType !== candidate.targetType) {
103
- throw new DuplicateQueryHandlerError(createDuplicateHandlerMessage('query', metadata.queryType, existing, {
104
- moduleName: candidate.moduleName,
105
- targetType: candidate.targetType
106
- }));
107
- }
108
- if (!existing) {
109
- descriptors.set(metadata.queryType, {
110
- moduleName: candidate.moduleName,
111
- queryType: metadata.queryType,
112
- targetType: candidate.targetType,
113
- token: candidate.token
114
- });
131
+ const nextDescriptor = {
132
+ moduleName: candidate.moduleName,
133
+ targetType: candidate.targetType,
134
+ token: candidate.token
135
+ };
136
+ if (existing) {
137
+ if (isSameHandlerRegistration(existing, nextDescriptor)) {
138
+ continue;
139
+ }
140
+ throw new DuplicateQueryHandlerError(createDuplicateHandlerMessage('query', metadata.queryType, existing, nextDescriptor));
115
141
  }
142
+ descriptors.set(metadata.queryType, {
143
+ queryType: metadata.queryType,
144
+ ...nextDescriptor
145
+ });
116
146
  }
117
147
  return descriptors;
118
148
  }
@@ -1,11 +1,13 @@
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
+ /** Private capability authorizing saga dispatch from an already active publish drain. */
6
+ export declare const CQRS_SAGA_DRAIN_AUTHORIZATION: unique symbol;
5
7
  /**
6
8
  * Runtime saga coordinator that discovers `@Saga()` providers and serializes execution per saga token.
7
9
  *
8
- * The service prevents re-entrant dispatch loops within the same async context and waits for
10
+ * The service prevents re-entrant dispatch loops within the same explicit dispatch context and waits for
9
11
  * in-flight saga chains during shutdown so lifecycle guarantees remain predictable.
10
12
  */
11
13
  export declare class CqrsSagaLifecycleService extends CqrsBusBase implements OnApplicationBootstrap, OnApplicationShutdown {
@@ -17,7 +19,8 @@ export declare class CqrsSagaLifecycleService extends CqrsBusBase implements OnA
17
19
  private lifecycleState;
18
20
  private readonly pendingDispatches;
19
21
  private shutdownDrainTimeouts;
20
- constructor(runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions);
22
+ private unregisterShutdownStartCleanup;
23
+ constructor(runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions, registerRuntimeCleanup?: RuntimeCleanupRegistration);
21
24
  onApplicationBootstrap(): Promise<void>;
22
25
  onApplicationShutdown(): Promise<void>;
23
26
  /**
@@ -38,13 +41,14 @@ export declare class CqrsSagaLifecycleService extends CqrsBusBase implements OnA
38
41
  * @param event Event instance that may trigger one or more sagas.
39
42
  * @returns A promise that resolves once all matching saga chains for the event complete.
40
43
  */
41
- dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext): Promise<void>;
44
+ dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext, drainAuthorization?: typeof CQRS_SAGA_DRAIN_AUTHORIZATION): Promise<void>;
45
+ private assertAcceptingNewWork;
46
+ private markApplicationShutdownStarted;
42
47
  private matchSagaDescriptors;
43
48
  private dispatchWithOrdering;
44
49
  private drainActiveSagaWork;
45
- private awaitShutdownDrain;
50
+ private reportShutdownDrainTimeout;
46
51
  private resolveShutdownDrainTimeoutMs;
47
- private createDispatchContext;
48
52
  private invokeSaga;
49
53
  private ensureDiscovered;
50
54
  private discoverHandlers;
@@ -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;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,EAAyB,MAAM,aAAa,CAAC;AAqBrG;;;;;GAKG;AACH,qBACa,wBAAyB,SAAQ,WAAY,YAAW,sBAAsB,EAAE,qBAAqB;IAa9G,OAAO,CAAC,QAAQ,CAAC,aAAa;IAZhC,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,qBAAqB,CAAK;gBAGhC,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;IAKlD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5C;;;;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;IAUD;;;;;OAKG;IACG,QAAQ,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlG,OAAO,CAAC,oBAAoB;YAYd,oBAAoB;YAyCpB,mBAAmB;YAmBnB,kBAAkB;IAiBhC,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,qBAAqB;YAYf,UAAU;YAoBV,gBAAgB;YAchB,gBAAgB;IAiB9B,OAAO,CAAC,uBAAuB;CA+ChC"}
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;AAOrG,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,EAAE,OAAO,MAAmD,CAAC;AAkBvG;;;;;GAKG;AACH,qBACa,wBAAyB,SAAQ,WAAY,YAAW,sBAAsB,EAAE,qBAAqB;IAc9G,OAAO,CAAC,QAAQ,CAAC,aAAa;IAbhC,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,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;IAStE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5C;;;;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;IAUD;;;;;OAKG;IACG,QAAQ,CAAC,MAAM,SAAS,MAAM,EAClC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,mBAAmB,EAC7B,kBAAkB,CAAC,EAAE,OAAO,6BAA6B,GACxD,OAAO,CAAC,IAAI,CAAC;IAahB,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,8BAA8B;IAMtC,OAAO,CAAC,oBAAoB;YAYd,oBAAoB;YAuBpB,mBAAmB;IAUjC,OAAO,CAAC,0BAA0B;IAQlC,OAAO,CAAC,6BAA6B;YAUvB,UAAU;YAoBV,gBAAgB;YAchB,gBAAgB;IAiB9B,OAAO,CAAC,uBAAuB;CAGhC"}