-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor handler stores and publish strategy factory + tests
- Loading branch information
Showing
12 changed files
with
837 additions
and
171 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,10 @@ | ||
import { | ||
assertEquals, | ||
assertRejects, | ||
assertThrows, | ||
} from "https://deno.land/[email protected]/testing/asserts.ts"; | ||
import { Mediator } from "./mediator.ts"; | ||
import { Notification } from "./notification.ts"; | ||
import { PublishStrategy } from "./publish-strategy.ts"; | ||
import { Request } from "./request.ts"; | ||
import { Rhum } from "https://deno.land/x/[email protected]/mod.ts"; | ||
|
||
Deno.test("Mediator", async (t) => { | ||
Rhum.testPlan("Mediator", () => { | ||
// Setup | ||
class TestClass1 extends Request<Promise<number>> {} | ||
class TestClass3 extends Request {} | ||
|
@@ -20,51 +16,70 @@ Deno.test("Mediator", async (t) => { | |
}); | ||
const expected = 42; | ||
|
||
await t.step("can add handler for request type", () => { | ||
mediator.handle(TestClass1, () => Promise.resolve(expected)); | ||
}); | ||
Rhum.testSuite("handle()", () => { | ||
Rhum.testCase("when request type, it succeeds", () => { | ||
mediator.handle(TestClass1, () => Promise.resolve(expected)); | ||
}); | ||
|
||
await t.step("can add handler for notification type", () => { | ||
mediator.handle(TestNotification1, () => Promise.resolve()); | ||
}); | ||
Rhum.testCase("when notification type, it succeeds", () => { | ||
mediator.handle(TestNotification1, () => Promise.resolve()); | ||
}); | ||
|
||
Rhum.testCase( | ||
"when multiple handlers for same notification type, it succeeds", | ||
() => { | ||
mediator.handle(TestNotification1, () => Promise.resolve()); | ||
}, | ||
); | ||
|
||
await t.step("can add multiple handlers for same notification type", () => { | ||
mediator.handle(TestNotification1, () => Promise.resolve()); | ||
Rhum.testCase( | ||
"when handler for request type previously registered, it fails", | ||
() => { | ||
Rhum.asserts.assertThrows(() => { | ||
mediator.handle(TestClass1, () => Promise.resolve(expected)); | ||
}); | ||
}, | ||
); | ||
}); | ||
|
||
await t.step("can add handler with no response for request type", () => { | ||
Rhum.testSuite("send()", () => { | ||
mediator.handle(TestClass3, () => {}); | ||
mediator.send(new TestClass3()); | ||
}); | ||
|
||
await t.step( | ||
"cannot add a request handler for the same type more than once", | ||
() => { | ||
assertThrows(() => { | ||
mediator.handle(TestClass1, () => Promise.resolve(expected)); | ||
}); | ||
}, | ||
); | ||
Rhum.testCase("when handler with void response, it succeeds", () => { | ||
mediator.send(new TestClass3()); | ||
}); | ||
|
||
await t.step("send request calls correct handler", async () => { | ||
assertEquals(await mediator.send(new TestClass1()), expected); | ||
}); | ||
Rhum.testCase("when handler with value response, it succeeds", async () => { | ||
Rhum.asserts.assertEquals( | ||
await mediator.send(new TestClass1()), | ||
expected, | ||
); | ||
}); | ||
|
||
await t.step("publish notification calls correct handlers", async () => { | ||
await mediator.publish(new TestNotification1()); | ||
}); | ||
Rhum.testCase( | ||
"when no registered handler, it throws exception", | ||
() => { | ||
Rhum.asserts.assertThrows(() => | ||
mediator.send(new UnregisteredRequest()) | ||
); | ||
}, | ||
); | ||
|
||
await t.step( | ||
"send request has exception if there is no registered handler", | ||
() => { | ||
assertThrows(() => mediator.send(new UnregisteredRequest())); | ||
}, | ||
); | ||
Rhum.testCase( | ||
"when no registered async handler, it throws exception", | ||
() => { | ||
Rhum.asserts.assertRejects(() => | ||
mediator.send(new UnregisteredPromiseRequest()) | ||
); | ||
}, | ||
); | ||
}); | ||
|
||
await t.step( | ||
"send async request has exception if there is no registered handler", | ||
() => { | ||
assertRejects(() => mediator.send(new UnregisteredPromiseRequest())); | ||
}, | ||
); | ||
Rhum.testSuite("publish()", () => { | ||
Rhum.testCase("when notification, it calls correct handlers", async () => { | ||
await mediator.publish(new TestNotification1()); | ||
}); | ||
}); | ||
}); | ||
|
||
Rhum.run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,155 +1,83 @@ | ||
import { Notification } from "./notification.ts"; | ||
import { PublishStrategy } from "./publish-strategy.ts"; | ||
import { Request } from "./request.ts"; | ||
import { RequestHandlerStore } from "./request-handler-store.ts"; | ||
import { NotificationHandlerStore } from "./notification-handler-store.ts"; | ||
import { | ||
AnyType, | ||
Constructor, | ||
Handler, | ||
NotificationConstructor, | ||
NotificationHandler, | ||
RequestConstructor, | ||
RequestHandler, | ||
Response, | ||
} from "./types.ts"; | ||
import { IPublisher, PublisherFactory } from "./publisher-factory.ts"; | ||
import { TypeGuards } from "./type-guards.ts"; | ||
|
||
interface MediatorConfig { | ||
publishStratey?: PublishStrategy; | ||
} | ||
|
||
export class Mediator { | ||
#notificationHandlers: Record< | ||
symbol, | ||
Array<NotificationHandler<Notification>> | ||
> = {}; | ||
#requestHandlers: Record<symbol, RequestHandler> = {}; | ||
#publishStrategy: PublishStrategy; | ||
#notificationHandlers: NotificationHandlerStore = | ||
new NotificationHandlerStore(); | ||
#requestHandlers: RequestHandlerStore = new RequestHandlerStore(); | ||
#publishStrategy: IPublisher; | ||
|
||
constructor(config?: MediatorConfig) { | ||
this.#publishStrategy = config?.publishStratey ?? | ||
PublishStrategy.SyncContinueOnException; | ||
this.#publishStrategy = PublisherFactory.create( | ||
config?.publishStratey ?? | ||
PublishStrategy.SyncContinueOnException, | ||
); | ||
} | ||
|
||
public handle<TRequest extends (Request<AnyType> | Notification)>( | ||
constructor: Constructor<TRequest>, | ||
handler: Handler<TRequest>, | ||
): void { | ||
if ( | ||
this.isRequestConstructor(constructor) | ||
) { | ||
const { name, requestTypeId } = constructor; | ||
|
||
if (requestTypeId in this.#requestHandlers) { | ||
throw new Error(`Handler for ${name} already exists`); | ||
} | ||
|
||
this.#requestHandlers = { | ||
...this.#requestHandlers, | ||
[requestTypeId]: handler, | ||
}; | ||
|
||
if (TypeGuards.isRequestConstructor(constructor)) { | ||
this.#requestHandlers.add(constructor, handler); | ||
return; | ||
} | ||
|
||
if (this.isNotificationConstructor(constructor)) { | ||
const { notificationTypeId } = constructor; | ||
this.#notificationHandlers = { | ||
...this.#notificationHandlers, | ||
[notificationTypeId]: [ | ||
...(this.#notificationHandlers[notificationTypeId] ?? []), | ||
handler as NotificationHandler<Notification>, | ||
], | ||
}; | ||
if (TypeGuards.isNotificationConstructor(constructor)) { | ||
this.#notificationHandlers.add( | ||
constructor, | ||
handler as NotificationHandler, | ||
); | ||
return; | ||
} | ||
|
||
throw new Error(`Invalid request or notification`); | ||
} | ||
|
||
public async publish<TNotification extends Notification>( | ||
notificaiton: TNotification, | ||
notification: TNotification, | ||
publishStrategy?: PublishStrategy, | ||
): Promise<void> { | ||
const { constructor } = notificaiton; | ||
|
||
if (!this.isNotificationConstructor(constructor)) { | ||
throw new Error(`No handler found for notification, ${constructor.name}`); | ||
const publisher = publishStrategy != null | ||
? PublisherFactory.create(publishStrategy) | ||
: this.#publishStrategy; | ||
|
||
if (!TypeGuards.isNotification(notification)) { | ||
throw new Error( | ||
`No handler found for notification, ${notification.constructor.name}`, | ||
); | ||
} | ||
|
||
const handlers = | ||
this.#notificationHandlers[constructor.notificationTypeId] ?? []; | ||
|
||
const aggregateErrors: Error[] = []; | ||
switch (publishStrategy ?? this.#publishStrategy) { | ||
case PublishStrategy.ParallelNoWait: | ||
handlers.forEach((handler) => handler(notificaiton)); | ||
break; | ||
case PublishStrategy.ParallelWhenAny: | ||
await Promise.any(handlers.map((handler) => handler(notificaiton))); | ||
break; | ||
case PublishStrategy.ParallelWhenAll: | ||
case PublishStrategy.Async: | ||
await Promise.all(handlers.map((handler) => handler(notificaiton))); | ||
break; | ||
case PublishStrategy.SyncContinueOnException: | ||
for (const handler of handlers) { | ||
try { | ||
await handler(notificaiton); | ||
} catch (error) { | ||
aggregateErrors.push(error); | ||
} | ||
} | ||
|
||
if (aggregateErrors.length > 0) { | ||
throw new AggregateError(aggregateErrors); | ||
} | ||
|
||
break; | ||
case PublishStrategy.SyncStopOnException: | ||
for (const handler of handlers) { | ||
await handler(notificaiton); | ||
} | ||
break; | ||
default: | ||
throw new Error(`Invalid publish strategy`); | ||
} | ||
await publisher.publish( | ||
notification, | ||
this.#notificationHandlers.get(notification), | ||
); | ||
} | ||
|
||
public send<TRequest extends Request>( | ||
requestOrNotification: TRequest, | ||
request: TRequest, | ||
): Response<TRequest> { | ||
if ( | ||
requestOrNotification instanceof Request && | ||
this.isRequestConstructor(requestOrNotification.constructor) | ||
) { | ||
const { name, requestTypeId } = requestOrNotification.constructor; | ||
|
||
if (!(requestTypeId in this.#requestHandlers)) { | ||
throw new Error(`No handler found for request, ${name}`); | ||
} | ||
|
||
const handler = this.#requestHandlers[requestTypeId]; | ||
|
||
return handler(requestOrNotification); | ||
if (TypeGuards.isRequest<TRequest>(request)) { | ||
const handler = this.#requestHandlers.get<TRequest>(request); | ||
return handler(request); | ||
} | ||
|
||
throw new Error(`Invalid request`); | ||
} | ||
|
||
private isNotificationConstructor<TNotification extends Notification>( | ||
constructor: AnyType, | ||
): constructor is NotificationConstructor<TNotification> { | ||
return ( | ||
constructor.notificationTypeId != null && | ||
typeof constructor.notificationTypeId === "symbol" | ||
); | ||
} | ||
|
||
private isRequestConstructor<TRequest extends Request>( | ||
constructor: AnyType, | ||
): constructor is RequestConstructor<TRequest> { | ||
return ( | ||
constructor.requestTypeId != null && | ||
typeof constructor.requestTypeId === "symbol" | ||
); | ||
} | ||
} |
Oops, something went wrong.