Skip to content

Commit 979399c

Browse files
committed
fix(@app/serverless): common circular dependencies problem in inmemory implementations addressed
1 parent 105f15f commit 979399c

File tree

14 files changed

+123
-40
lines changed

14 files changed

+123
-40
lines changed

src/apps/example-context/serverless-backend/src/config/dependency-injection/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import appRegister from '@src/config/dependency-injection/app/application.di';
22
import exampleAggregateRegister from '@src/config/dependency-injection/example-aggregate/application.di';
3+
import setupBuses from '@src/config/dependency-injection/setupBuses';
34
import sharedRegister from '@src/config/dependency-injection/shared/application.di';
45
import { ContainerBuilder } from 'node-dependency-injection';
56

@@ -9,4 +10,6 @@ sharedRegister(container);
910
appRegister(container);
1011
exampleAggregateRegister(container);
1112

13+
setupBuses(container);
14+
1215
export default container;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type DomainEvent from '@context/shared/domain/eventBus/domainEvent';
2+
import type { DomainEventSubscriber } from '@context/shared/domain/eventBus/domainEventSubscriber';
3+
import type CommandHandlersInformation from '@context/shared/infrastructure/commandBus/commandHandlersInformation';
4+
import type InMemoryCommandBus from '@context/shared/infrastructure/commandBus/inMemoryCommandBus';
5+
import type InMemorySyncEventBus from '@context/shared/infrastructure/eventBus/inMemorySyncEventBus';
6+
import type InMemoryQueryBus from '@context/shared/infrastructure/queryBus/inMemoryQueryBus';
7+
import type QueryHandlersInformation from '@context/shared/infrastructure/queryBus/queryHandlersInformation';
8+
import type { ContainerBuilder } from 'node-dependency-injection';
9+
10+
function setupInMemoryCommandBus(container: ContainerBuilder): void {
11+
const mapping = container.get<CommandHandlersInformation>('Shared.CommandHandlersInformation'),
12+
commandBus = container.get<InMemoryCommandBus>('Shared.CommandBus');
13+
14+
commandBus.registerHandlers(mapping);
15+
}
16+
17+
function setupInMemoryQueryBus(container: ContainerBuilder): void {
18+
const mapping = container.get<QueryHandlersInformation>('Shared.QueryHandlersInformation'),
19+
queryBus = container.get<InMemoryQueryBus>('Shared.QueryBus');
20+
21+
queryBus.registerHandlers(mapping);
22+
}
23+
24+
function setupInMemoryEventBus(container: ContainerBuilder): void {
25+
const subscriberDefinitions = container.findTaggedServiceIds('eventSubscriber'),
26+
eventBus = container.get<InMemorySyncEventBus>('Shared.InMemoryEventBus'),
27+
subscribers: DomainEventSubscriber<DomainEvent>[] = [];
28+
29+
for (const { id } of subscriberDefinitions) {
30+
subscribers.push(container.get(id));
31+
}
32+
33+
eventBus.registerSubscribers(subscribers);
34+
}
35+
36+
// This functions help to break circular dependencies in the DI container. Instead of injecting the mappings directly into the buses'
37+
// constructors (which would attempt to instanciate handlers and would lead to a circular dependency), we register handlers and
38+
// subscribers after the buses have been created.
39+
export default function setupBuses(container: ContainerBuilder): void {
40+
setupInMemoryCommandBus(container);
41+
setupInMemoryQueryBus(container);
42+
setupInMemoryEventBus(container);
43+
}

src/apps/example-context/serverless-backend/src/config/dependency-injection/shared/application.di.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import DomainEventMapping from '@context/shared/domain/eventBus/domainEventMapping';
2+
import CommandHandlersInformation from '@context/shared/infrastructure/commandBus/commandHandlersInformation';
3+
import InMemoryCommandBus from '@context/shared/infrastructure/commandBus/inMemoryCommandBus';
24
import CurrentTimeClock from '@context/shared/infrastructure/currentTimeClock';
35
import EventBridgeClientFactory from '@context/shared/infrastructure/eventBus/eventBridge/eventBridgeClientFactory';
46
import EventBridgeEventBus from '@context/shared/infrastructure/eventBus/eventBridge/eventBridgeEventBus';
@@ -9,6 +11,8 @@ import DdbOneTableClientFactory from '@context/shared/infrastructure/persistence
911
import DdbOneTableDomainEventRepository from '@context/shared/infrastructure/persistence/ddbOneTable/ddbOneTableDomainEventRepository';
1012
import DynamodbClientFactory from '@context/shared/infrastructure/persistence/dynamodb/dynamodbClientFactory';
1113
import DynamodbStreamsOutboxConsumer from '@context/shared/infrastructure/persistence/dynamodb/dynamodbStreamsOutboxConsumer';
14+
import InMemoryQueryBus from '@context/shared/infrastructure/queryBus/inMemoryQueryBus';
15+
import QueryHandlersInformation from '@context/shared/infrastructure/queryBus/queryHandlersInformation';
1216
import config from '@src/config/config';
1317
import { type ContainerBuilder, Definition, Reference, TagReference } from 'node-dependency-injection';
1418

@@ -85,6 +89,12 @@ const serviceName = config.get('serviceName'),
8589
// .addArgument(new Reference('Shared.EventBus.NoFailover'))
8690
.addArgument(failoverOrOutboxConfig)
8791
.addTag('dynamodbStreamProcessor');
92+
93+
container.register('Shared.QueryHandlersInformation', QueryHandlersInformation).addArgument(new TagReference('queryHandler'));
94+
container.register('Shared.QueryBus', InMemoryQueryBus);
95+
96+
container.register('Shared.CommandHandlersInformation', CommandHandlersInformation).addArgument(new TagReference('commandHandler'));
97+
container.register('Shared.CommandBus', InMemoryCommandBus);
8898
};
8999

90100
export default register;

src/apps/example-context/serverless-backend/src/subscribers/domainEvents/registerSubscribers.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/apps/example-context/serverless-backend/src/subscribers/domainEvents/subscribers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import type { DomainEventUnmarshaller } from '@context/shared/domain/eventBus/do
55
import type { EventBus } from '@context/shared/domain/eventBus/eventBus';
66
import type { Logger } from '@context/shared/domain/logger';
77
import container from '@src/config/dependency-injection';
8-
import registerSubscribers from '@src/subscribers/domainEvents/registerSubscribers';
98

109
const logger: Logger = container.get('Shared.Logger'),
1110
clock: Clock = container.get('Shared.Clock'),
1211
unmarshaller: DomainEventUnmarshaller = container.get('Shared.EventBus.EventMarshaller'),
13-
eventBus: EventBus = registerSubscribers(container);
12+
eventBus: EventBus = container.get('Shared.InMemoryEventBus');
1413

1514
export const on: EventBridgeHandler<string, Record<string, unknown>, void> = async (event) => {
1615
const domainEvents = [unmarshaller.unmarshall(JSON.stringify(event.detail))];

src/contexts/shared/src/infrastructure/commandBus/inMemoryCommandBus.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,23 @@ class MyCommandHandler implements CommandHandler<HandledCommand> {
2323
}
2424

2525
describe('inMemoryCommandBus', () => {
26+
it('throws an error when no handlers are registered', async () => {
27+
expect.hasAssertions();
28+
29+
const unhandledCommand = new UnhandledCommand(),
30+
commandBus = new InMemoryCommandBus();
31+
32+
await expect(commandBus.dispatch(unhandledCommand)).rejects.toThrow(Error);
33+
});
34+
2635
it('throws an error if dispatches a command without handler', async () => {
2736
expect.hasAssertions();
2837

2938
const unhandledCommand = new UnhandledCommand(),
3039
commandHandlersInformation = new CommandHandlersInformation([]),
31-
commandBus = new InMemoryCommandBus(commandHandlersInformation);
40+
commandBus = new InMemoryCommandBus();
41+
42+
commandBus.registerHandlers(commandHandlersInformation);
3243

3344
await expect(commandBus.dispatch(unhandledCommand)).rejects.toThrow(CommandNotRegisteredError);
3445
});
@@ -37,7 +48,9 @@ describe('inMemoryCommandBus', () => {
3748
const handledCommand = new HandledCommand(),
3849
myCommandHandler = new MyCommandHandler(),
3950
commandHandlersInformation = new CommandHandlersInformation([myCommandHandler]),
40-
commandBus = new InMemoryCommandBus(commandHandlersInformation);
51+
commandBus = new InMemoryCommandBus();
52+
53+
commandBus.registerHandlers(commandHandlersInformation);
4154

4255
await commandBus.dispatch(handledCommand);
4356
});

src/contexts/shared/src/infrastructure/commandBus/inMemoryCommandBus.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import type { CommandBus } from '@src/domain/commandBus/commandBus';
33
import type CommandHandlersInformation from '@src/infrastructure/commandBus/commandHandlersInformation';
44

55
export default class InMemoryCommandBus implements CommandBus {
6-
private commandHandlersInformation: CommandHandlersInformation;
6+
private commandHandlersInformation?: CommandHandlersInformation;
77

8-
constructor(commandHandlersInformation: CommandHandlersInformation) {
8+
registerHandlers(commandHandlersInformation: CommandHandlersInformation): void {
99
this.commandHandlersInformation = commandHandlersInformation;
1010
}
1111

1212
async dispatch(command: Command): Promise<void> {
13+
if (!this.commandHandlersInformation) {
14+
throw new Error('No command handlers registered');
15+
}
16+
1317
const handler = this.commandHandlersInformation.search(command);
1418

1519
await handler.handle(command);

src/contexts/shared/src/infrastructure/eventBus/inMemoryAsyncEventBus.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,23 @@ class DomainEventSubscriberDummy implements DomainEventSubscriber<DummyEvent> {
4545
}
4646

4747
describe('inMemoryAsyncEventBus', () => {
48+
it('throws an error when no subscribers are registered', async () => {
49+
expect.hasAssertions();
50+
51+
const event = new DummyEvent({ id: ObjectMother.uuid() }),
52+
eventBus = new InMemoryAsyncEventBus();
53+
54+
await expect(eventBus.publish([event])).rejects.toThrow(Error);
55+
});
56+
4857
it('the subscriber should be called when the event it is subscribed to is published', async () => {
4958
expect.assertions(1);
5059

5160
const event = new DummyEvent({ id: ObjectMother.uuid() }),
5261
subscriber = new DomainEventSubscriberDummy(),
53-
eventBus = new InMemoryAsyncEventBus([subscriber]);
62+
eventBus = new InMemoryAsyncEventBus();
63+
64+
eventBus.registerSubscribers([subscriber]);
5465

5566
subscriber.setExpectation((actual: DummyEvent) => {
5667
expect(actual).toStrictEqual(event);

src/contexts/shared/src/infrastructure/eventBus/inMemoryAsyncEventBus.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import type { EventBus } from '@src/domain/eventBus/eventBus';
44
import EventEmitterBus from '@src/infrastructure/eventBus/eventEmitterBus';
55

66
export default class InMemoryAsyncEventBus implements EventBus {
7-
private readonly bus: EventEmitterBus;
7+
private bus?: EventEmitterBus;
88

9-
constructor(subscribers: DomainEventSubscriber<DomainEvent>[]) {
9+
registerSubscribers(subscribers: DomainEventSubscriber<DomainEvent>[]): void {
1010
this.bus = new EventEmitterBus(subscribers);
1111
}
1212

1313
publish(events: DomainEvent[]): Promise<void> {
14+
if (!this.bus) {
15+
return Promise.reject(new Error('No event subscribers registered'));
16+
}
17+
1418
this.bus.publish(events);
1519
return Promise.resolve();
1620
}

src/contexts/shared/src/infrastructure/eventBus/inMemorySyncEventBus.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ describe('inMemorySyncEventBus', () => {
5050

5151
const event = new DummyEvent({ id: ObjectMother.uuid() }),
5252
subscriber = new DomainEventSubscriberDummy(),
53-
eventBus = new InMemorySyncEventBus([subscriber]);
53+
eventBus = new InMemorySyncEventBus();
54+
55+
eventBus.registerSubscribers([subscriber]);
5456

5557
subscriber.setExpectation((actual: DummyEvent) => {
5658
expect(actual).toStrictEqual(event);

0 commit comments

Comments
 (0)