Browse Source

feat(server): Create EventBus service, module & tests

Closes #40
Michael Bromley 7 years ago
parent
commit
303349e3b5

+ 9 - 0
server/src/event-bus/event-bus.module.ts

@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+
+import { EventBus } from './event-bus';
+
+@Module({
+    providers: [EventBus],
+    exports: [EventBus],
+})
+export class EventBusModule {}

+ 133 - 0
server/src/event-bus/event-bus.spec.ts

@@ -0,0 +1,133 @@
+import { EventBus } from './event-bus';
+import { VendureEvent } from './vendure-event';
+
+describe('EventBus', () => {
+    let eventBus: EventBus;
+
+    beforeEach(() => {
+        eventBus = new EventBus();
+    });
+
+    it('can publish without subscribers', () => {
+        const event = new TestEvent('foo');
+
+        expect(() => eventBus.publish(event)).not.toThrow();
+    });
+
+    it('single handler is called once', () => {
+        const handler = jest.fn();
+        const event = new TestEvent('foo');
+        eventBus.subscribe(TestEvent, handler);
+
+        eventBus.publish(event);
+
+        expect(handler).toHaveBeenCalledTimes(1);
+        expect(handler).toHaveBeenCalledWith(event);
+    });
+
+    it('single handler is called on multiple events', () => {
+        const handler = jest.fn();
+        const event1 = new TestEvent('foo');
+        const event2 = new TestEvent('bar');
+        const event3 = new TestEvent('baz');
+        eventBus.subscribe(TestEvent, handler);
+
+        eventBus.publish(event1);
+        eventBus.publish(event2);
+        eventBus.publish(event3);
+
+        expect(handler).toHaveBeenCalledTimes(3);
+        expect(handler).toHaveBeenCalledWith(event1);
+        expect(handler).toHaveBeenCalledWith(event2);
+        expect(handler).toHaveBeenCalledWith(event3);
+    });
+
+    it('subscribing same handler multiple times does not result in multiple dispatch of event', () => {
+        const handler = jest.fn();
+        const event = new TestEvent('foo');
+        eventBus.subscribe(TestEvent, handler);
+        eventBus.subscribe(TestEvent, handler);
+        eventBus.subscribe(TestEvent, handler);
+
+        eventBus.publish(event);
+
+        expect(handler).toHaveBeenCalledTimes(1);
+        expect(handler).toHaveBeenCalledWith(event);
+    });
+
+    it('multiple handlers are called', () => {
+        const handler1 = jest.fn();
+        const handler2 = jest.fn();
+        const handler3 = jest.fn();
+        const event = new TestEvent('foo');
+        eventBus.subscribe(TestEvent, handler1);
+        eventBus.subscribe(TestEvent, handler2);
+        eventBus.subscribe(TestEvent, handler3);
+
+        eventBus.publish(event);
+
+        expect(handler1).toHaveBeenCalledWith(event);
+        expect(handler2).toHaveBeenCalledWith(event);
+        expect(handler3).toHaveBeenCalledWith(event);
+    });
+
+    it('handler is not called for other events', () => {
+        const handler = jest.fn();
+        const event = new OtherTestEvent('foo');
+        eventBus.subscribe(TestEvent, handler);
+
+        eventBus.publish(event);
+
+        expect(handler).not.toHaveBeenCalled();
+    });
+
+    it('subscribe() returns an unsubscribe() funtion', () => {
+        const handler = jest.fn();
+        const event = new TestEvent('foo');
+        const unsubscribe = eventBus.subscribe(TestEvent, handler);
+
+        eventBus.publish(event);
+
+        expect(handler).toHaveBeenCalledTimes(1);
+
+        unsubscribe();
+
+        eventBus.publish(event);
+        eventBus.publish(event);
+
+        expect(handler).toHaveBeenCalledTimes(1);
+    });
+
+    it('unsubscribe() only unsubscribes own handler', () => {
+        const handler1 = jest.fn();
+        const handler2 = jest.fn();
+        const event = new TestEvent('foo');
+        const unsubscribe1 = eventBus.subscribe(TestEvent, handler1);
+        const unsubscribe2 = eventBus.subscribe(TestEvent, handler2);
+
+        eventBus.publish(event);
+
+        expect(handler1).toHaveBeenCalledTimes(1);
+        expect(handler2).toHaveBeenCalledTimes(1);
+
+        unsubscribe1();
+
+        eventBus.publish(event);
+        eventBus.publish(event);
+
+        expect(handler1).toHaveBeenCalledTimes(1);
+        expect(handler2).toHaveBeenCalledTimes(3);
+    });
+});
+
+class TestEvent extends VendureEvent {
+    constructor(public payload: string) {
+        super();
+    }
+}
+
+class OtherTestEvent extends VendureEvent {
+    constructor(public payload: string) {
+        super();
+    }
+}

+ 42 - 0
server/src/event-bus/event-bus.ts

@@ -0,0 +1,42 @@
+import { Injectable } from '@nestjs/common';
+import { Type } from 'shared/shared-types';
+
+import { VendureEvent } from './vendure-event';
+
+export type EventHandler<T extends VendureEvent> = (event: T) => void;
+export type UnsubscribeFn = () => void;
+
+/**
+ * The EventBus is used to globally publish events which can then be subscribed to.
+ */
+@Injectable()
+export class EventBus {
+    subscriberMap = new Map<VendureEvent, Array<EventHandler<any>>>();
+
+    /**
+     * Publish an event which any subscribers can react to.
+     */
+    publish(event: VendureEvent) {
+        const eventType = (event as any).constructor;
+        const handlers = this.subscriberMap.get(eventType);
+        if (handlers) {
+            const length = handlers.length;
+            for (let i = 0; i < length; i++) {
+                handlers[i](event);
+            }
+        }
+    }
+
+    /**
+     * Subscribe to the given event type. Returns an unsubscribe function which can be used
+     * to unsubscribe the handler from the event.
+     */
+    subscribe<T extends VendureEvent>(type: Type<T>, handler: EventHandler<T>): UnsubscribeFn {
+        const handlers = this.subscriberMap.get(type) || [];
+        if (!handlers.includes(handler)) {
+            handlers.push(handler);
+        }
+        this.subscriberMap.set(type, handlers);
+        return () => this.subscriberMap.set(type, handlers.filter(h => h !== handler));
+    }
+}

+ 7 - 0
server/src/event-bus/vendure-event.ts

@@ -0,0 +1,7 @@
+/**
+ * The base class for all events used by the EventBus system.
+ */
+export abstract class VendureEvent {
+    createdAt = new Date();
+    protected constructor() {}
+}