🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@fluojs/cqrs

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/cqrs - npm Package Compare versions

Comparing version
1.1.2
to
2.0.0
+12
dist/buses/event-handler-discovery.d.ts
import type { ApplicationLogger } from '@fluojs/runtime';
import type { DiscoveryCandidate } from '../discovery.js';
import type { EventHandlerDescriptor } from '../types.js';
/**
* Discovers singleton event-handler registrations by provider-token identity.
*
* @param candidates Provider registrations compiled from the application module graph.
* @param logger Application logger used for non-singleton discovery warnings.
* @returns Event-handler descriptors in discovery order.
*/
export declare function discoverEventHandlerDescriptors(candidates: readonly DiscoveryCandidate[], logger: ApplicationLogger): EventHandlerDescriptor[];
//# sourceMappingURL=event-handler-discovery.d.ts.map
{"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"}
import { getEventHandlerMetadata } from '../metadata.js';
/**
* Discovers singleton event-handler registrations by provider-token identity.
*
* @param candidates Provider registrations compiled from the application module graph.
* @param logger Application logger used for non-singleton discovery warnings.
* @returns Event-handler descriptors in discovery order.
*/
export function discoverEventHandlerDescriptors(candidates, logger) {
const descriptors = [];
const seenEventTypesByToken = new Map();
for (const candidate of candidates) {
const metadata = getEventHandlerMetadata(candidate.targetType);
if (!metadata) {
continue;
}
if (candidate.scope !== 'singleton') {
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');
continue;
}
const seenEventTypes = seenEventTypesByToken.get(candidate.token) ?? new Set();
if (seenEventTypes.has(metadata.eventType)) {
continue;
}
seenEventTypes.add(metadata.eventType);
seenEventTypesByToken.set(candidate.token, seenEventTypes);
descriptors.push({
eventType: metadata.eventType,
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
});
}
return descriptors;
}
import type { ApplicationLogger } from '@fluojs/runtime';
/** Tracks active CQRS publish pipelines and their shutdown-drain capabilities. */
export declare class CqrsPublishDrainTracker {
private readonly logger;
private readonly activePipelines;
private readonly activeTokens;
private drainTimeouts;
constructor(logger: ApplicationLogger);
/** Current count of bounded shutdown-drain timeouts. */
get shutdownDrainTimeouts(): number;
/** Whether at least one publish pipeline remains active. */
get hasActivePipelines(): boolean;
/**
* Returns whether a private publish capability belongs to active work.
*
* @param token Private publish-drain token from an opaque context.
* @returns `true` while at least one associated pipeline is active.
*/
isActive(token: symbol): boolean;
/**
* Tracks one publish pipeline and releases its private capability on settlement.
*
* @param pipeline Publish work to track.
* @param token Private drain token associated with the pipeline.
* @returns A promise that mirrors the tracked pipeline.
*/
track(pipeline: Promise<void>, token: symbol): Promise<void>;
/**
* Waits for publish quiescence within the configured shutdown bound.
*
* @param timeoutMs Maximum drain duration in milliseconds.
* @returns A promise that resolves after quiescence or timeout reporting.
*/
drain(timeoutMs: number): Promise<void>;
private awaitDrain;
private waitForQuiescence;
}
//# sourceMappingURL=publish-drain-tracker.d.ts.map
{"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"}
/** Tracks active CQRS publish pipelines and their shutdown-drain capabilities. */
export class CqrsPublishDrainTracker {
activePipelines = new Set();
activeTokens = new Map();
drainTimeouts = 0;
constructor(logger) {
this.logger = logger;
}
/** Current count of bounded shutdown-drain timeouts. */
get shutdownDrainTimeouts() {
return this.drainTimeouts;
}
/** Whether at least one publish pipeline remains active. */
get hasActivePipelines() {
return this.activePipelines.size > 0;
}
/**
* Returns whether a private publish capability belongs to active work.
*
* @param token Private publish-drain token from an opaque context.
* @returns `true` while at least one associated pipeline is active.
*/
isActive(token) {
return (this.activeTokens.get(token) ?? 0) > 0;
}
/**
* Tracks one publish pipeline and releases its private capability on settlement.
*
* @param pipeline Publish work to track.
* @param token Private drain token associated with the pipeline.
* @returns A promise that mirrors the tracked pipeline.
*/
async track(pipeline, token) {
this.activePipelines.add(pipeline);
this.activeTokens.set(token, (this.activeTokens.get(token) ?? 0) + 1);
try {
await pipeline;
} finally {
this.activePipelines.delete(pipeline);
const activeCount = this.activeTokens.get(token) ?? 0;
if (activeCount <= 1) {
this.activeTokens.delete(token);
} else {
this.activeTokens.set(token, activeCount - 1);
}
}
}
/**
* Waits for publish quiescence within the configured shutdown bound.
*
* @param timeoutMs Maximum drain duration in milliseconds.
* @returns A promise that resolves after quiescence or timeout reporting.
*/
async drain(timeoutMs) {
const drained = await this.awaitDrain(timeoutMs);
if (drained) {
return;
}
this.drainTimeouts += 1;
this.logger.warn(`CQRS event shutdown drain exceeded ${String(timeoutMs)}ms with ${String(this.activePipelines.size)} active publish pipeline(s); continuing shutdown.`, 'CqrsEventBusService');
}
async awaitDrain(timeoutMs) {
let timeoutId;
const timeout = new Promise(resolve => {
timeoutId = setTimeout(() => resolve(false), timeoutMs);
});
const drain = this.waitForQuiescence().then(() => true);
try {
return await Promise.race([drain, timeout]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
async waitForQuiescence() {
while (this.activePipelines.size > 0) {
await Promise.allSettled([...this.activePipelines]);
}
}
}
import type { ApplicationLogger } from '@fluojs/runtime';
import type { DiscoveryCandidate } from '../discovery.js';
import type { CqrsEventType, SagaDescriptor } from '../types.js';
/**
* Discovers singleton saga registrations by provider-token identity.
*
* @param candidates Provider registrations compiled from the application module graph.
* @param logger Application logger used for non-singleton discovery warnings.
* @returns Saga descriptors grouped by event type in discovery order.
*/
export declare function discoverSagaDescriptors(candidates: readonly DiscoveryCandidate[], logger: ApplicationLogger): Map<CqrsEventType, SagaDescriptor[]>;
//# sourceMappingURL=saga-discovery.d.ts.map
{"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"}
import { getSagaMetadata } from '../metadata.js';
/**
* Discovers singleton saga registrations by provider-token identity.
*
* @param candidates Provider registrations compiled from the application module graph.
* @param logger Application logger used for non-singleton discovery warnings.
* @returns Saga descriptors grouped by event type in discovery order.
*/
export function discoverSagaDescriptors(candidates, logger) {
const descriptorsByEvent = new Map();
const seenEventTypesByToken = new Map();
for (const candidate of candidates) {
const metadata = getSagaMetadata(candidate.targetType);
if (!metadata) {
continue;
}
if (candidate.scope !== 'singleton') {
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');
continue;
}
const seenEventTypes = seenEventTypesByToken.get(candidate.token) ?? new Set();
for (const eventType of metadata.eventTypes) {
if (seenEventTypes.has(eventType)) {
continue;
}
seenEventTypes.add(eventType);
const descriptors = descriptorsByEvent.get(eventType) ?? [];
descriptors.push({
eventType,
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
});
descriptorsByEvent.set(eventType, descriptors);
}
seenEventTypesByToken.set(candidate.token, seenEventTypes);
}
return descriptorsByEvent;
}
/**
* Waits until a mutable set of saga tasks becomes quiescent within one deadline.
*
* @param pendingDispatches Live saga task set owned by the lifecycle service.
* @param timeoutMs Maximum drain duration in milliseconds.
* @returns `true` when all current and late-added tasks settle before the deadline.
*/
export declare function drainPendingSagaDispatches(pendingDispatches: ReadonlySet<Promise<void>>, timeoutMs: number): Promise<boolean>;
//# sourceMappingURL=saga-drain.d.ts.map
{"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"}
/**
* Waits until a mutable set of saga tasks becomes quiescent within one deadline.
*
* @param pendingDispatches Live saga task set owned by the lifecycle service.
* @param timeoutMs Maximum drain duration in milliseconds.
* @returns `true` when all current and late-added tasks settle before the deadline.
*/
export async function drainPendingSagaDispatches(pendingDispatches, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (pendingDispatches.size > 0) {
const remainingTimeoutMs = deadline - Date.now();
if (remainingTimeoutMs <= 0) {
return false;
}
if (!(await awaitSagaTasks([...pendingDispatches], remainingTimeoutMs))) {
return false;
}
}
return true;
}
async function awaitSagaTasks(activeWork, timeoutMs) {
let timeoutId;
const timeout = new Promise(resolve => {
timeoutId = setTimeout(() => resolve(false), timeoutMs);
});
const drain = Promise.allSettled(activeWork).then(() => true);
try {
return await Promise.race([drain, timeout]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
import type { CqrsDispatchContext, SagaDescriptor } from '../types.js';
/** Result of entering one guarded saga route. */
export interface SagaTopologyEntry {
readonly context: CqrsDispatchContext;
readonly reentrantToken: boolean;
}
/**
* Validates and enters one saga route using private immutable context state.
*
* @param context Opaque context passed through the active CQRS pipeline.
* @param descriptor Saga route selected for the current event.
* @returns The next opaque context and whether the same provider token is already active.
*/
export declare function enterSagaTopology(context: CqrsDispatchContext | undefined, descriptor: SagaDescriptor): SagaTopologyEntry;
//# sourceMappingURL=saga-topology.d.ts.map
{"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"}
import { createInternalCqrsDispatchContext, getInternalCqrsDispatchContextState } from '../dispatch-context.js';
import { SagaTopologyError } from '../errors.js';
const MAX_NESTED_SAGA_DEPTH = 32;
/** Result of entering one guarded saga route. */
/**
* Validates and enters one saga route using private immutable context state.
*
* @param context Opaque context passed through the active CQRS pipeline.
* @param descriptor Saga route selected for the current event.
* @returns The next opaque context and whether the same provider token is already active.
*/
export function enterSagaTopology(context, descriptor) {
const internalState = getInternalCqrsDispatchContextState(context);
const activeTopology = internalState?.sagaTopology;
const routeLabel = `${descriptor.targetType.name}(${descriptor.eventType.name})`;
const reenteredRoute = activeTopology?.activeRoutes.some(route => route.token === descriptor.token && route.eventType === descriptor.eventType);
if (reenteredRoute) {
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeTopology?.path ?? []), routeLabel].join(' -> ')}.`);
}
if ((activeTopology?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
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.');
}
return {
context: createInternalCqrsDispatchContext({
publishDrainToken: internalState?.publishDrainToken,
sagaTopology: {
activeRoutes: [...(activeTopology?.activeRoutes ?? []), {
eventType: descriptor.eventType,
token: descriptor.token
}],
depth: (activeTopology?.depth ?? 0) + 1,
path: [...(activeTopology?.path ?? []), routeLabel]
}
}),
reentrantToken: activeTopology?.activeRoutes.some(route => route.token === descriptor.token) ?? false
};
}
import type { Token } from '@fluojs/core';
import type { CqrsDispatchContext, CqrsEventType } from './types.js';
/** One active saga route retained in private CQRS dispatch state. */
export interface CqrsDispatchRoute {
readonly eventType: CqrsEventType;
readonly token: Token;
}
/** Private saga topology state associated with an opaque dispatch context. */
export interface CqrsSagaTopologyState {
readonly activeRoutes: readonly CqrsDispatchRoute[];
readonly depth: number;
readonly path: readonly string[];
}
/** Private state carried by an internally created dispatch context. */
export interface InternalCqrsDispatchContextState {
readonly publishDrainToken: symbol | undefined;
readonly sagaTopology: CqrsSagaTopologyState | undefined;
}
/**
* Creates an opaque immutable dispatch context and retains its state in a private weak map.
*
* @param state Internal publish-drain and saga-topology state.
* @returns A frozen fieldless context safe to pass through application handlers.
*/
export declare function createInternalCqrsDispatchContext(state: InternalCqrsDispatchContextState): CqrsDispatchContext;
/**
* Reads private state only for context values created by CQRS internals.
*
* @param context Optional public dispatch context received from a handler or saga.
* @returns The immutable internal state, or `undefined` for caller-created values.
*/
export declare function getInternalCqrsDispatchContextState(context: CqrsDispatchContext | undefined): InternalCqrsDispatchContextState | undefined;
//# sourceMappingURL=dispatch-context.d.ts.map
{"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"}
/** One active saga route retained in private CQRS dispatch state. */
/** Private saga topology state associated with an opaque dispatch context. */
/** Private state carried by an internally created dispatch context. */
const internalContextStates = new WeakMap();
function freezeSagaTopology(state) {
return Object.freeze({
activeRoutes: Object.freeze(state.activeRoutes.map(route => Object.freeze({
eventType: route.eventType,
token: route.token
}))),
depth: state.depth,
path: Object.freeze([...state.path])
});
}
/**
* Creates an opaque immutable dispatch context and retains its state in a private weak map.
*
* @param state Internal publish-drain and saga-topology state.
* @returns A frozen fieldless context safe to pass through application handlers.
*/
export function createInternalCqrsDispatchContext(state) {
const context = Object.freeze({});
internalContextStates.set(context, Object.freeze({
publishDrainToken: state.publishDrainToken,
sagaTopology: state.sagaTopology ? freezeSagaTopology(state.sagaTopology) : undefined
}));
return context;
}
/**
* Reads private state only for context values created by CQRS internals.
*
* @param context Optional public dispatch context received from a handler or saga.
* @returns The immutable internal state, or `undefined` for caller-created values.
*/
export function getInternalCqrsDispatchContextState(context) {
return context ? internalContextStates.get(context) : undefined;
}
export {};
//# sourceMappingURL=test-setup.d.ts.map
{"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":""}
import { afterEach, vi } from 'vitest';
afterEach(() => {
vi.useRealTimers();
});
+9
-2

@@ -1,2 +0,3 @@

import type { OnApplicationBootstrap } from '@fluojs/runtime';
import type { Container } from '@fluojs/di';
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
import { CqrsBusBase } from '../discovery.js';

@@ -10,7 +11,11 @@ import type { CommandBus, CqrsDispatchContext, ICommand } from '../types.js';

*/
export declare class CommandBusLifecycleService extends CqrsBusBase implements CommandBus, OnApplicationBootstrap {
export declare class CommandBusLifecycleService extends CqrsBusBase implements CommandBus, OnApplicationBootstrap, OnApplicationShutdown {
private descriptors;
private discoveryPromise;
private discovered;
private lifecycleState;
private unregisterShutdownStartCleanup;
constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, registerRuntimeCleanup?: RuntimeCleanupRegistration);
onApplicationBootstrap(): Promise<void>;
onApplicationShutdown(): Promise<void>;
/**

@@ -27,2 +32,4 @@ * Executes one command by dispatching it to the discovered handler for its constructor.

execute<TCommand extends ICommand, TResult = void>(command: TCommand, context?: CqrsDispatchContext): Promise<TResult>;
private assertAcceptingNewWork;
private markApplicationShutdownStarted;
private ensureDiscovered;

@@ -29,0 +36,0 @@ private discoverHandlers;

+1
-1

@@ -1,1 +0,1 @@

{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../../src/buses/command-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,EAAE,WAAW,EAAiC,MAAM,iBAAiB,CAAC;AAG7E,OAAO,KAAK,EACV,UAAU,EAGV,mBAAmB,EACnB,QAAQ,EAET,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,0BAA2B,SAAQ,WAAY,YAAW,UAAU,EAAE,sBAAsB;IACvG,OAAO,CAAC,WAAW,CAAoD;IACvE,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAErB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7C;;;;;;;;;OASG;IACG,OAAO,CAAC,QAAQ,SAAS,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;YAmB9G,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,0BAA0B;CAmDnC"}
{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../../src/buses/command-bus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEpJ,OAAO,EAAE,WAAW,EAA4D,MAAM,iBAAiB,CAAC;AAGxG,OAAO,KAAK,EACV,UAAU,EAGV,mBAAmB,EACnB,QAAQ,EAET,MAAM,aAAa,CAAC;AAUrB;;;;;GAKG;AACH,qBACa,0BAA2B,SAAQ,WAAY,YAAW,UAAU,EAAE,sBAAsB,EAAE,qBAAqB;IAC9H,OAAO,CAAC,WAAW,CAAoD;IACvE,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,QAAQ,SAAS,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB5H,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,8BAA8B;YAMxB,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,0BAA0B;CA2CnC"}

@@ -8,4 +8,4 @@ let _initClass;

import { Inject, InvariantError } from '@fluojs/core';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase, createDuplicateHandlerMessage } from '../discovery.js';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase, createDuplicateHandlerMessage, isSameHandlerRegistration } from '../discovery.js';
import { CommandHandlerNotFoundException, DuplicateCommandHandlerError } from '../errors.js';

@@ -29,3 +29,3 @@ import { getCommandHandlerMetadata } from '../metadata.js';

static {
[_CommandBusLifecycleS, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], [], 0, void 0, CqrsBusBase).c;
[_CommandBusLifecycleS, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
}

@@ -35,5 +35,30 @@ descriptors = new Map();

discovered = false;
lifecycleState = 'created';
unregisterShutdownStartCleanup;
constructor(runtimeContainer, compiledModules, logger, registerRuntimeCleanup = () => () => undefined) {
super(runtimeContainer, compiledModules, logger);
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
this.markApplicationShutdownStarted();
});
}
async onApplicationBootstrap() {
await this.ensureDiscovered();
this.lifecycleState = 'discovering';
try {
await this.ensureDiscovered();
this.lifecycleState = 'ready';
} catch (error) {
this.lifecycleState = 'failed';
throw error;
}
}
async onApplicationShutdown() {
this.markApplicationShutdownStarted();
this.unregisterShutdownStartCleanup?.();
this.unregisterShutdownStartCleanup = undefined;
this.descriptors.clear();
this.handlerInstances.clear();
this.discovered = false;
this.discoveryPromise = undefined;
this.lifecycleState = 'stopped';
}

@@ -51,2 +76,3 @@ /**

async execute(command, context) {
this.assertAcceptingNewWork('execute');
await this.ensureDiscovered();

@@ -64,2 +90,12 @@ const commandType = command.constructor;

}
assertAcceptingNewWork(operation) {
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
throw new InvariantError(`CQRS command bus cannot ${operation} after shutdown has started.`);
}
}
markApplicationShutdownStarted() {
if (this.lifecycleState !== 'stopped') {
this.lifecycleState = 'stopping';
}
}
async ensureDiscovered() {

@@ -90,3 +126,2 @@ if (this.discovered) {

const descriptors = new Map();
const seenByTarget = new WeakMap();
for (const candidate of this.discoveryCandidates()) {

@@ -101,23 +136,18 @@ const metadata = getCommandHandlerMetadata(candidate.targetType);

}
const seenCommandTypes = seenByTarget.get(candidate.targetType) ?? new Set();
if (seenCommandTypes.has(metadata.commandType)) {
continue;
}
seenCommandTypes.add(metadata.commandType);
seenByTarget.set(candidate.targetType, seenCommandTypes);
const existing = descriptors.get(metadata.commandType);
if (existing && existing.targetType !== candidate.targetType) {
throw new DuplicateCommandHandlerError(createDuplicateHandlerMessage('command', metadata.commandType, existing, {
moduleName: candidate.moduleName,
targetType: candidate.targetType
}));
const nextDescriptor = {
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
};
if (existing) {
if (isSameHandlerRegistration(existing, nextDescriptor)) {
continue;
}
throw new DuplicateCommandHandlerError(createDuplicateHandlerMessage('command', metadata.commandType, existing, nextDescriptor));
}
if (!existing) {
descriptors.set(metadata.commandType, {
commandType: metadata.commandType,
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
});
}
descriptors.set(metadata.commandType, {
commandType: metadata.commandType,
...nextDescriptor
});
}

@@ -124,0 +154,0 @@ return descriptors;

import { type EventBus } from '@fluojs/event-bus';
import type { OnApplicationBootstrap, OnApplicationShutdown } from '@fluojs/runtime';
import type { OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
import { CqrsBusBase } from '../discovery.js';

@@ -20,6 +20,6 @@ import type { CqrsModuleOptions } from '../module.js';

private discovered;
private readonly activePublishPipelines;
private shutdownDrainTimeouts;
private readonly publishDrainTracker;
private lifecycleState;
constructor(eventBus: EventBus, sagaService: CqrsSagaLifecycleService, runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions);
private unregisterShutdownStartCleanup;
constructor(eventBus: EventBus, sagaService: CqrsSagaLifecycleService, runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions, registerRuntimeCleanup?: RuntimeCleanupRegistration);
onApplicationBootstrap(): Promise<void>;

@@ -54,5 +54,4 @@ onApplicationShutdown(): Promise<void>;

private assertAcceptingNewWork;
private trackPublishPipeline;
private drainActivePublishPipelines;
private awaitShutdownDrain;
private markApplicationShutdownStarted;
private createPublishContext;
private resolveShutdownDrainTimeoutMs;

@@ -59,0 +58,0 @@ private matchEventDescriptors;

@@ -1,1 +0,1 @@

{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/buses/event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAyC,MAAM,EAAiB,MAAM,aAAa,CAAC;AACnI,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAYzD;;;;;GAKG;AACH,qBACa,mBAAoB,SAAQ,WAAY,YAAW,YAAY,EAAE,sBAAsB,EAAE,qBAAqB;IASvH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAI5B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAbhC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA4B;IACnE,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,cAAc,CAAsF;gBAGzF,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,wBAAwB,EACtD,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;IAU5C;;;;OAIG;IACH,4BAA4B;IAe5B;;;;;;;;OAQG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjG;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;YAKlG,kBAAkB;YAiBlB,qBAAqB;IAMnC,OAAO,CAAC,sBAAsB;YAMhB,oBAAoB;YAUpB,2BAA2B;YAc3B,kBAAkB;IAiBhC,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,qBAAqB;YAIf,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CA4CjC"}
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/buses/event-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGjH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAM9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAyC,MAAM,EAAiB,MAAM,aAAa,CAAC;AAGnI,OAAO,EAAiC,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAiBxF;;;;;GAKG;AACH,qBASa,mBAAoB,SAAQ,WAAY,YAAW,YAAY,EAAE,sBAAsB,EAAE,qBAAqB;IASvH,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAI5B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAbhC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA0B;IAC9D,OAAO,CAAC,cAAc,CAAsF;IAC5G,OAAO,CAAC,8BAA8B,CAA2B;gBAG9C,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,wBAAwB,EACtD,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;IAUtE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5C;;;;OAIG;IACH,4BAA4B;IAe5B;;;;;;;;OAQG;IACG,OAAO,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IASjG;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;YASlG,kBAAkB;YAiBlB,qBAAqB;IAMnC,OAAO,CAAC,sBAAsB;IAc9B,OAAO,CAAC,8BAA8B;IAMtC,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,qBAAqB;YAIf,gBAAgB;YAchB,gBAAgB;IAe9B,OAAO,CAAC,wBAAwB;CAGjC"}

@@ -9,9 +9,11 @@ let _initClass;

import { EVENT_BUS as FLUO_EVENT_BUS } from '@fluojs/event-bus';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase } from '../discovery.js';
import { createInternalCqrsDispatchContext, getInternalCqrsDispatchContextState } from '../dispatch-context.js';
import { createIsolatedEvent } from '../event-clone.js';
import { getEventHandlerMetadata } from '../metadata.js';
import { createCqrsPlatformStatusSnapshot } from '../status.js';
import { CQRS_MODULE_OPTIONS } from '../tokens.js';
import { CqrsSagaLifecycleService } from './saga-bus.js';
import { discoverEventHandlerDescriptors } from './event-handler-discovery.js';
import { CqrsPublishDrainTracker } from './publish-drain-tracker.js';
import { CQRS_SAGA_DRAIN_AUTHORIZATION, CqrsSagaLifecycleService } from './saga-bus.js';
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;

@@ -34,3 +36,3 @@ function isEventHandler(value) {

static {
[_CqrsEventBusService, _initClass] = _applyDecs(this, [Inject(FLUO_EVENT_BUS, CqrsSagaLifecycleService, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
[_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;
}

@@ -40,6 +42,6 @@ descriptors = [];

discovered = false;
activePublishPipelines = new Set();
shutdownDrainTimeouts = 0;
publishDrainTracker;
lifecycleState = 'created';
constructor(eventBus, sagaService, runtimeContainer, compiledModules, logger, moduleOptions = {}) {
unregisterShutdownStartCleanup;
constructor(eventBus, sagaService, runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
super(runtimeContainer, compiledModules, logger);

@@ -49,2 +51,6 @@ this.eventBus = eventBus;

this.moduleOptions = moduleOptions;
this.publishDrainTracker = new CqrsPublishDrainTracker(logger);
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
this.markApplicationShutdownStarted();
});
}

@@ -62,5 +68,7 @@ async onApplicationBootstrap() {

async onApplicationShutdown() {
this.lifecycleState = 'stopping';
if (this.activePublishPipelines.size > 0) {
await this.drainActivePublishPipelines();
this.markApplicationShutdownStarted();
this.unregisterShutdownStartCleanup?.();
this.unregisterShutdownStartCleanup = undefined;
if (this.publishDrainTracker.hasActivePipelines) {
await this.publishDrainTracker.drain(this.resolveShutdownDrainTimeoutMs());
}

@@ -85,3 +93,3 @@ this.lifecycleState = 'stopped';

shutdownDrainTimeoutMs: this.resolveShutdownDrainTimeoutMs(),
shutdownDrainTimeouts: this.shutdownDrainTimeouts
shutdownDrainTimeouts: this.publishDrainTracker.shutdownDrainTimeouts
});

@@ -100,4 +108,5 @@ }

async publish(event, context) {
this.assertAcceptingNewWork('publish');
await this.trackPublishPipeline(this.runPublishPipeline(event, context));
this.assertAcceptingNewWork('publish', context);
const publishContext = this.createPublishContext(context);
await this.publishDrainTracker.track(this.runPublishPipeline(event, publishContext.context), publishContext.drainToken);
}

@@ -113,4 +122,5 @@

async publishAll(events, context) {
this.assertAcceptingNewWork('publishAll');
await this.trackPublishPipeline(this.runPublishAllPipeline(events, context));
this.assertAcceptingNewWork('publishAll', context);
const publishContext = this.createPublishContext(context);
await this.publishDrainTracker.track(this.runPublishAllPipeline(events, publishContext.context), publishContext.drainToken);
}

@@ -126,5 +136,3 @@ async runPublishPipeline(event, context) {

}
await this.sagaService.dispatch(event, context, {
allowDuringShutdown: true
});
await this.sagaService.dispatch(event, context, CQRS_SAGA_DRAIN_AUTHORIZATION);
await this.eventBus.publish(event);

@@ -137,37 +145,34 @@ }

}
assertAcceptingNewWork(operation) {
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
assertAcceptingNewWork(operation, context) {
if (this.lifecycleState === 'stopped') {
throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
}
}
async trackPublishPipeline(pipeline) {
this.activePublishPipelines.add(pipeline);
try {
await pipeline;
} finally {
this.activePublishPipelines.delete(pipeline);
if (this.lifecycleState === 'stopping') {
const drainToken = getInternalCqrsDispatchContextState(context)?.publishDrainToken;
if (!drainToken || !this.publishDrainTracker.isActive(drainToken)) {
throw new InvariantError(`CQRS event bus cannot ${operation} after shutdown has started.`);
}
}
}
async drainActivePublishPipelines() {
const activePipelines = Array.from(this.activePublishPipelines);
const timeoutMs = this.resolveShutdownDrainTimeoutMs();
const drained = await this.awaitShutdownDrain(activePipelines, timeoutMs);
if (!drained) {
this.shutdownDrainTimeouts += 1;
this.logger.warn(`CQRS event shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activePipelines.length)} active publish pipeline(s); continuing shutdown.`, 'CqrsEventBusService');
markApplicationShutdownStarted() {
if (this.lifecycleState !== 'stopped') {
this.lifecycleState = 'stopping';
}
}
async awaitShutdownDrain(activePipelines, timeoutMs) {
let timeoutId;
const timeout = new Promise(resolve => {
timeoutId = setTimeout(() => resolve(false), timeoutMs);
});
const drain = Promise.allSettled(activePipelines).then(() => true);
try {
return await Promise.race([drain, timeout]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
createPublishContext(context) {
const internalState = getInternalCqrsDispatchContextState(context);
const drainToken = internalState?.publishDrainToken ?? Symbol('fluo.cqrs.activePublishDrain');
if (context && internalState?.publishDrainToken) {
return {
context,
drainToken
};
}
return {
context: createInternalCqrsDispatchContext({
publishDrainToken: drainToken,
sagaTopology: internalState?.sagaTopology
}),
drainToken
};
}

@@ -208,30 +213,3 @@ resolveShutdownDrainTimeoutMs() {

discoverEventDescriptors() {
const descriptors = [];
const seenByTarget = new WeakMap();
for (const candidate of this.discoveryCandidates()) {
const metadata = getEventHandlerMetadata(candidate.targetType);
if (!metadata) {
continue;
}
if (candidate.scope !== 'singleton') {
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');
continue;
}
const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
if (seenEventTypes.has(metadata.eventType)) {
continue;
}
seenEventTypes.add(metadata.eventType);
seenByTarget.set(candidate.targetType, seenEventTypes);
const alreadyRegistered = descriptors.some(descriptor => descriptor.eventType === metadata.eventType && descriptor.targetType === candidate.targetType);
if (!alreadyRegistered) {
descriptors.push({
eventType: metadata.eventType,
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
});
}
}
return descriptors;
return discoverEventHandlerDescriptors(this.discoveryCandidates(), this.logger);
}

@@ -238,0 +216,0 @@ static {

@@ -1,2 +0,3 @@

import type { OnApplicationBootstrap } from '@fluojs/runtime';
import type { Container } from '@fluojs/di';
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
import { CqrsBusBase } from '../discovery.js';

@@ -10,7 +11,11 @@ import type { CqrsDispatchContext, IQuery, QueryBus } from '../types.js';

*/
export declare class QueryBusLifecycleService extends CqrsBusBase implements QueryBus, OnApplicationBootstrap {
export declare class QueryBusLifecycleService extends CqrsBusBase implements QueryBus, OnApplicationBootstrap, OnApplicationShutdown {
private descriptors;
private discoveryPromise;
private discovered;
private lifecycleState;
private unregisterShutdownStartCleanup;
constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, registerRuntimeCleanup?: RuntimeCleanupRegistration);
onApplicationBootstrap(): Promise<void>;
onApplicationShutdown(): Promise<void>;
/**

@@ -27,2 +32,4 @@ * Executes one query by dispatching it to the discovered handler for its constructor.

execute<TQuery extends IQuery<TResult>, TResult = unknown>(query: TQuery, context?: CqrsDispatchContext): Promise<TResult>;
private assertAcceptingNewWork;
private markApplicationShutdownStarted;
private ensureDiscovered;

@@ -29,0 +36,0 @@ private discoverHandlers;

@@ -1,1 +0,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"}
{"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"}

@@ -8,4 +8,4 @@ let _initClass;

import { Inject, InvariantError } from '@fluojs/core';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase, createDuplicateHandlerMessage } from '../discovery.js';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase, createDuplicateHandlerMessage, isSameHandlerRegistration } from '../discovery.js';
import { DuplicateQueryHandlerError, QueryHandlerNotFoundException } from '../errors.js';

@@ -29,3 +29,3 @@ import { getQueryHandlerMetadata } from '../metadata.js';

static {
[_QueryBusLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], [], 0, void 0, CqrsBusBase).c;
[_QueryBusLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
}

@@ -35,5 +35,30 @@ descriptors = new Map();

discovered = false;
lifecycleState = 'created';
unregisterShutdownStartCleanup;
constructor(runtimeContainer, compiledModules, logger, registerRuntimeCleanup = () => () => undefined) {
super(runtimeContainer, compiledModules, logger);
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
this.markApplicationShutdownStarted();
});
}
async onApplicationBootstrap() {
await this.ensureDiscovered();
this.lifecycleState = 'discovering';
try {
await this.ensureDiscovered();
this.lifecycleState = 'ready';
} catch (error) {
this.lifecycleState = 'failed';
throw error;
}
}
async onApplicationShutdown() {
this.markApplicationShutdownStarted();
this.unregisterShutdownStartCleanup?.();
this.unregisterShutdownStartCleanup = undefined;
this.descriptors.clear();
this.handlerInstances.clear();
this.discovered = false;
this.discoveryPromise = undefined;
this.lifecycleState = 'stopped';
}

@@ -51,2 +76,3 @@ /**

async execute(query, context) {
this.assertAcceptingNewWork('execute');
await this.ensureDiscovered();

@@ -64,2 +90,12 @@ const queryType = query.constructor;

}
assertAcceptingNewWork(operation) {
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
throw new InvariantError(`CQRS query bus cannot ${operation} after shutdown has started.`);
}
}
markApplicationShutdownStarted() {
if (this.lifecycleState !== 'stopped') {
this.lifecycleState = 'stopping';
}
}
async ensureDiscovered() {

@@ -90,3 +126,2 @@ if (this.discovered) {

const descriptors = new Map();
const seenByTarget = new WeakMap();
for (const candidate of this.discoveryCandidates()) {

@@ -101,23 +136,18 @@ const metadata = getQueryHandlerMetadata(candidate.targetType);

}
const seenQueryTypes = seenByTarget.get(candidate.targetType) ?? new Set();
if (seenQueryTypes.has(metadata.queryType)) {
continue;
}
seenQueryTypes.add(metadata.queryType);
seenByTarget.set(candidate.targetType, seenQueryTypes);
const existing = descriptors.get(metadata.queryType);
if (existing && existing.targetType !== candidate.targetType) {
throw new DuplicateQueryHandlerError(createDuplicateHandlerMessage('query', metadata.queryType, existing, {
moduleName: candidate.moduleName,
targetType: candidate.targetType
}));
const nextDescriptor = {
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
};
if (existing) {
if (isSameHandlerRegistration(existing, nextDescriptor)) {
continue;
}
throw new DuplicateQueryHandlerError(createDuplicateHandlerMessage('query', metadata.queryType, existing, nextDescriptor));
}
if (!existing) {
descriptors.set(metadata.queryType, {
moduleName: candidate.moduleName,
queryType: metadata.queryType,
targetType: candidate.targetType,
token: candidate.token
});
}
descriptors.set(metadata.queryType, {
queryType: metadata.queryType,
...nextDescriptor
});
}

@@ -124,0 +154,0 @@ return descriptors;

@@ -1,8 +0,7 @@

import type { OnApplicationBootstrap, OnApplicationShutdown } from '@fluojs/runtime';
import type { OnApplicationBootstrap, OnApplicationShutdown, RuntimeCleanupRegistration } from '@fluojs/runtime';
import { CqrsBusBase } from '../discovery.js';
import type { CqrsModuleOptions } from '../module.js';
import type { CqrsDispatchContext, IEvent } from '../types.js';
interface SagaDispatchOptions {
readonly allowDuringShutdown?: boolean;
}
/** Private capability authorizing saga dispatch from an already active publish drain. */
export declare const CQRS_SAGA_DRAIN_AUTHORIZATION: unique symbol;
/**

@@ -23,3 +22,4 @@ * Runtime saga coordinator that discovers `@Saga()` providers and serializes execution per saga token.

private shutdownDrainTimeouts;
constructor(runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions);
private unregisterShutdownStartCleanup;
constructor(runtimeContainer: ConstructorParameters<typeof CqrsBusBase>[0], compiledModules: ConstructorParameters<typeof CqrsBusBase>[1], logger: ConstructorParameters<typeof CqrsBusBase>[2], moduleOptions?: CqrsModuleOptions, registerRuntimeCleanup?: RuntimeCleanupRegistration);
onApplicationBootstrap(): Promise<void>;

@@ -45,10 +45,10 @@ onApplicationShutdown(): Promise<void>;

*/
dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext, options?: SagaDispatchOptions): Promise<void>;
dispatch<TEvent extends IEvent>(event: TEvent, context?: CqrsDispatchContext, drainAuthorization?: typeof CQRS_SAGA_DRAIN_AUTHORIZATION): Promise<void>;
private assertAcceptingNewWork;
private markApplicationShutdownStarted;
private matchSagaDescriptors;
private dispatchWithOrdering;
private drainActiveSagaWork;
private awaitShutdownDrain;
private reportShutdownDrainTimeout;
private resolveShutdownDrainTimeoutMs;
private createDispatchContext;
private invokeSaga;

@@ -59,3 +59,2 @@ private ensureDiscovered;

}
export {};
//# sourceMappingURL=saga-bus.d.ts.map

@@ -1,1 +0,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;AAMrG,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACxC;AAsCD;;;;;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,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAarI,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,oBAAoB;YAYd,oBAAoB;YA0CpB,mBAAmB;YAmBnB,kBAAkB;IAiBhC,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,qBAAqB;YAaf,UAAU;YAoBV,gBAAgB;YAchB,gBAAgB;IAiB9B,OAAO,CAAC,uBAAuB;CA+ChC"}
{"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"}

@@ -8,11 +8,14 @@ let _initClass;

import { FluoError, Inject, InvariantError } from '@fluojs/core';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CLEANUP_REGISTRATION, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
import { CqrsBusBase } from '../discovery.js';
import { SagaExecutionError, SagaTopologyError } from '../errors.js';
import { SagaExecutionError } from '../errors.js';
import { createIsolatedEvent } from '../event-clone.js';
import { getSagaMetadata } from '../metadata.js';
import { CQRS_MODULE_OPTIONS } from '../tokens.js';
const MAX_NESTED_SAGA_DEPTH = 32;
import { discoverSagaDescriptors } from './saga-discovery.js';
import { drainPendingSagaDispatches } from './saga-drain.js';
import { enterSagaTopology } from './saga-topology.js';
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
const cqrsDispatchContextStateBrand = Symbol('fluo.cqrs.dispatchContextState');
/** Private capability authorizing saga dispatch from an already active publish drain. */
export const CQRS_SAGA_DRAIN_AUTHORIZATION = Symbol('fluo.cqrs.sagaDrainAuthorization');
function isSaga(value) {

@@ -30,8 +33,2 @@ if (typeof value !== 'object' || value === null) {

}
function isCqrsDispatchContextState(context) {
if (typeof context !== 'object' || context === null) {
return false;
}
return context[cqrsDispatchContextStateBrand] === true;
}

@@ -47,3 +44,3 @@ /**

static {
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS)], [], 0, void 0, CqrsBusBase).c;
[_CqrsSagaLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, CQRS_MODULE_OPTIONS, RUNTIME_CLEANUP_REGISTRATION)], [], 0, void 0, CqrsBusBase).c;
}

@@ -57,5 +54,9 @@ descriptorsByEvent = new Map();

shutdownDrainTimeouts = 0;
constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}) {
unregisterShutdownStartCleanup;
constructor(runtimeContainer, compiledModules, logger, moduleOptions = {}, registerRuntimeCleanup = () => () => undefined) {
super(runtimeContainer, compiledModules, logger);
this.moduleOptions = moduleOptions;
this.unregisterShutdownStartCleanup = registerRuntimeCleanup(() => {
this.markApplicationShutdownStarted();
});
}

@@ -73,3 +74,5 @@ async onApplicationBootstrap() {

async onApplicationShutdown() {
this.lifecycleState = 'stopping';
this.markApplicationShutdownStarted();
this.unregisterShutdownStartCleanup?.();
this.unregisterShutdownStartCleanup = undefined;
await this.drainActiveSagaWork();

@@ -105,4 +108,4 @@ this.executionChains.clear();

*/
async dispatch(event, context, options = {}) {
this.assertAcceptingNewWork(options);
async dispatch(event, context, drainAuthorization) {
this.assertAcceptingNewWork(drainAuthorization);
await this.ensureDiscovered();

@@ -115,10 +118,12 @@ const descriptors = this.matchSagaDescriptors(event);

}
assertAcceptingNewWork(options) {
if (options.allowDuringShutdown) {
return;
}
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
assertAcceptingNewWork(drainAuthorization) {
if ((this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') && drainAuthorization !== CQRS_SAGA_DRAIN_AUTHORIZATION) {
throw new InvariantError('CQRS saga bus cannot dispatch after shutdown has started.');
}
}
markApplicationShutdownStarted() {
if (this.lifecycleState !== 'stopped') {
this.lifecycleState = 'stopping';
}
}
matchSagaDescriptors(event) {

@@ -134,14 +139,5 @@ const descriptors = [];

async dispatchWithOrdering(descriptor, event, activeContext) {
const activeState = isCqrsDispatchContextState(activeContext) ? activeContext : undefined;
const routeLabel = `${descriptor.targetType.name}(${descriptor.eventType.name})`;
const isActiveRoute = activeState?.activeRoutes.some(route => route.token === descriptor.token && route.eventType === descriptor.eventType);
const isActiveToken = activeState?.activeRoutes.some(route => route.token === descriptor.token) ?? false;
if (isActiveRoute) {
throw new SagaTopologyError(`Saga ${descriptor.targetType.name} re-entered an unsafe cycle while handling ${descriptor.eventType.name}. ` + `Active saga path: ${[...(activeState?.path ?? []), routeLabel].join(' -> ')}.`);
}
if ((activeState?.depth ?? 0) >= MAX_NESTED_SAGA_DEPTH) {
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.');
}
if (isActiveToken) {
await this.invokeSaga(descriptor, event, this.createDispatchContext(activeState, descriptor, routeLabel));
const topology = enterSagaTopology(activeContext, descriptor);
if (topology.reentrantToken) {
await this.invokeSaga(descriptor, event, topology.context);
return;

@@ -151,3 +147,3 @@ }

const current = previous.then(async () => {
await this.invokeSaga(descriptor, event, this.createDispatchContext(activeState, descriptor, routeLabel));
await this.invokeSaga(descriptor, event, topology.context);
});

@@ -163,26 +159,12 @@ this.executionChains.set(descriptor.token, current.catch(() => undefined));

async drainActiveSagaWork() {
const activeWork = [...this.pendingDispatches, ...this.executionChains.values()];
if (activeWork.length === 0) {
return;
}
const timeoutMs = this.resolveShutdownDrainTimeoutMs();
const drained = await this.awaitShutdownDrain(activeWork, timeoutMs);
const activeWorkCount = this.pendingDispatches.size;
const drained = await drainPendingSagaDispatches(this.pendingDispatches, timeoutMs);
if (!drained) {
this.shutdownDrainTimeouts += 1;
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWork.length)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
this.reportShutdownDrainTimeout(timeoutMs, activeWorkCount);
}
}
async awaitShutdownDrain(activeWork, timeoutMs) {
let timeoutId;
const timeout = new Promise(resolve => {
timeoutId = setTimeout(() => resolve(false), timeoutMs);
});
const drain = Promise.allSettled(activeWork).then(() => true);
try {
return await Promise.race([drain, timeout]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
reportShutdownDrainTimeout(timeoutMs, activeWorkCount) {
this.shutdownDrainTimeouts += 1;
this.logger.warn(`CQRS saga shutdown drain exceeded ${String(timeoutMs)}ms with ${String(activeWorkCount)} active saga task(s); continuing shutdown.`, 'CqrsSagaLifecycleService');
}

@@ -196,13 +178,2 @@ resolveShutdownDrainTimeoutMs() {

}
createDispatchContext(activeContext, descriptor, routeLabel) {
return {
[cqrsDispatchContextStateBrand]: true,
activeRoutes: [...(activeContext?.activeRoutes ?? []), {
eventType: descriptor.eventType,
token: descriptor.token
}],
depth: (activeContext?.depth ?? 0) + 1,
path: [...(activeContext?.path ?? []), routeLabel]
};
}
async invokeSaga(descriptor, event, context) {

@@ -248,33 +219,3 @@ const instance = await this.resolveHandlerInstance(descriptor.token);

discoverSagaDescriptors() {
const descriptorsByEvent = new Map();
const seenByTarget = new WeakMap();
for (const candidate of this.discoveryCandidates()) {
const metadata = getSagaMetadata(candidate.targetType);
if (!metadata) {
continue;
}
if (candidate.scope !== 'singleton') {
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');
continue;
}
const seenEventTypes = seenByTarget.get(candidate.targetType) ?? new Set();
for (const eventType of metadata.eventTypes) {
if (seenEventTypes.has(eventType)) {
continue;
}
seenEventTypes.add(eventType);
const descriptors = descriptorsByEvent.get(eventType) ?? [];
if (!descriptors.some(descriptor => descriptor.targetType === candidate.targetType)) {
descriptors.push({
eventType,
moduleName: candidate.moduleName,
targetType: candidate.targetType,
token: candidate.token
});
descriptorsByEvent.set(eventType, descriptors);
}
}
seenByTarget.set(candidate.targetType, seenEventTypes);
}
return descriptorsByEvent;
return discoverSagaDescriptors(this.discoveryCandidates(), this.logger);
}

@@ -281,0 +222,0 @@ static {

@@ -25,7 +25,23 @@ import { type Token } from '@fluojs/core';

targetType: Function;
token: Token;
}, second: {
moduleName: string;
targetType: Function;
token: Token;
}): string;
/**
* Checks whether two discovered handler candidates refer to the same provider registration.
*
* @param first The first handler registration.
* @param second The second handler registration.
* @returns Whether both target type and provider token match.
*/
export declare function isSameHandlerRegistration(first: {
targetType: Function;
token: Token;
}, second: {
targetType: Function;
token: Token;
}): boolean;
/**
* Represents the cqrs bus base.

@@ -32,0 +48,0 @@ */

@@ -1,1 +0,1 @@

{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,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,CAAA;CAAE,EACnD,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,QAAQ,CAAA;CAAE,GACnD,MAAM,CAER;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;cAsCrC,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;cAgBnD,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;CAiBvE"}
{"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"}

@@ -0,1 +1,2 @@

import { formatTokenName } from '@fluojs/core';
import { getClassDiMetadata } from '@fluojs/core/internal';

@@ -30,6 +31,20 @@

export function createDuplicateHandlerMessage(kind, messageType, first, second) {
return `Duplicate ${kind} handler for ${messageType.name} was discovered in ${first.moduleName}.${first.targetType.name} and ${second.moduleName}.${second.targetType.name}.`;
return `Duplicate ${kind} handler for ${messageType.name} was discovered in ${describeHandlerRegistration(first)} and ${describeHandlerRegistration(second)}.`;
}
function describeHandlerRegistration(registration) {
return `${registration.moduleName}.${registration.targetType.name} [token: ${formatTokenName(registration.token)}]`;
}
/**
* Checks whether two discovered handler candidates refer to the same provider registration.
*
* @param first The first handler registration.
* @param second The second handler registration.
* @returns Whether both target type and provider token match.
*/
export function isSameHandlerRegistration(first, second) {
return first.targetType === second.targetType && first.token === second.token;
}
/**
* Represents the cqrs bus base.

@@ -66,10 +81,2 @@ */

}
for (const controller of compiledModule.definition.controllers ?? []) {
candidates.push({
moduleName: compiledModule.type.name,
scope: scopeFromProvider(controller),
targetType: controller,
token: controller
});
}
}

@@ -76,0 +83,0 @@ return candidates;

@@ -1,1 +0,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;AAgGD,iFAAiF;AACjF,qBAAa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAiB5D"}
{"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"}

@@ -27,2 +27,9 @@ import { EventBusModule } from '@fluojs/event-bus';

}
function resolveDelegatedEventBusOptions(options) {
const eventBusOptions = options.eventBus ?? {};
return {
...eventBusOptions,
global: eventBusOptions.global ?? options.global ?? true
};
}
function assertCommandBusService(service) {

@@ -98,3 +105,3 @@ if (!(service instanceof CommandBusLifecycleService)) {

global: options.global ?? true,
imports: [EventBusModule.forRoot(options.eventBus)],
imports: [EventBusModule.forRoot(resolveDelegatedEventBusOptions(options))],
providers: createCqrsProviders(options)

@@ -101,0 +108,0 @@ });

@@ -62,3 +62,3 @@ import type { Token } from '@fluojs/core';

* nested `execute(...)`, `publish(...)`, or `publishAll(...)` calls. The context intentionally
* exposes no public topology fields and should not be inspected or constructed by callers.
* exposes no public topology fields, and caller-shaped objects cannot supply trusted runtime state.
*/

@@ -65,0 +65,0 @@ export interface CqrsDispatchContext {

@@ -12,3 +12,3 @@ {

],
"version": "1.1.2",
"version": "2.0.0",
"private": false,

@@ -40,6 +40,6 @@ "license": "MIT",

"dependencies": {
"@fluojs/core": "^1.0.3",
"@fluojs/di": "^1.1.0",
"@fluojs/event-bus": "^1.0.1",
"@fluojs/runtime": "^1.1.8"
"@fluojs/core": "^1.1.0",
"@fluojs/di": "^2.0.0",
"@fluojs/event-bus": "^2.0.0",
"@fluojs/runtime": "^2.0.1"
},

@@ -46,0 +46,0 @@ "devDependencies": {

@@ -142,3 +142,3 @@ # @fluojs/cqrs

`CqrsModule.forRoot(...)`를 import하는 애플리케이션 모듈에 projection handler, query handler, projection store를 singleton provider로 등록하세요. `CqrsEventBusService.publish(new OrderPlacedEvent(...))`는 일치하는 `@EventHandler(...)` provider를 saga와 위임 `@fluojs/event-bus` 발행보다 먼저 실행하므로, read model은 문서화된 CQRS event pipeline을 통해 write-side fact를 관찰합니다. Event replay, retry, 외부 transport가 같은 business fact를 두 번 이상 전달할 수 있으므로 projection handler는 idempotent하게 유지하세요.
`CqrsModule.forRoot(...)`를 import하는 애플리케이션 모듈에 projection handler, query handler, projection store를 singleton provider로 등록하세요. CQRS handler discovery는 provider registration만 검사합니다. HTTP controller는 request boundary에 남으며 controller class가 실수로 CQRS handler decorator를 가지고 있어도 무시됩니다. `CqrsModule.forRoot(...)`는 기본적으로 bus를 global로 export하며, `CqrsModule.forRoot({ global: false })`는 `eventBus.global`을 명시적으로 override하지 않는 한 해당 CQRS module을 import한 module을 통해서만 bus provider와 위임 `@fluojs/event-bus` provider가 보이도록 유지합니다. `CqrsEventBusService.publish(new OrderPlacedEvent(...))`는 일치하는 `@EventHandler(...)` provider를 saga와 위임 `@fluojs/event-bus` 발행보다 먼저 실행하므로, read model은 문서화된 CQRS event pipeline을 통해 write-side fact를 관찰합니다. Fan-out identity는 singleton provider token을 따릅니다. Decorated handler class 하나를 서로 다른 두 token으로 등록하면 두 registration이 모두 의도적으로 호출되고, 같은 token과 event route가 반복 discovery될 때만 deduplicate됩니다. Event replay, retry, 외부 transport가 같은 business fact를 두 번 이상 전달할 수 있으므로 projection handler는 idempotent하게 유지하세요.

@@ -172,9 +172,9 @@ ### Saga 프로세스 매니저

Saga 실행은 같은 프로세스 안에서 동일 saga route로 순환 재진입하거나 중첩 hop 수가 32를 넘으면 `SagaTopologyError`로 즉시 실패합니다. 서로 다른 이벤트 단계를 순차 처리하는 multi-stage saga는 계속 허용되지만, in-process saga graph 전체는 비순환(acyclic) 구조를 유지해야 하며, 의도적인 순환/피드백 루프나 더 긴 체인은 외부 transport, scheduler, 또는 다른 bounded boundary 뒤로 이동해야 합니다.
Saga 실행은 같은 프로세스 안에서 동일 provider-token/event route로 순환 재진입하거나 중첩 hop 수가 32를 넘으면 `SagaTopologyError`로 즉시 실패합니다. 같은 decorated saga class를 사용하는 서로 다른 singleton token은 별도의 fan-out route로 유지됩니다. 서로 다른 이벤트 단계를 순차 처리하는 multi-stage saga는 계속 허용되지만, in-process saga graph 전체는 비순환(acyclic) 구조를 유지해야 하며, 의도적인 순환/피드백 루프나 더 긴 체인은 외부 transport, scheduler, 또는 다른 bounded boundary 뒤로 이동해야 합니다.
Saga, command handler, query handler, event handler 안에서 다시 CQRS `execute(...)`, `publish(...)`, `publishAll(...)`를 호출할 때는 optional `CqrsDispatchContext` 인자를 그대로 전달하세요. CQRS는 이 명시적인 runtime-agnostic context로 Node.js async-local API에 의존하지 않고 nested dispatch 전반의 saga topology check를 유지합니다. 이 context는 opaque입니다. 직접 생성하거나 검사하거나 topology field에 의존하지 마세요. 해당 field는 내부 runtime state입니다.
Saga, command handler, query handler, event handler 안에서 다시 CQRS `execute(...)`, `publish(...)`, `publishAll(...)`를 호출할 때는 optional `CqrsDispatchContext` 인자를 그대로 전달하세요. CQRS는 이 명시적인 runtime-agnostic context로 Node.js async-local API에 의존하지 않고 nested dispatch 전반의 saga topology check를 유지합니다. 이 context는 opaque, frozen fieldless pass-through value이며, 신뢰하는 topology와 shutdown-drain state는 CQRS 내부에 비공개로 유지됩니다. Caller-shaped object와 복사된 값은 신뢰된 runtime state를 운반하지 않으므로 context를 직접 생성, 복제, 검사, mutate하지 마세요.
### Event 발행 계약
`CqrsEventBusService.publish(event)`는 CQRS event pipeline을 고정된 순서로 실행합니다. 먼저 일치하는 `@EventHandler(...)` provider를 실행하고, 그다음 일치하는 `@Saga(...)` provider를 실행한 뒤, 마지막으로 `@fluojs/event-bus`로 위임 발행합니다. `publishAll(events)`는 각 event의 CQRS handler, saga, 위임 발행 호출을 기다린 뒤 다음 event를 발행하므로 입력 순서를 보존합니다. 애플리케이션 shutdown 중에는 CQRS event bus가 진행 중인 `publish(...)` pipeline, `publishAll(...)` sequence, saga execution chain이 settle될 때까지 기다린 뒤 stopped 상태로 전환합니다. Shutdown이 시작되면 새로운 `publish(...)`, `publishAll(...)`, direct saga dispatch 호출은 거부됩니다. 이미 진행 중인 publish와 saga 작업은 bounded shutdown window 안에서 계속 drain됩니다. Shutdown drain은 기본값이 5000ms인 `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`로 제한됩니다. CQRS handler, saga 또는 위임 publish chain이 이 bound 이후에도 멈춰 있으면 CQRS는 degraded status diagnostic을 기록하고 경고를 남긴 뒤 애플리케이션 close를 무기한 hang시키지 않고 계속 진행합니다. `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })`로 설정한 경우 위임 발행 호출은 일치하는 `@OnEvent(...)` subscriber가 완료되기 전에 resolve될 수 있으므로, 이 모드에서 `publish(...)`, `publishAll(...)`, shutdown drain 완료는 subscriber 완료를 의미하지 않습니다.
`CqrsEventBusService.publish(event)`는 CQRS event pipeline을 고정된 순서로 실행합니다. 먼저 일치하는 `@EventHandler(...)` provider를 실행하고, 그다음 일치하는 `@Saga(...)` provider를 실행한 뒤, 마지막으로 `@fluojs/event-bus`로 위임 발행합니다. `publishAll(events)`는 각 event의 CQRS handler, saga, 위임 발행 호출을 기다린 뒤 다음 event를 발행하므로 입력 순서를 보존합니다. 애플리케이션 shutdown 중에는 CQRS event bus가 진행 중인 `publish(...)` pipeline, `publishAll(...)` sequence, saga execution chain이 settle될 때까지 기다린 뒤 stopped 상태로 전환합니다. Command bus와 query bus는 shutdown이 시작되면 새로운 `execute(...)` 호출을 거부하고 shutdown 중 preload된 handler cache를 정리하므로, close 이후 dispatch가 오래된 provider instance를 재사용할 수 없습니다. Shutdown이 시작되면 brand-new external `publish(...)`, `publishAll(...)`, direct saga dispatch 호출은 거부됩니다. 이미 활성화된 handler나 saga에서 호출되는 nested `publish(...)` 또는 `publishAll(...)`은 CQRS가 제공한 `CqrsDispatchContext`를 그대로 전달할 때만 계속 진행할 수 있습니다. 이렇게 하면 drain 작업은 활성 pipeline 안에 머무르고 관련 없는 caller는 계속 거부됩니다. 이미 진행 중인 publish와 saga 작업은 bounded shutdown window 안에서 계속 drain됩니다. Shutdown drain은 기본값이 5000ms인 `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`로 제한됩니다. CQRS handler, saga 또는 위임 publish chain이 이 bound 이후에도 멈춰 있으면 CQRS는 degraded status diagnostic을 기록하고 경고를 남긴 뒤 애플리케이션 close를 무기한 hang시키지 않고 계속 진행합니다. `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })`로 설정한 경우 위임 발행 호출은 일치하는 `@OnEvent(...)` subscriber가 완료되기 전에 resolve될 수 있으므로, 이 모드에서 `publish(...)`, `publishAll(...)`, shutdown drain 완료는 subscriber 완료를 의미하지 않습니다.

@@ -185,3 +185,3 @@ 각 CQRS event handler와 saga는 매칭된 event prototype이 복원된 격리 event 복사본을 받습니다. 이 복사본을 mutate해도 변경은 현재 handler 또는 saga route 안에만 머물며, 다른 CQRS handler, saga, 원본 event 객체, 또는 위임된 `@fluojs/event-bus` subscriber에는 보이지 않습니다. 위임된 event-bus 발행은 CQRS side effect가 끝난 뒤 원본 event를 받으므로, `@OnEvent(...)` projection과 transport는 CQRS handler가 mutate한 복사본이 아니라 호출자가 소유한 payload를 관찰합니다.

CQRS handler, event handler, saga는 singleton provider에서만 discovery됩니다. Non-singleton registration은 경고와 함께 건너뜁니다.
CQRS handler, event handler, saga는 singleton provider에서만 discovery됩니다. Non-singleton registration은 경고와 함께 건너뜁니다. Event handler와 saga fan-out은 singleton provider token으로 구분되므로 같은 decorated class를 사용해도 서로 다른 token은 별도 route로 유지됩니다.

@@ -205,3 +205,3 @@ ### 심볼 토큰

### 모듈 및 프로바이더
- `CqrsModule.forRoot(options)`: 메인 진입점입니다. 버스를 등록하고 탐색을 시작합니다.
- `CqrsModule.forRoot(options)`: 메인 진입점입니다. 버스를 등록하고 provider-only discovery를 시작합니다. Bus provider는 기본적으로 global이며 module-local visibility가 필요하면 `global: false`를 전달합니다.
- Module option은 명시적인 `commandHandlers`, `queryHandlers`, `eventHandlers`, `sagas`, 위임 `eventBus` option을 받을 수 있습니다.

@@ -226,3 +226,3 @@ - `CommandBusLifecycleService`: Command 실행을 위한 기본 서비스입니다.

- `DuplicateCommandHandlerError`, `DuplicateQueryHandlerError`: 서로 다른 singleton provider가 같은 command 또는 query type을 claim할 때 발생합니다.
- `DuplicateEventHandlerError`: 충돌하는 event-handler discovery 실패를 위해 export됩니다. 일반적으로 같은 event에 여러 `@EventHandler(...)` provider를 등록하는 것은 유효하며 discovery 순서대로 fan-out됩니다.
- `DuplicateEventHandlerError`: 호환성을 위해서만 export가 유지되며, event-handler discovery는 이 오류를 throw하거나 중복 registration을 failure로 취급하지 않습니다. 같은 provider token과 event route가 반복 discovery되면 조용히 deduplicate하고, 서로 다른 singleton provider token은 discovery 순서대로 fan-out되는 유효한 route로 유지합니다.
- `SagaExecutionError`: 예상하지 못한 non-Fluo saga 실패를 감쌉니다.

@@ -229,0 +229,0 @@ - `SagaTopologyError`: 자기 트리거, 순환, 또는 과도하게 깊은 in-process saga graph를 감지했을 때 발생합니다.

@@ -142,3 +142,3 @@ # @fluojs/cqrs

Register the projection handler, query handler, and projection store as singleton providers in the same application module that imports `CqrsModule.forRoot(...)`. `CqrsEventBusService.publish(new OrderPlacedEvent(...))` runs matching `@EventHandler(...)` providers before sagas and delegated `@fluojs/event-bus` publication, so the read model observes the write-side fact through the documented CQRS event pipeline. Keep projection handlers idempotent because event replay, retries, or external transports can deliver the same business fact more than once.
Register the projection handler, query handler, and projection store as singleton providers in the same application module that imports `CqrsModule.forRoot(...)`. CQRS handler discovery inspects provider registrations only; HTTP controllers stay on the request boundary and are ignored even when a controller class accidentally carries a CQRS handler decorator. `CqrsModule.forRoot(...)` exports the buses globally by default, and `CqrsModule.forRoot({ global: false })` keeps those bus providers and the delegated `@fluojs/event-bus` providers visible only through modules that import the CQRS module unless `eventBus.global` is explicitly overridden. `CqrsEventBusService.publish(new OrderPlacedEvent(...))` runs matching `@EventHandler(...)` providers before sagas and delegated `@fluojs/event-bus` publication, so the read model observes the write-side fact through the documented CQRS event pipeline. Fan-out identity follows the singleton provider token: registering one decorated handler class under two distinct tokens intentionally invokes both registrations, while repeated discovery of the same token and event route is deduplicated. Keep projection handlers idempotent because event replay, retries, or external transports can deliver the same business fact more than once.

@@ -172,9 +172,9 @@ ### Saga Process Managers

Saga execution fails fast with `SagaTopologyError` when an in-process publish chain re-enters the same saga route cyclically or exceeds 32 nested saga hops. Multi-stage sagas may still react to different event types in sequence, but in-process saga graphs must stay acyclic overall; move intentionally cyclic or long-running feedback loops behind an external transport, scheduler, or other bounded boundary.
Saga execution fails fast with `SagaTopologyError` when an in-process publish chain re-enters the same provider-token/event route cyclically or exceeds 32 nested saga hops. Distinct singleton tokens that use the same decorated saga class remain distinct fan-out routes. Multi-stage sagas may still react to different event types in sequence, but in-process saga graphs must stay acyclic overall; move intentionally cyclic or long-running feedback loops behind an external transport, scheduler, or other bounded boundary.
When a saga, command handler, query handler, or event handler performs another CQRS `execute(...)`, `publish(...)`, or `publishAll(...)` call, pass the optional `CqrsDispatchContext` argument through unchanged. CQRS uses this explicit runtime-agnostic context to keep saga topology checks intact across nested dispatch without relying on Node.js async-local APIs. The context is opaque: do not construct it, inspect it, or depend on topology fields because those fields are internal runtime state.
When a saga, command handler, query handler, or event handler performs another CQRS `execute(...)`, `publish(...)`, or `publishAll(...)` call, pass the optional `CqrsDispatchContext` argument through unchanged. CQRS uses this explicit runtime-agnostic context to keep saga topology checks intact across nested dispatch without relying on Node.js async-local APIs. The context is an opaque, frozen fieldless pass-through value; trusted topology and shutdown-drain state remains private to CQRS. Do not construct, clone, inspect, or mutate it because caller-shaped objects and copied values do not carry trusted runtime state.
### Event Publishing Contracts
`CqrsEventBusService.publish(event)` runs the CQRS event pipeline in a fixed order: matching `@EventHandler(...)` providers first, matching `@Saga(...)` providers second, and delegated `@fluojs/event-bus` publication last. `publishAll(events)` preserves the input order by awaiting each event's CQRS handlers, sagas, and delegated publication call before publishing the next event. During application shutdown, the CQRS event bus waits for active `publish(...)` pipelines, `publishAll(...)` sequences, and saga execution chains to settle before marking itself stopped. Once shutdown starts, new `publish(...)`, `publishAll(...)`, and direct saga dispatch calls are rejected; already active publish and saga work continues draining inside the bounded shutdown window. Shutdown drain is bounded by `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`, which defaults to 5000ms; if a CQRS handler, saga, or delegated publish chain is still stuck after the bound, CQRS records degraded status diagnostics, logs a warning, and lets application close continue instead of hanging indefinitely. When `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })` is configured, the delegated publication call can resolve before matching `@OnEvent(...)` subscribers finish, so `publish(...)`, `publishAll(...)`, and shutdown drain completion do not imply subscriber completion in that mode.
`CqrsEventBusService.publish(event)` runs the CQRS event pipeline in a fixed order: matching `@EventHandler(...)` providers first, matching `@Saga(...)` providers second, and delegated `@fluojs/event-bus` publication last. `publishAll(events)` preserves the input order by awaiting each event's CQRS handlers, sagas, and delegated publication call before publishing the next event. During application shutdown, the CQRS event bus waits for active `publish(...)` pipelines, `publishAll(...)` sequences, and saga execution chains to settle before marking itself stopped. Command and query buses reject new `execute(...)` calls once shutdown starts and clear their preloaded handler caches during shutdown, so post-close dispatch cannot reuse stale provider instances. Once shutdown starts, brand-new external `publish(...)`, `publishAll(...)`, and direct saga dispatch calls are rejected. A nested `publish(...)` or `publishAll(...)` invoked from an already active handler or saga may continue only when it passes through the CQRS-provided `CqrsDispatchContext`; this keeps drain work inside the active pipeline while still rejecting unrelated callers. Already active publish and saga work continues draining inside the bounded shutdown window. Shutdown drain is bounded by `CqrsModule.forRoot({ shutdown: { drainTimeoutMs } })`, which defaults to 5000ms; if a CQRS handler, saga, or delegated publish chain is still stuck after the bound, CQRS records degraded status diagnostics, logs a warning, and lets application close continue instead of hanging indefinitely. When `CqrsModule.forRoot({ eventBus: { publish: { waitForHandlers: false } } })` is configured, the delegated publication call can resolve before matching `@OnEvent(...)` subscribers finish, so `publish(...)`, `publishAll(...)`, and shutdown drain completion do not imply subscriber completion in that mode.

@@ -185,3 +185,3 @@ Each CQRS event handler and saga receives an isolated event copy with the matched event prototype restored. Mutating that copy is local to the current handler or saga route; those mutations are not visible to other CQRS handlers, sagas, the original event object, or delegated `@fluojs/event-bus` subscribers. The delegated event-bus publication receives the original event after CQRS side effects complete, so `@OnEvent(...)` projections and transports observe the caller-owned payload rather than a CQRS handler's mutated copy.

CQRS handlers, event handlers, and sagas are discovered only on singleton providers. Non-singleton registrations are skipped with warnings.
CQRS handlers, event handlers, and sagas are discovered only on singleton providers. Non-singleton registrations are skipped with warnings. Event-handler and saga fan-out is keyed by singleton provider token, so distinct tokens remain distinct routes even when they use the same decorated class.

@@ -205,3 +205,3 @@ ### Symbol Tokens

### Modules & Providers
- `CqrsModule.forRoot(options)`: Main entry point. Registers buses and starts discovery.
- `CqrsModule.forRoot(options)`: Main entry point. Registers buses and starts provider-only discovery. Bus providers are global by default; pass `global: false` for module-local visibility.
- Module options can provide explicit `commandHandlers`, `queryHandlers`, `eventHandlers`, `sagas`, and delegated `eventBus` options.

@@ -226,3 +226,3 @@ - `CommandBusLifecycleService`: Primary service for executing commands.

- `DuplicateCommandHandlerError`, `DuplicateQueryHandlerError`: Raised when different singleton providers claim the same command or query type.
- `DuplicateEventHandlerError`: Exported for conflicting event-handler discovery failures; ordinary multiple `@EventHandler(...)` providers for the same event are valid and fan out in discovery order.
- `DuplicateEventHandlerError`: Retained only as a compatibility export; event-handler discovery does not throw it or treat duplicate registrations as failures. Repeated discovery of the same provider token and event route is silently deduplicated, while distinct singleton provider tokens remain valid fan-out routes in discovery order.
- `SagaExecutionError`: Wraps unexpected non-Fluo saga failures.

@@ -229,0 +229,0 @@ - `SagaTopologyError`: Raised when saga orchestration detects a self-triggering, cyclic, or over-deep in-process saga graph.