From 0c4872864f3a98d5fedffd940f4e26645b2aed97 Mon Sep 17 00:00:00 2001 From: Gabriele Toselli Date: Tue, 10 Sep 2024 16:02:09 +0200 Subject: [PATCH] feat(command-bus): introduce typed context to local command bus --- package.json | 3 +- .../src/command-bus/command-bus.interface.ts | 11 ++-- .../command-bus/context-manager.interface.ts | 3 ++ .../src/command-bus/local-command-bus.spec.ts | 53 +++++++++++++++++-- .../src/command-bus/local-command-bus.ts | 21 +++++--- 5 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 packages/ddd-toolkit/src/command-bus/context-manager.interface.ts diff --git a/package.json b/package.json index b977055..e767844 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,6 @@ "lint-staged": { "*.ts": "eslint --fix", "*.json": "prettier --write" - } + }, + "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" } diff --git a/packages/ddd-toolkit/src/command-bus/command-bus.interface.ts b/packages/ddd-toolkit/src/command-bus/command-bus.interface.ts index ea36790..1955245 100644 --- a/packages/ddd-toolkit/src/command-bus/command-bus.interface.ts +++ b/packages/ddd-toolkit/src/command-bus/command-bus.interface.ts @@ -8,12 +8,15 @@ export interface ICommandClass> { new (payload: unknown): C; } -export interface ICommandHandler> { - handle: (command: C) => Promise; +export interface ICommandHandler, TContext = void> { + handle: (command: C, context?: TContext) => Promise; } -export interface ICommandBus { - register>(command: ICommandClass, handler: ICommandHandler): void; +export interface ICommandBus { + register>( + command: ICommandClass, + handler: ICommandHandler, + ): void; send>(command: C): Promise; } diff --git a/packages/ddd-toolkit/src/command-bus/context-manager.interface.ts b/packages/ddd-toolkit/src/command-bus/context-manager.interface.ts new file mode 100644 index 0000000..885a64f --- /dev/null +++ b/packages/ddd-toolkit/src/command-bus/context-manager.interface.ts @@ -0,0 +1,3 @@ +export interface IContextManager { + wrapWithContext(operation: (context: TContext) => Promise): Promise; +} diff --git a/packages/ddd-toolkit/src/command-bus/local-command-bus.spec.ts b/packages/ddd-toolkit/src/command-bus/local-command-bus.spec.ts index 46d0b45..1b4b10b 100644 --- a/packages/ddd-toolkit/src/command-bus/local-command-bus.spec.ts +++ b/packages/ddd-toolkit/src/command-bus/local-command-bus.spec.ts @@ -3,6 +3,7 @@ import { Command } from './command'; import { waitFor } from '../utils'; import { ICommandHandler } from './command-bus.interface'; import { ILogger } from '../logger'; +import { IContextManager } from './context-manager.interface'; const loggerMock: ILogger = { log: jest.fn(), @@ -24,17 +25,15 @@ class BarCommand extends Command<{ foo: string }> { } describe('LocalCommandBus', () => { + afterEach(() => jest.resetAllMocks()); + describe('Given an command bus', () => { - let commandBus: LocalCommandBus; + let commandBus: LocalCommandBus; beforeEach(() => { commandBus = new LocalCommandBus(loggerMock, 3, 100); }); - afterEach(() => { - jest.resetAllMocks(); - }); - describe('Given no registered handler to foo command', () => { describe('When send a foo command', () => { it('Should log warning message', async () => { @@ -160,6 +159,50 @@ describe('LocalCommandBus', () => { }); }); + describe('Given a command bus with context manager', () => { + type TContext = { contextKey: string }; + + let commandBus: LocalCommandBus; + + class FooContextManager implements IContextManager { + public async wrapWithContext(operation: (context: TContext) => Promise): Promise { + const context: TContext = { contextKey: 'foo-bar' }; + return await operation(context); + } + } + + beforeEach(() => { + commandBus = new LocalCommandBus(loggerMock, 3, 100, new FooContextManager()); + }); + + it('should be defined', () => { + expect(commandBus).toBeDefined(); + }); + + describe('Given one registered handler to foo command', () => { + const FooHandlerMock = jest.fn(); + + class FooCommandHandler implements ICommandHandler { + async handle(...args: unknown[]) { + return await FooHandlerMock(args); + } + } + + beforeEach(() => { + commandBus.register(FooCommand, new FooCommandHandler()); + }); + + describe('When sendSync a foo command', () => { + it('context should be passed to command', async () => { + const command = new FooCommand({ foo: 'bar' }); + await commandBus.sendSync(command); + + expect(FooHandlerMock).toHaveBeenCalledWith([command, { contextKey: 'foo-bar' }]); + }); + }); + }); + }); + it('default retry max attempts should be 0', () => { const commandBus = new LocalCommandBus(loggerMock); expect(commandBus['retryMaxAttempts']).toBe(0); diff --git a/packages/ddd-toolkit/src/command-bus/local-command-bus.ts b/packages/ddd-toolkit/src/command-bus/local-command-bus.ts index c270bc8..ae40b45 100644 --- a/packages/ddd-toolkit/src/command-bus/local-command-bus.ts +++ b/packages/ddd-toolkit/src/command-bus/local-command-bus.ts @@ -2,30 +2,32 @@ import { ILogger } from '../logger'; import { ICommand, ICommandBus, ICommandClass, ICommandHandler } from './command-bus.interface'; import { ExponentialBackoff, IRetryMechanism } from '../event-bus'; import { inspect } from 'util'; +import { IContextManager } from './context-manager.interface'; -export class LocalCommandBus implements ICommandBus { +export class LocalCommandBus implements ICommandBus { private readonly retryMechanism: IRetryMechanism; - private handlers: { [key: string]: ICommandHandler> } = {}; + private handlers: { [key: string]: ICommandHandler, TContext> } = {}; constructor( private logger: ILogger, private readonly retryMaxAttempts = 0, retryInitialDelay = 500, + private readonly contextManager?: IContextManager, ) { this.retryMechanism = new ExponentialBackoff(retryInitialDelay); } public register>( command: ICommandClass, - handler: ICommandHandler, + handler: ICommandHandler, ): void { if (this.handlers[command.name]) throw new Error(`Command ${command.name} is already registered`); this.handlers[command.name] = handler; } public async send>(command: C): Promise { - const handler = this.handlers[command.name] as ICommandHandler; + const handler = this.handlers[command.name] as ICommandHandler; if (!handler) { this.logger.warn(`No handler found for ${command.name}`); return; @@ -35,14 +37,19 @@ export class LocalCommandBus implements ICommandBus { } public async sendSync>(command: C): Promise { - const handler = this.handlers[command.name] as ICommandHandler; + const handler = this.handlers[command.name] as ICommandHandler; if (!handler) throw new Error(`No handler found for ${command.name}`); - return await handler.handle(command); + + return this.contextManager + ? await this.contextManager.wrapWithContext(async (context) => { + return await handler.handle(command, context); + }) + : await handler.handle(command); } private async handleCommand>( command: C, - handler: ICommandHandler, + handler: ICommandHandler, attempt = 0, ) { try {