Skip to content

Commit

Permalink
refactor handler stores and publish strategy factory + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
myty committed Feb 7, 2022
1 parent 7123209 commit 1ef40ca
Show file tree
Hide file tree
Showing 12 changed files with 837 additions and 171 deletions.
101 changes: 58 additions & 43 deletions mediator.test.ts
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 {}
Expand All @@ -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();
144 changes: 36 additions & 108 deletions mediator.ts
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"
);
}
}
Loading

0 comments on commit 1ef40ca

Please sign in to comment.