@fluojs/cqrs 2.0.0 → 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.
Files changed (45) hide show
  1. package/README.ko.md +25 -5
  2. package/README.md +25 -5
  3. package/dist/buses/command-bus.d.ts +10 -0
  4. package/dist/buses/command-bus.d.ts.map +1 -1
  5. package/dist/buses/command-bus.js +14 -0
  6. package/dist/buses/event-bus.d.ts +11 -2
  7. package/dist/buses/event-bus.d.ts.map +1 -1
  8. package/dist/buses/event-bus.js +40 -8
  9. package/dist/buses/publish-drain-tracker.d.ts.map +1 -1
  10. package/dist/buses/publish-drain-tracker.js +3 -0
  11. package/dist/buses/query-bus.d.ts +10 -0
  12. package/dist/buses/query-bus.d.ts.map +1 -1
  13. package/dist/buses/query-bus.js +14 -0
  14. package/dist/buses/saga-bus.d.ts +15 -2
  15. package/dist/buses/saga-bus.d.ts.map +1 -1
  16. package/dist/buses/saga-bus.js +87 -27
  17. package/dist/buses/saga-continuation.d.ts +14 -0
  18. package/dist/buses/saga-continuation.d.ts.map +1 -0
  19. package/dist/buses/saga-continuation.js +24 -0
  20. package/dist/buses/saga-topology.d.ts +4 -1
  21. package/dist/buses/saga-topology.d.ts.map +1 -1
  22. package/dist/buses/saga-topology.js +9 -2
  23. package/dist/buses/shutdown-deadline.d.ts +23 -0
  24. package/dist/buses/shutdown-deadline.d.ts.map +1 -0
  25. package/dist/buses/shutdown-deadline.js +31 -0
  26. package/dist/decorators.d.ts.map +1 -1
  27. package/dist/decorators.js +15 -1
  28. package/dist/discovery.d.ts +2 -0
  29. package/dist/discovery.d.ts.map +1 -1
  30. package/dist/discovery.js +80 -4
  31. package/dist/dispatch-context.d.ts +2 -0
  32. package/dist/dispatch-context.d.ts.map +1 -1
  33. package/dist/dispatch-context.js +1 -0
  34. package/dist/errors.d.ts +4 -2
  35. package/dist/errors.d.ts.map +1 -1
  36. package/dist/errors.js +4 -2
  37. package/dist/module.d.ts +4 -1
  38. package/dist/module.d.ts.map +1 -1
  39. package/dist/module.js +17 -4
  40. package/dist/status.d.ts +4 -0
  41. package/dist/status.d.ts.map +1 -1
  42. package/dist/status.js +10 -6
  43. package/dist/types.d.ts +2 -0
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +7 -7
@@ -10,8 +10,10 @@ import { CqrsBusBase } from '../discovery.js';
10
10
  import { SagaExecutionError } from '../errors.js';
11
11
  import { createIsolatedEvent } from '../event-clone.js';
12
12
  import { CQRS_MODULE_OPTIONS } from '../tokens.js';
13
+ import { drainSagaContinuations, runSerializedSagaContinuationTasks } from './saga-continuation.js';
13
14
  import { discoverSagaDescriptors } from './saga-discovery.js';
14
15
  import { drainPendingSagaDispatches } from './saga-drain.js';
16
+ import { CqrsShutdownDeadline } from './shutdown-deadline.js';
15
17
  import { enterSagaTopology } from './saga-topology.js';
16
18
  const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
17
19
 
@@ -39,7 +41,7 @@ function toErrorMessage(error) {
39
41
  let _CqrsSagaLifecycleSer;
40
42
  class CqrsSagaLifecycleService extends CqrsBusBase {
41
43
  static {
42
- [_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION)], [], 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;
43
45
  }
44
46
  descriptorsByEvent = new Map();
45
47
  discoveryPromise;
@@ -47,11 +49,13 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
47
49
  executionChains = new Map();
48
50
  lifecycleState = 'created';
49
51
  pendingDispatches = new Set();
52
+ authorizedPipelineLeaseCount = 0;
50
53
  shutdownDrainTimeouts = 0;
51
54
  unregisterShutdownStartCleanup;
52
- constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
55
+ constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined, shutdownDeadline = new CqrsShutdownDeadline()) {
53
56
  super(runtimeContainer, compiledModules, logger);
54
57
  this.moduleOptions = moduleOptions;
58
+ this.shutdownDeadline = shutdownDeadline;
55
59
  this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
56
60
  this.markApplicationShutdownStarted();
57
61
  });
@@ -71,12 +75,8 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
71
75
  this.unregisterShutdownStartCleanup?.();
72
76
  this.unregisterShutdownStartCleanup = undefined;
73
77
  await this.drainActiveSagaWork();
74
- this.executionChains.clear();
75
- this.handlerInstances.clear();
76
- this.descriptorsByEvent.clear();
77
- this.discovered = false;
78
- this.discoveryPromise = undefined;
79
78
  this.lifecycleState = 'stopped';
79
+ this.clearStoppedSagaGraphIfQuiescent();
80
80
  }
81
81
 
82
82
  /**
@@ -85,11 +85,12 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
85
85
  * @returns Current discovery state, in-flight execution count, lifecycle state, and discovered saga count.
86
86
  */
87
87
  getRuntimeSnapshot() {
88
+ const stopped = this.lifecycleState === 'stopped';
88
89
  return {
89
- discovered: this.discovered,
90
+ discovered: stopped ? false : this.discovered,
90
91
  inFlightSagaExecutions: this.pendingDispatches.size,
91
92
  lifecycleState: this.lifecycleState,
92
- 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,
93
94
  shutdownDrainTimeouts: this.shutdownDrainTimeouts
94
95
  };
95
96
  }
@@ -100,14 +101,55 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
100
101
  * @param event Event instance that may trigger one or more sagas.
101
102
  * @returns A promise that resolves once all matching saga chains for the event complete.
102
103
  */
103
- async dispatch(event, context, drainAuthorization) {
104
- this.assertAcceptingNewWork(drainAuthorization);
104
+ async dispatch(event, context, 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) {
105
127
  await this.ensureDiscovered();
106
- const descriptors = this.matchSagaDescriptors(event);
107
- if (descriptors.length === 0) {
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?.();
134
+ return;
135
+ }
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
+ });
108
149
  return;
109
150
  }
110
- await Promise.all(descriptors.map(descriptor => this.dispatchWithOrdering(descriptor, event, context)));
151
+ await Promise.all(entries.map(entry => this.dispatchWithOrdering(entry.descriptor, event, entry.topology)));
152
+ await options.afterSagas?.();
111
153
  }
112
154
  assertAcceptingNewWork(drainAuthorization) {
113
155
  if ((this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') && drainAuthorization !== CQRS_SAGA_DRAIN_AUTHORIZATION) {
@@ -115,6 +157,7 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
115
157
  }
116
158
  }
117
159
  markApplicationShutdownStarted() {
160
+ this.shutdownDeadline.start(this.resolveShutdownDrainTimeoutMs());
118
161
  if (this.lifecycleState !== 'stopped') {
119
162
  this.lifecycleState = 'stopping';
120
163
  }
@@ -128,26 +171,43 @@ class CqrsSagaLifecycleService extends CqrsBusBase {
128
171
  }
129
172
  return descriptors;
130
173
  }
131
- async dispatchWithOrdering(descriptor, event, activeContext) {
132
- const topology = enterSagaTopology(activeContext, descriptor);
133
- if (topology.reentrantToken) {
134
- await this.invokeSaga(descriptor, event, topology.context);
135
- return;
136
- }
174
+ async dispatchWithOrdering(descriptor, event, topology) {
137
175
  const previous = this.executionChains.get(descriptor.token) ?? Promise.resolve();
138
- const current = previous.then(async () => {
139
- await this.invokeSaga(descriptor, event, topology.context);
140
- });
141
- this.executionChains.set(descriptor.token, current.catch(() => undefined));
142
- 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);
143
179
  try {
144
180
  await current;
181
+ if (topology.ownsContinuationScope) {
182
+ await drainSagaContinuations(topology.continuationScope);
183
+ }
184
+ } finally {
185
+ if (this.executionChains.get(descriptor.token) === settled) {
186
+ this.executionChains.delete(descriptor.token);
187
+ }
188
+ }
189
+ }
190
+ async trackPendingDispatch(dispatch) {
191
+ this.pendingDispatches.add(dispatch);
192
+ try {
193
+ await dispatch;
145
194
  } finally {
146
- this.pendingDispatches.delete(current);
195
+ this.pendingDispatches.delete(dispatch);
196
+ this.clearStoppedSagaGraphIfQuiescent();
147
197
  }
148
198
  }
199
+ clearStoppedSagaGraphIfQuiescent() {
200
+ if (this.lifecycleState !== 'stopped' || this.pendingDispatches.size > 0 || this.authorizedPipelineLeaseCount > 0) {
201
+ return;
202
+ }
203
+ this.executionChains.clear();
204
+ this.handlerInstances.clear();
205
+ this.descriptorsByEvent.clear();
206
+ this.discovered = false;
207
+ this.discoveryPromise = undefined;
208
+ }
149
209
  async drainActiveSagaWork() {
150
- const timeoutMs = this.resolveShutdownDrainTimeoutMs();
210
+ const timeoutMs = this.shutdownDeadline.remainingTimeoutMs() ?? this.resolveShutdownDrainTimeoutMs();
151
211
  const activeWorkCount = this.pendingDispatches.size;
152
212
  const drained = await drainPendingSagaDispatches(this.pendingDispatches, timeoutMs);
153
213
  if (!drained) {
@@ -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
+ }
@@ -1,7 +1,10 @@
1
1
  import type { CqrsDispatchContext, SagaDescriptor } from '../types.js';
2
+ import type { SagaContinuationScope } from './saga-continuation.js';
2
3
  /** Result of entering one guarded saga route. */
3
4
  export interface SagaTopologyEntry {
5
+ readonly continuationScope: SagaContinuationScope;
4
6
  readonly context: CqrsDispatchContext;
7
+ readonly ownsContinuationScope: boolean;
5
8
  readonly reentrantToken: boolean;
6
9
  }
7
10
  /**
@@ -9,7 +12,7 @@ export interface SagaTopologyEntry {
9
12
  *
10
13
  * @param context Opaque context passed through the active CQRS pipeline.
11
14
  * @param descriptor Saga route selected for the current event.
12
- * @returns The next opaque context and whether the same provider token is already active.
15
+ * @returns The next opaque context.
13
16
  */
14
17
  export declare function enterSagaTopology(context: CqrsDispatchContext | undefined, descriptor: SagaDescriptor): SagaTopologyEntry;
15
18
  //# sourceMappingURL=saga-topology.d.ts.map
@@ -1 +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"}
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"}
@@ -9,22 +9,28 @@ const MAX_NESTED_SAGA_DEPTH = 32;
9
9
  *
10
10
  * @param context Opaque context passed through the active CQRS pipeline.
11
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.
12
+ * @returns The next opaque context.
13
13
  */
14
14
  export function enterSagaTopology(context, descriptor) {
15
15
  const internalState = getInternalCqrsDispatchContextState(context);
16
16
  const activeTopology = internalState?.sagaTopology;
17
17
  const routeLabel = `${descriptor.targetType.name}(${descriptor.eventType.name})`;
18
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;
19
20
  if (reenteredRoute) {
20
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(' -> ')}.`);
21
22
  }
22
23
  if ((activeTopology?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
23
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.');
24
25
  }
26
+ const continuationScope = internalState?.sagaContinuationScope ?? {
27
+ queue: []
28
+ };
25
29
  return {
30
+ continuationScope,
26
31
  context: createInternalCqrsDispatchContext({
27
32
  publishDrainToken: internalState?.publishDrainToken,
33
+ sagaContinuationScope: continuationScope,
28
34
  sagaTopology: {
29
35
  activeRoutes: [...(activeTopology?.activeRoutes ?? []), {
30
36
  eventType: descriptor.eventType,
@@ -34,6 +40,7 @@ export function enterSagaTopology(context, descriptor) {
34
40
  path: [...(activeTopology?.path ?? []), routeLabel]
35
41
  }
36
42
  }),
37
- reentrantToken: activeTopology?.activeRoutes.some(route => route.token === descriptor.token) ?? false
43
+ ownsContinuationScope: internalState?.sagaContinuationScope === undefined,
44
+ reentrantToken
38
45
  };
39
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"}
@@ -0,0 +1,31 @@
1
+ /** Coordinates one absolute CQRS shutdown deadline across lifecycle drain hooks. */
2
+ export class CqrsShutdownDeadline {
3
+ deadlineMs;
4
+
5
+ /**
6
+ * Starts the shared deadline once.
7
+ *
8
+ * @param timeoutMs Configured CQRS shutdown drain duration.
9
+ */
10
+ start(timeoutMs) {
11
+ this.deadlineMs ??= Date.now() + timeoutMs;
12
+ }
13
+
14
+ /**
15
+ * Returns the remaining duration from the shared deadline.
16
+ *
17
+ * @returns Remaining duration in milliseconds, or `undefined` before shutdown starts.
18
+ */
19
+ remainingTimeoutMs() {
20
+ return this.deadlineMs === undefined ? undefined : Math.max(0, this.deadlineMs - Date.now());
21
+ }
22
+
23
+ /**
24
+ * Returns the absolute timestamp for the shared deadline.
25
+ *
26
+ * @returns Absolute deadline in milliseconds, or `undefined` before shutdown starts.
27
+ */
28
+ deadlineAtMs() {
29
+ return this.deadlineMs;
30
+ }
31
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAEV,WAAW,EACX,aAAa,EAGb,SAAS,EAEV,MAAM,YAAY,CAAC;AAEpB,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AA0DpF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,kBAAkB,CAU3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAUrE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,aAAa,GAAG,kBAAkB,CAUzE;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,gBAAgB,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,GAAG,kBAAkB,CAYnG"}
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAEV,WAAW,EACX,aAAa,EAGb,SAAS,EAEV,MAAM,YAAY,CAAC;AAEpB,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AA2EpF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,kBAAkB,CAU3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAUrE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,aAAa,GAAG,kBAAkB,CAUzE;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,gBAAgB,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,GAAG,kBAAkB,CAYnG"}
@@ -31,6 +31,20 @@ function defineStandardSagaMetadata(metadata, sagaMetadata) {
31
31
  eventTypes: [...sagaMetadata.eventTypes]
32
32
  };
33
33
  }
34
+ function isConstructableClass(value) {
35
+ if (typeof value !== 'function') {
36
+ return false;
37
+ }
38
+ try {
39
+ Reflect.construct(Object, [], value);
40
+ return true;
41
+ } catch (error) {
42
+ if (error instanceof TypeError) {
43
+ return false;
44
+ }
45
+ throw error;
46
+ }
47
+ }
34
48
  function normalizeSagaEventTypes(eventTypeOrTypes) {
35
49
  const eventTypes = Array.isArray(eventTypeOrTypes) ? eventTypeOrTypes : [eventTypeOrTypes];
36
50
  const uniqueEventTypes = Array.from(new Set(eventTypes));
@@ -38,7 +52,7 @@ function normalizeSagaEventTypes(eventTypeOrTypes) {
38
52
  throw new Error('@Saga() requires at least one event type.');
39
53
  }
40
54
  for (const eventType of uniqueEventTypes) {
41
- if (typeof eventType !== 'function') {
55
+ if (!isConstructableClass(eventType)) {
42
56
  throw new Error('@Saga() event types must be class constructors.');
43
57
  }
44
58
  }
@@ -52,6 +52,8 @@ export declare abstract class CqrsBusBase {
52
52
  protected readonly handlerInstances: Map<Token, Promise<unknown>>;
53
53
  constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger);
54
54
  protected discoveryCandidates(): DiscoveryCandidate[];
55
+ private resolveProviderDiscoveryCandidate;
56
+ private createUnresolvedProviderDiscoveryCandidate;
55
57
  protected preloadHandlerInstance(token: Token): Promise<void>;
56
58
  protected resolveHandlerInstance(token: Token): Promise<unknown>;
57
59
  }
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAQzE;;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;AA4CD;;;;;;;;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;IA2CrD,OAAO,CAAC,iCAAiC;IA+CzC,OAAO,CAAC,0CAA0C;cAmBlC,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,5 +1,9 @@
1
1
  import { formatTokenName } from '@fluojs/core';
2
- import { getClassDiMetadata } from '@fluojs/core/internal';
2
+ import { getRuntimeClassDiMetadata } from '@fluojs/runtime/internal';
3
+ import { getCommandHandlerMetadata } from './metadata.js';
4
+ import { getEventHandlerMetadata } from './metadata.js';
5
+ import { getQueryHandlerMetadata } from './metadata.js';
6
+ import { getSagaMetadata } from './metadata.js';
3
7
 
4
8
  /**
5
9
  * Describes the discovery candidate contract.
@@ -7,16 +11,26 @@ import { getClassDiMetadata } from '@fluojs/core/internal';
7
11
 
8
12
  function scopeFromProvider(provider) {
9
13
  if (typeof provider === 'function') {
10
- return getClassDiMetadata(provider)?.scope ?? 'singleton';
14
+ return getRuntimeClassDiMetadata(provider)?.scope ?? 'singleton';
11
15
  }
12
16
  if ('useClass' in provider) {
13
- return provider.scope ?? getClassDiMetadata(provider.useClass)?.scope ?? 'singleton';
17
+ return provider.scope ?? getRuntimeClassDiMetadata(provider.useClass)?.scope ?? 'singleton';
14
18
  }
15
- return 'scope' in provider ? provider.scope ?? 'singleton' : 'singleton';
19
+ if ('useFactory' in provider) {
20
+ const resolverClass = provider.resolverClass;
21
+ return provider.scope ?? (resolverClass ? getRuntimeClassDiMetadata(resolverClass)?.scope : undefined) ?? 'singleton';
22
+ }
23
+ return 'singleton';
16
24
  }
17
25
  function isClassProvider(provider) {
18
26
  return typeof provider === 'object' && provider !== null && 'useClass' in provider;
19
27
  }
28
+ function isFactoryOrValueProvider(provider) {
29
+ return typeof provider === 'object' && provider !== null && ('useFactory' in provider || 'useValue' in provider);
30
+ }
31
+ function hasCqrsMetadata(targetType) {
32
+ return getCommandHandlerMetadata(targetType) !== undefined || getQueryHandlerMetadata(targetType) !== undefined || getEventHandlerMetadata(targetType) !== undefined || getSagaMetadata(targetType) !== undefined;
33
+ }
20
34
 
21
35
  /**
22
36
  * Create duplicate handler message.
@@ -57,6 +71,7 @@ export class CqrsBusBase {
57
71
  }
58
72
  discoveryCandidates() {
59
73
  const candidates = [];
74
+ const providerCandidates = [];
60
75
  for (const compiledModule of this.compiledModules) {
61
76
  for (const provider of compiledModule.definition.providers ?? []) {
62
77
  if (typeof provider === 'function') {
@@ -75,11 +90,72 @@ export class CqrsBusBase {
75
90
  targetType: provider.useClass,
76
91
  token: provider.provide
77
92
  });
93
+ continue;
78
94
  }
95
+ if (isFactoryOrValueProvider(provider)) {
96
+ providerCandidates.push({
97
+ moduleName: compiledModule.type.name,
98
+ provider
99
+ });
100
+ }
101
+ }
102
+ }
103
+ for (const candidate of providerCandidates) {
104
+ const resolvedCandidate = this.resolveProviderDiscoveryCandidate(candidate);
105
+ if (resolvedCandidate) {
106
+ candidates.push(resolvedCandidate);
79
107
  }
80
108
  }
81
109
  return candidates;
82
110
  }
111
+ resolveProviderDiscoveryCandidate(candidate) {
112
+ const provider = candidate.provider;
113
+ if (!('provide' in provider)) {
114
+ return undefined;
115
+ }
116
+ const scope = scopeFromProvider(provider);
117
+ const token = provider.provide;
118
+ if (scope !== 'singleton') {
119
+ return this.createUnresolvedProviderDiscoveryCandidate(candidate.moduleName, token, scope);
120
+ }
121
+ if ('useValue' in provider) {
122
+ const instance = provider.useValue;
123
+ if (typeof instance !== 'object' || instance === null) {
124
+ return undefined;
125
+ }
126
+ const targetType = instance.constructor;
127
+ if (typeof targetType !== 'function' || !hasCqrsMetadata(targetType)) {
128
+ return undefined;
129
+ }
130
+ return {
131
+ moduleName: candidate.moduleName,
132
+ scope,
133
+ targetType,
134
+ token
135
+ };
136
+ }
137
+ if (typeof token !== 'function' || !hasCqrsMetadata(token)) {
138
+ return undefined;
139
+ }
140
+ return {
141
+ moduleName: candidate.moduleName,
142
+ scope,
143
+ targetType: token,
144
+ token
145
+ };
146
+ }
147
+ createUnresolvedProviderDiscoveryCandidate(moduleName, token, scope) {
148
+ const tokenType = typeof token === 'function' ? token : undefined;
149
+ if (!tokenType) {
150
+ return undefined;
151
+ }
152
+ return {
153
+ moduleName,
154
+ scope,
155
+ targetType: tokenType,
156
+ token
157
+ };
158
+ }
83
159
  async preloadHandlerInstance(token) {
84
160
  if (this.handlerInstances.has(token)) {
85
161
  return;
@@ -1,4 +1,5 @@
1
1
  import type { Token } from '@fluojs/core';
2
+ import type { SagaContinuationScope } from './buses/saga-continuation.js';
2
3
  import type { CqrsDispatchContext, CqrsEventType } from './types.js';
3
4
  /** One active saga route retained in private CQRS dispatch state. */
4
5
  export interface CqrsDispatchRoute {
@@ -14,6 +15,7 @@ export interface CqrsSagaTopologyState {
14
15
  /** Private state carried by an internally created dispatch context. */
15
16
  export interface InternalCqrsDispatchContextState {
16
17
  readonly publishDrainToken: symbol | undefined;
18
+ readonly sagaContinuationScope: SagaContinuationScope | undefined;
17
19
  readonly sagaTopology: CqrsSagaTopologyState | undefined;
18
20
  }
19
21
  /**
@@ -1 +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"}
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,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1E,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,qBAAqB,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,GAAG,SAAS,CAAC;CAC1D;AAcD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,gCAAgC,GAAG,mBAAmB,CAW9G;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,mBAAmB,GAAG,SAAS,GACvC,gCAAgC,GAAG,SAAS,CAE9C"}
@@ -26,6 +26,7 @@ export function createInternalCqrsDispatchContext(state) {
26
26
  const context = Object.freeze({});
27
27
  internalContextStates.set(context, Object.freeze({
28
28
  publishDrainToken: state.publishDrainToken,
29
+ sagaContinuationScope: state.sagaContinuationScope,
29
30
  sagaTopology: state.sagaTopology ? freezeSagaTopology(state.sagaTopology) : undefined
30
31
  }));
31
32
  return context;
package/dist/errors.d.ts CHANGED
@@ -36,9 +36,11 @@ export declare class DuplicateQueryHandlerError extends FluoError {
36
36
  constructor(message: string);
37
37
  }
38
38
  /**
39
- * Raised when conflicting event-handler registrations break CQRS discovery invariants.
39
+ * Compatibility error type retained for existing imports.
40
40
  *
41
- * Ordinary multiple `@EventHandler(...)` providers for the same event are valid and fan out.
41
+ * Event-handler discovery does not throw this error. Repeated discovery of the same singleton provider token and event
42
+ * type is silently deduplicated, while distinct singleton provider tokens remain valid fan-out routes in discovery
43
+ * order.
42
44
  */
43
45
  export declare class DuplicateEventHandlerError extends FluoError {
44
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,wFAAwF;AACxF,qBAAa,+BAAgC,SAAQ,SAAS;IAC5D;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,iFAAiF;AACjF,qBAAa,4BAA6B,SAAQ,SAAS;IACzD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,oFAAoF;AACpF,qBAAa,6BAA8B,SAAQ,SAAS;IAC1D;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,+EAA+E;AAC/E,qBAAa,0BAA2B,SAAQ,SAAS;IACvD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,SAAS;IACvD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,0EAA0E;AAC1E,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,SAAS;IAC9C;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,wFAAwF;AACxF,qBAAa,+BAAgC,SAAQ,SAAS;IAC5D;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,iFAAiF;AACjF,qBAAa,4BAA6B,SAAQ,SAAS;IACzD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,oFAAoF;AACpF,qBAAa,6BAA8B,SAAQ,SAAS;IAC1D;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,+EAA+E;AAC/E,qBAAa,0BAA2B,SAAQ,SAAS;IACvD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,SAAS;IACvD;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,0EAA0E;AAC1E,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B;AAED,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,SAAS;IAC9C;;;;OAIG;gBACS,OAAO,EAAE,MAAM;CAG5B"}
package/dist/errors.js CHANGED
@@ -57,9 +57,11 @@ export class DuplicateQueryHandlerError extends FluoError {
57
57
  }
58
58
 
59
59
  /**
60
- * Raised when conflicting event-handler registrations break CQRS discovery invariants.
60
+ * Compatibility error type retained for existing imports.
61
61
  *
62
- * Ordinary multiple `@EventHandler(...)` providers for the same event are valid and fan out.
62
+ * Event-handler discovery does not throw this error. Repeated discovery of the same singleton provider token and event
63
+ * type is silently deduplicated, while distinct singleton provider tokens remain valid fan-out routes in discovery
64
+ * order.
63
65
  */
64
66
  export class DuplicateEventHandlerError extends FluoError {
65
67
  /**
package/dist/module.d.ts CHANGED
@@ -18,7 +18,10 @@ export interface CqrsModuleOptions {
18
18
  /** Runtime module entrypoint for CQRS bus registration and handler discovery. */
19
19
  export declare class CqrsModule {
20
20
  /**
21
- * Registers the CQRS buses globally and wires them to the event-bus integration.
21
+ * Registers CQRS buses and wires them to the event-bus integration.
22
+ *
23
+ * The exported buses are global by default. Set {@link CqrsModuleOptions.global} to `false` to
24
+ * keep them visible only through modules that import this module definition.
22
25
  *
23
26
  * @param options CQRS module options including explicit handler classes and event-bus settings.
24
27
  * @returns A module definition that exports the lifecycle services and compatibility tokens.
@@ -1 +1 @@
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"}
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;AAQhE,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;AAkHD,iFAAiF;AACjF,qBAAa,UAAU;IACrB;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAkB5D"}