From 72216ebbeadfc72a5e33e5d66671d5b6a627c693 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:51:46 +0900 Subject: [PATCH] Create third-party.mediatr --- .eslintrc.cjs | 11 ++ packages/app/src/index.ts | 1 + packages/base/src/StringWriter.ts | 5 +- packages/base/src/TextWriter.ts | 14 +++ packages/third-party.mediatr/src/index.ts | 11 ++ .../src/mediatr.contracts/INotification.ts | 2 + .../src/mediatr.contracts/IRequest.ts | 5 + .../src/mediatr/IMediator.ts | 6 + .../src/mediatr/INotificationHandler.ts | 19 +++ .../src/mediatr/INotificationPublisher.ts | 11 ++ .../src/mediatr/IPublisher.ts | 8 ++ .../src/mediatr/IRequestHandler.ts | 9 ++ .../src/mediatr/ISender.ts | 6 + .../src/mediatr/Mediatr.ts | 70 +++++++++++ .../mediatr/NotificationHandlerExecutor.ts | 11 ++ .../extensions-di/MediatRServiceConfig.ts | 26 ++++ .../ServiceCollectionExtensions.ts | 27 +++++ .../ForOfAwaitPublisher.ts | 15 +++ .../PromiseAllPublisher.ts | 17 +++ .../src/mediatr/registrar/ServiceRegistrar.ts | 41 +++++++ .../wrappers/NotificationHandlerWrapper.ts | 48 ++++++++ .../test/NotificationHandler.test.ts | 39 ++++++ .../test/NotificationPublisher.test.ts | 49 ++++++++ .../third-party.mediatr/test/Publish.test.ts | 113 ++++++++++++++++++ tsconfig.base.json | 1 + vitest.config.ts | 4 + 26 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 packages/third-party.mediatr/src/index.ts create mode 100644 packages/third-party.mediatr/src/mediatr.contracts/INotification.ts create mode 100644 packages/third-party.mediatr/src/mediatr.contracts/IRequest.ts create mode 100644 packages/third-party.mediatr/src/mediatr/IMediator.ts create mode 100644 packages/third-party.mediatr/src/mediatr/INotificationHandler.ts create mode 100644 packages/third-party.mediatr/src/mediatr/INotificationPublisher.ts create mode 100644 packages/third-party.mediatr/src/mediatr/IPublisher.ts create mode 100644 packages/third-party.mediatr/src/mediatr/IRequestHandler.ts create mode 100644 packages/third-party.mediatr/src/mediatr/ISender.ts create mode 100644 packages/third-party.mediatr/src/mediatr/Mediatr.ts create mode 100644 packages/third-party.mediatr/src/mediatr/NotificationHandlerExecutor.ts create mode 100644 packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts create mode 100644 packages/third-party.mediatr/src/mediatr/extensions-di/ServiceCollectionExtensions.ts create mode 100644 packages/third-party.mediatr/src/mediatr/notification-publishers/ForOfAwaitPublisher.ts create mode 100644 packages/third-party.mediatr/src/mediatr/notification-publishers/PromiseAllPublisher.ts create mode 100644 packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts create mode 100644 packages/third-party.mediatr/src/mediatr/wrappers/NotificationHandlerWrapper.ts create mode 100644 packages/third-party.mediatr/test/NotificationHandler.test.ts create mode 100644 packages/third-party.mediatr/test/NotificationPublisher.test.ts create mode 100644 packages/third-party.mediatr/test/Publish.test.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 53a44a31..5eb170ae 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -245,6 +245,10 @@ module.exports = { type: '@yohira/third-party.inversify', pattern: 'packages/third-party.inversify/*', }, + { + type: '@yohira/third-party.mediatr', + pattern: 'packages/third-party.mediatr/*', + }, { type: '@yohira/third-party.ts-results', pattern: 'packages/third-party.ts-results/*', @@ -831,6 +835,13 @@ module.exports = { from: '@yohira/third-party.inversify', allow: [], }, + { + from: '@yohira/third-party.mediatr', + allow: [ + '@yohira/base', + '@yohira/extensions.dependency-injection.abstractions', + ], + }, { from: '@yohira/third-party.ts-results', allow: [], diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 8902dd0b..c25bca7d 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -41,4 +41,5 @@ export * from '@yohira/server.node'; export * from '@yohira/server.node.core'; export * from '@yohira/static-files'; export * from '@yohira/third-party.inversify'; +export * from '@yohira/third-party.mediatr'; export * from '@yohira/third-party.ts-results'; diff --git a/packages/base/src/StringWriter.ts b/packages/base/src/StringWriter.ts index 5da31d24..a38fea7a 100644 --- a/packages/base/src/StringWriter.ts +++ b/packages/base/src/StringWriter.ts @@ -3,9 +3,12 @@ import { TextWriter } from './TextWriter'; // https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/IO/StringWriter.cs,fd76db5d443fe076,references export class StringWriter extends TextWriter { - private readonly sb = new StringBuilder(); private isOpen = true; + constructor(private readonly sb: StringBuilder = new StringBuilder()) { + super(); + } + [Symbol.dispose](): void { this.isOpen = false; super[Symbol.dispose](); diff --git a/packages/base/src/TextWriter.ts b/packages/base/src/TextWriter.ts index ed75114d..dfeaf108 100644 --- a/packages/base/src/TextWriter.ts +++ b/packages/base/src/TextWriter.ts @@ -34,4 +34,18 @@ export abstract class TextWriter implements Disposable { this.writeChar(buffer[index + i]); } } + + writeString(value: string | undefined): void { + if (value !== undefined) { + const chars = value.split('').map((char) => char.charCodeAt(0)); + this.writeChars(chars, 0, chars.length); + } + } + + writeLine(value: string | undefined): void { + if (value !== undefined) { + this.writeString(value); + } + this.writeString('\n'); + } } diff --git a/packages/third-party.mediatr/src/index.ts b/packages/third-party.mediatr/src/index.ts new file mode 100644 index 00000000..b5bface6 --- /dev/null +++ b/packages/third-party.mediatr/src/index.ts @@ -0,0 +1,11 @@ +export * from './mediatr/extensions-di/ServiceCollectionExtensions'; +export * from './mediatr/notification-publishers/ForOfAwaitPublisher'; +export * from './mediatr/notification-publishers/PromiseAllPublisher'; +export * from './mediatr/registrar/ServiceRegistrar'; +export * from './mediatr/IMediator'; +export * from './mediatr/INotificationHandler'; +export * from './mediatr/IPublisher'; +export * from './mediatr/IRequestHandler'; +export * from './mediatr/ISender'; +export * from './mediatr.contracts/INotification'; +export * from './mediatr.contracts/IRequest'; diff --git a/packages/third-party.mediatr/src/mediatr.contracts/INotification.ts b/packages/third-party.mediatr/src/mediatr.contracts/INotification.ts new file mode 100644 index 00000000..85e2a3ed --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr.contracts/INotification.ts @@ -0,0 +1,2 @@ +// https://github.com/jbogard/MediatR/blob/a533cf288a50201aba3087ee7b521ec37f4337fd/src/MediatR.Contracts/INotification.cs#L6 +export interface INotification {} diff --git a/packages/third-party.mediatr/src/mediatr.contracts/IRequest.ts b/packages/third-party.mediatr/src/mediatr.contracts/IRequest.ts new file mode 100644 index 00000000..dcb68086 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr.contracts/IRequest.ts @@ -0,0 +1,5 @@ +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR.Contracts/IRequest.cs#L17 +export interface IBaseRequest {} + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR.Contracts/IRequest.cs#L12 +export interface IRequest extends IBaseRequest {} diff --git a/packages/third-party.mediatr/src/mediatr/IMediator.ts b/packages/third-party.mediatr/src/mediatr/IMediator.ts new file mode 100644 index 00000000..66395744 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/IMediator.ts @@ -0,0 +1,6 @@ +import { IPublisher } from './IPublisher'; +import { ISender } from './ISender'; + +export const IMediator = Symbol.for('IMediator'); +// https://github.com/jbogard/MediatR/blob/c4f1a918b4cb90030f2df0878f5930b9ed7baf16/src/MediatR/IMediator.cs#L6 +export interface IMediator extends ISender, IPublisher {} diff --git a/packages/third-party.mediatr/src/mediatr/INotificationHandler.ts b/packages/third-party.mediatr/src/mediatr/INotificationHandler.ts new file mode 100644 index 00000000..7fc79c19 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/INotificationHandler.ts @@ -0,0 +1,19 @@ +import { INotification } from '../mediatr.contracts/INotification'; + +// https://github.com/jbogard/MediatR/blob/c4f1a918b4cb90030f2df0878f5930b9ed7baf16/src/MediatR/INotificationHandler.cs#L10 +export interface INotificationHandler { + handle(notification: TNotification): Promise; +} + +// https://github.com/jbogard/MediatR/blob/c4f1a918b4cb90030f2df0878f5930b9ed7baf16/src/MediatR/INotificationHandler.cs#L25 +export abstract class NotificationHandler + implements INotificationHandler +{ + protected abstract handleCore(notification: TNotification): void; + + handle(notification: TNotification): Promise { + this.handleCore(notification); + + return Promise.resolve(); + } +} diff --git a/packages/third-party.mediatr/src/mediatr/INotificationPublisher.ts b/packages/third-party.mediatr/src/mediatr/INotificationPublisher.ts new file mode 100644 index 00000000..d2f0e0d6 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/INotificationPublisher.ts @@ -0,0 +1,11 @@ +import { INotification } from '../mediatr.contracts/INotification'; +import { NotificationHandlerExecutor } from './NotificationHandlerExecutor'; + +export const INotificationPublisher = Symbol.for('INotificationPublisher'); +// https://github.com/jbogard/MediatR/blob/838a8e12b62ee95f2f1caa503d282a8d9bce6047/src/MediatR/INotificationPublisher.cs#L7 +export interface INotificationPublisher { + publish( + handlerExecutors: NotificationHandlerExecutor[], + notification: INotification, + ): Promise; +} diff --git a/packages/third-party.mediatr/src/mediatr/IPublisher.ts b/packages/third-party.mediatr/src/mediatr/IPublisher.ts new file mode 100644 index 00000000..742b7e81 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/IPublisher.ts @@ -0,0 +1,8 @@ +import { INotification } from '../mediatr.contracts/INotification'; + +// https://github.com/jbogard/MediatR/blob/c4f1a918b4cb90030f2df0878f5930b9ed7baf16/src/MediatR/IPublisher.cs#L9 +export interface IPublisher { + publish( + notification: TNotification, + ): Promise; +} diff --git a/packages/third-party.mediatr/src/mediatr/IRequestHandler.ts b/packages/third-party.mediatr/src/mediatr/IRequestHandler.ts new file mode 100644 index 00000000..dbb9e9a9 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/IRequestHandler.ts @@ -0,0 +1,9 @@ +import { IRequest } from '../mediatr.contracts/IRequest'; + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR/IRequestHandler.cs#L11 +export interface IRequestHandler< + TRequest extends IRequest, + TResponse, +> { + handle(request: TRequest): Promise; +} diff --git a/packages/third-party.mediatr/src/mediatr/ISender.ts b/packages/third-party.mediatr/src/mediatr/ISender.ts new file mode 100644 index 00000000..1c302da9 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/ISender.ts @@ -0,0 +1,6 @@ +import { IRequest } from '../mediatr.contracts/IRequest'; + +// https://github.com/jbogard/MediatR/blob/761fb0b1b420f5a8c2cb4a751617dce7ab9c3fe3/src/MediatR/ISender.cs#L10 +export interface ISender { + send(request: IRequest): Promise; +} diff --git a/packages/third-party.mediatr/src/mediatr/Mediatr.ts b/packages/third-party.mediatr/src/mediatr/Mediatr.ts new file mode 100644 index 00000000..671e7aed --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/Mediatr.ts @@ -0,0 +1,70 @@ +import { Ctor, IServiceProvider, getOrAdd } from '@yohira/base'; +import { inject } from '@yohira/extensions.dependency-injection.abstractions'; + +import { INotification } from '../mediatr.contracts/INotification'; +import { IRequest } from '../mediatr.contracts/IRequest'; +import { IMediator } from './IMediator'; +import { INotificationPublisher } from './INotificationPublisher'; +import { NotificationHandlerExecutor } from './NotificationHandlerExecutor'; +import { + NotificationHandlerWrapper, + NotificationHandlerWrapperImpl, +} from './wrappers/NotificationHandlerWrapper'; + +// https://github.com/jbogard/MediatR/blob/f4de8196adafd37faff274ce819ada93a3d7531b/src/MediatR/Mediator.cs#L16 +export class Mediator implements IMediator { + private readonly notificationHandlers = new Map< + Ctor, + NotificationHandlerWrapper + >(); + + constructor( + @inject(IServiceProvider) + private readonly serviceProvider: IServiceProvider, + @inject(INotificationPublisher) + private readonly publisher: INotificationPublisher, + ) {} + + send(request: IRequest): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Override in a derived class to control how the tasks are awaited. By default the implementation calls the {@link INotificationPublisher}. + * @param handlerExecutors Enumerable of tasks representing invoking each notification handler + * @param notification The notification being published + * @returns A task representing invoking all handlers + */ + protected publishCore( + handlerExecutors: NotificationHandlerExecutor[], + notification: INotification, + ): Promise { + return this.publisher.publish(handlerExecutors, notification); + } + + private publishNotification(notification: INotification): Promise { + const handler = getOrAdd( + this.notificationHandlers, + notification.constructor as Ctor, + (notificationCtor) => { + const wrapper = new NotificationHandlerWrapperImpl( + notificationCtor, + ); + return wrapper; + }, + ); + + return handler.handle( + notification, + this.serviceProvider, + (executors, notification) => + this.publishCore(executors, notification), + ); + } + + publish( + notification: TNotification, + ): Promise { + return this.publishNotification(notification); + } +} diff --git a/packages/third-party.mediatr/src/mediatr/NotificationHandlerExecutor.ts b/packages/third-party.mediatr/src/mediatr/NotificationHandlerExecutor.ts new file mode 100644 index 00000000..7986b823 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/NotificationHandlerExecutor.ts @@ -0,0 +1,11 @@ +import { INotification } from '../mediatr.contracts/INotification'; + +// https://github.com/jbogard/MediatR/blob/838a8e12b62ee95f2f1caa503d282a8d9bce6047/src/MediatR/NotificationHandlerExecutor.cs#L7 +export class NotificationHandlerExecutor { + constructor( + readonly handlerInstance: object, + readonly handlerCallback: ( + notification: INotification, + ) => Promise, + ) {} +} diff --git a/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts b/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts new file mode 100644 index 00000000..7a29a1e6 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/extensions-di/MediatRServiceConfig.ts @@ -0,0 +1,26 @@ +import { Ctor } from '@yohira/base'; +import { ServiceLifetime } from '@yohira/extensions.dependency-injection.abstractions'; + +import { INotificationPublisher } from '../INotificationPublisher'; +import { Mediator } from '../Mediatr'; +import { ForOfAwaitPublisher } from '../notification-publishers/ForOfAwaitPublisher'; + +// https://github.com/jbogard/MediatR/blob/f4de8196adafd37faff274ce819ada93a3d7531b/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs#L9 +export class MediatRServiceConfig { + /** + * Mediator implementation type to register. Default is {@link Mediator} + */ + mediatorImplCtor: Ctor = Mediator; + /** + * Strategy for publishing notifications. Defaults to {@link ForeachAwaitPublisher} + */ + notificationPublisher: INotificationPublisher = new ForOfAwaitPublisher(); + /** + * Type of notification publisher strategy to register. If set, overrides {@link NotificationPublisher} + */ + notificationPublisherCtor?: Ctor; + /** + * Service lifetime to register services under. Default value is {@link ServiceLifetime.Transient} + */ + lifetime: ServiceLifetime = ServiceLifetime.Transient; +} diff --git a/packages/third-party.mediatr/src/mediatr/extensions-di/ServiceCollectionExtensions.ts b/packages/third-party.mediatr/src/mediatr/extensions-di/ServiceCollectionExtensions.ts new file mode 100644 index 00000000..2f015baf --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/extensions-di/ServiceCollectionExtensions.ts @@ -0,0 +1,27 @@ +import { IServiceCollection } from '@yohira/extensions.dependency-injection.abstractions'; + +import { addRequiredServices } from '../registrar/ServiceRegistrar'; +import { MediatRServiceConfig } from './MediatRServiceConfig'; + +function addMediatRCore( + services: IServiceCollection, + config: MediatRServiceConfig, +): IServiceCollection { + // TODO: addMediatRClasses(services, config); + + addRequiredServices(services, config); + + return services; +} + +// https://github.com/jbogard/MediatR/blob/f28cdc331faea401479d8e765b6f4dd536b2b085/src/MediatR/MicrosoftExtensionsDI/ServiceCollectionExtensions.cs#L26 +export function addMediatR( + services: IServiceCollection, + config: (serviceConfig: MediatRServiceConfig) => void, +): IServiceCollection { + const serviceConfig = new MediatRServiceConfig(); + + config(serviceConfig); + + return addMediatRCore(services, serviceConfig); +} diff --git a/packages/third-party.mediatr/src/mediatr/notification-publishers/ForOfAwaitPublisher.ts b/packages/third-party.mediatr/src/mediatr/notification-publishers/ForOfAwaitPublisher.ts new file mode 100644 index 00000000..51d319ef --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/notification-publishers/ForOfAwaitPublisher.ts @@ -0,0 +1,15 @@ +import { INotification } from '../../mediatr.contracts/INotification'; +import { INotificationPublisher } from '../INotificationPublisher'; +import { NotificationHandlerExecutor } from '../NotificationHandlerExecutor'; + +// https://github.com/jbogard/MediatR/blob/838a8e12b62ee95f2f1caa503d282a8d9bce6047/src/MediatR/NotificationPublishers/ForeachAwaitPublisher.cs#L17 +export class ForOfAwaitPublisher implements INotificationPublisher { + async publish( + handlerExecutors: NotificationHandlerExecutor[], + notification: INotification, + ): Promise { + for (const handler of handlerExecutors) { + await handler.handlerCallback(notification); + } + } +} diff --git a/packages/third-party.mediatr/src/mediatr/notification-publishers/PromiseAllPublisher.ts b/packages/third-party.mediatr/src/mediatr/notification-publishers/PromiseAllPublisher.ts new file mode 100644 index 00000000..277144e0 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/notification-publishers/PromiseAllPublisher.ts @@ -0,0 +1,17 @@ +import { INotification } from '../../mediatr.contracts/INotification'; +import { INotificationPublisher } from '../INotificationPublisher'; +import { NotificationHandlerExecutor } from '../NotificationHandlerExecutor'; + +// https://github.com/jbogard/MediatR/blob/40afa9fc6ec7ddcfc8fac2584861916fb571f817/src/MediatR/NotificationPublishers/TaskWhenAllPublisher.cs#L20 +export class PromiseAllPublisher implements INotificationPublisher { + async publish( + handlerExecutors: NotificationHandlerExecutor[], + notification: INotification, + ): Promise { + const tasks = handlerExecutors.map((handler) => + handler.handlerCallback(notification), + ); + + await Promise.all(tasks); + } +} diff --git a/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts b/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts new file mode 100644 index 00000000..d0224eb4 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/registrar/ServiceRegistrar.ts @@ -0,0 +1,41 @@ +import { + IServiceCollection, + ServiceDescriptor, + ServiceLifetime, + tryAddServiceDescriptor, +} from '@yohira/extensions.dependency-injection.abstractions'; + +import { IMediator } from '../IMediator'; +import { INotificationPublisher } from '../INotificationPublisher'; +import { MediatRServiceConfig } from '../extensions-di/MediatRServiceConfig'; + +// https://github.com/jbogard/MediatR/blob/43fb46f39020ab4880fefe75fa2315351f347742/src/MediatR/Registration/ServiceRegistrar.cs#L300 +export function addRequiredServices( + services: IServiceCollection, + serviceConfig: MediatRServiceConfig, +): void { + tryAddServiceDescriptor( + services, + ServiceDescriptor.fromCtor( + serviceConfig.lifetime, + IMediator, + serviceConfig.mediatorImplCtor, + ), + ); + // TODO + + const notificationPublisherServiceDescriptor = + serviceConfig.notificationPublisherCtor !== undefined + ? ServiceDescriptor.fromCtor( + serviceConfig.lifetime, + INotificationPublisher, + serviceConfig.notificationPublisherCtor, + ) + : ServiceDescriptor.fromInstance( + ServiceLifetime.Singleton, + INotificationPublisher, + serviceConfig.notificationPublisher, + ); + + tryAddServiceDescriptor(services, notificationPublisherServiceDescriptor); +} diff --git a/packages/third-party.mediatr/src/mediatr/wrappers/NotificationHandlerWrapper.ts b/packages/third-party.mediatr/src/mediatr/wrappers/NotificationHandlerWrapper.ts new file mode 100644 index 00000000..6b0d28c0 --- /dev/null +++ b/packages/third-party.mediatr/src/mediatr/wrappers/NotificationHandlerWrapper.ts @@ -0,0 +1,48 @@ +import { Ctor, IServiceProvider } from '@yohira/base'; +import { getServices } from '@yohira/extensions.dependency-injection.abstractions'; + +import { INotification } from '../../mediatr.contracts/INotification'; +import { INotificationHandler } from '../INotificationHandler'; +import { NotificationHandlerExecutor } from '../NotificationHandlerExecutor'; + +// https://github.com/jbogard/MediatR/blob/838a8e12b62ee95f2f1caa503d282a8d9bce6047/src/MediatR/Wrappers/NotificationHandlerWrapper.cs#L10 +export abstract class NotificationHandlerWrapper { + abstract handle( + notification: INotification, + serviceFactory: IServiceProvider, + publish: ( + executors: NotificationHandlerExecutor[], + notification: INotification, + ) => Promise, + ): Promise; +} + +// https://github.com/jbogard/MediatR/blob/838a8e12b62ee95f2f1caa503d282a8d9bce6047/src/MediatR/Wrappers/NotificationHandlerWrapper.cs#L17 +export class NotificationHandlerWrapperImpl< + TNotification extends INotification, +> extends NotificationHandlerWrapper { + constructor(private readonly notificationCtor: Ctor) { + super(); + } + + handle( + notification: INotification, + serviceFactory: IServiceProvider, + publish: ( + executors: NotificationHandlerExecutor[], + notification: INotification, + ) => Promise, + ): Promise { + const handlers = getServices>( + serviceFactory, + Symbol.for(`INotificationHandler<${this.notificationCtor.name}>`), + ).map( + (x) => + new NotificationHandlerExecutor(x, (theNotification) => + x.handle(theNotification as TNotification), + ), + ); + + return publish(handlers, notification); + } +} diff --git a/packages/third-party.mediatr/test/NotificationHandler.test.ts b/packages/third-party.mediatr/test/NotificationHandler.test.ts new file mode 100644 index 00000000..63d8963e --- /dev/null +++ b/packages/third-party.mediatr/test/NotificationHandler.test.ts @@ -0,0 +1,39 @@ +import { StringBuilder, StringWriter, TextWriter } from '@yohira/base'; +import { + INotification, + NotificationHandler, +} from '@yohira/third-party.mediatr'; +import { expect, test } from 'vitest'; + +class Ping implements INotification { + message: string | undefined; +} + +class PongChildHandler extends NotificationHandler { + constructor(private readonly writer: TextWriter) { + super(); + } + + handleCore(notification: Ping): void { + this.writer.writeString(notification.message + ' Pong'); + } +} + +// https://github.com/jbogard/MediatR/blob/e22f2f68f29dc19111987068afbfc99836efb11a/test/MediatR.Tests/NotificationHandlerTests.cs#L32 +test('Should_call_abstract_handle_method', async () => { + const builder = new StringBuilder(); + const writer = new StringWriter(builder); + + const handler = new PongChildHandler(writer); + + await handler.handle( + ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(), + ); + + const result = builder.toString(); + expect(result).toContain('Ping Pong'); +}); diff --git a/packages/third-party.mediatr/test/NotificationPublisher.test.ts b/packages/third-party.mediatr/test/NotificationPublisher.test.ts new file mode 100644 index 00000000..f9e35bb4 --- /dev/null +++ b/packages/third-party.mediatr/test/NotificationPublisher.test.ts @@ -0,0 +1,49 @@ +import { buildServiceProvider } from '@yohira/extensions.dependency-injection'; +import { + ServiceCollection, + getRequiredService, +} from '@yohira/extensions.dependency-injection.abstractions'; +import { + IMediator, + INotification, + PromiseAllPublisher, + addMediatR, +} from '@yohira/third-party.mediatr'; +import { expect, test } from 'vitest'; + +class Notification implements INotification {} + +// https://github.com/jbogard/MediatR/blob/e1a6418eab81fd56f433d7b031a2467a10fefeee/test/MediatR.Tests/NotificationPublisherTests.cs#L34 +test('Should_handle_sequentially_by_default', async () => { + let services = new ServiceCollection(); + addMediatR(services, (cfg) => {}); + let serviceProvider = buildServiceProvider(services); + + let mediator = getRequiredService(serviceProvider, IMediator); + + let start = Date.now(); + + await mediator.publish(new Notification()); + + let end = Date.now(); + + const sequentialElapsed = end - start; + + services = new ServiceCollection(); + addMediatR(services, (cfg) => { + cfg.notificationPublisherCtor = PromiseAllPublisher; + }); + serviceProvider = buildServiceProvider(services); + + mediator = getRequiredService(serviceProvider, IMediator); + + start = Date.now(); + + await mediator.publish(new Notification()); + + end = Date.now(); + + const parallelElapsed = end - start; + + expect(sequentialElapsed).greaterThan(parallelElapsed); +}); diff --git a/packages/third-party.mediatr/test/Publish.test.ts b/packages/third-party.mediatr/test/Publish.test.ts new file mode 100644 index 00000000..e3c89269 --- /dev/null +++ b/packages/third-party.mediatr/test/Publish.test.ts @@ -0,0 +1,113 @@ +import { StringBuilder, StringWriter, TextWriter } from '@yohira/base'; +import { buildServiceProvider } from '@yohira/extensions.dependency-injection'; +import { + ServiceCollection, + addTransientCtor, + addTransientInstance, + getRequiredService, + inject, +} from '@yohira/extensions.dependency-injection.abstractions'; +import { + IMediator, + INotification, + INotificationHandler, + addMediatR, +} from '@yohira/third-party.mediatr'; +import { expect, test } from 'vitest'; + +class Ping implements INotification { + message: string | undefined; +} + +class PongHandler implements INotificationHandler { + constructor( + @inject(Symbol.for('TextWriter')) private readonly writer: TextWriter, + ) {} + + handle(notification: Ping): Promise { + this.writer.writeLine(notification.message + ' Pong'); + return Promise.resolve(); + } +} + +class PungHandler implements INotificationHandler { + constructor( + @inject(Symbol.for('TextWriter')) private readonly writer: TextWriter, + ) {} + + handle(notification: Ping): Promise { + this.writer.writeLine(notification.message + ' Pung'); + return Promise.resolve(); + } +} + +// https://github.com/jbogard/MediatR/blob/395ef0f400bcb92f2d9f9d1371bc33ae68419ae6/test/MediatR.Tests/PublishTests.cs#L52 +test('Should_resolve_main_handler', async () => { + const builder = new StringBuilder(); + const writer = new StringWriter(builder); + + const services = new ServiceCollection(); + addTransientCtor( + services, + Symbol.for('INotificationHandler'), + PongHandler, + ); + addTransientCtor( + services, + Symbol.for('INotificationHandler'), + PungHandler, + ); + addTransientInstance(services, Symbol.for('TextWriter'), writer); + addMediatR(services, () => {}); + const serviceProvider = buildServiceProvider(services); + + const mediator = getRequiredService(serviceProvider, IMediator); + + await mediator.publish( + ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(), + ); + + const result = builder.toString().split('\n'); + expect(result).toContain('Ping Pong'); + expect(result).toContain('Ping Pung'); +}); + +// https://github.com/jbogard/MediatR/blob/395ef0f400bcb92f2d9f9d1371bc33ae68419ae6/test/MediatR.Tests/PublishTests.cs#L80C23-L80C72 +test('Should_resolve_main_handler_when_object_is_passed', async () => { + const builder = new StringBuilder(); + const writer = new StringWriter(builder); + + const services = new ServiceCollection(); + addTransientCtor( + services, + Symbol.for('INotificationHandler'), + PongHandler, + ); + addTransientCtor( + services, + Symbol.for('INotificationHandler'), + PungHandler, + ); + addTransientInstance(services, Symbol.for('TextWriter'), writer); + addMediatR(services, () => {}); + const serviceProvider = buildServiceProvider(services); + + const mediator = getRequiredService(serviceProvider, IMediator); + + const message = ((): Ping => { + const ping = new Ping(); + ping.message = 'Ping'; + return ping; + })(); + await mediator.publish(message); + + const result = builder.toString().split('\n'); + expect(result).toContain('Ping Pong'); + expect(result).toContain('Ping Pung'); +}); + +// TODO diff --git a/tsconfig.base.json b/tsconfig.base.json index 20f407e0..e7b0b385 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -113,6 +113,7 @@ "@yohira/third-party.inversify": [ "packages/third-party.inversify/src" ], + "@yohira/third-party.mediatr": ["packages/third-party.mediatr/src"], "@yohira/third-party.ts-results": [ "packages/third-party.ts-results/src" ], diff --git a/vitest.config.ts b/vitest.config.ts index 84408a21..9a1c9dbf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -169,6 +169,10 @@ export default defineConfig({ __dirname, './packages/third-party.inversify/src', ), + '@yohira/third-party.mediatr': resolve( + __dirname, + './packages/third-party.mediatr/src', + ), '@yohira/third-party.ts-results': resolve( __dirname, './packages/third-party.ts-results/src',