├── .gitignore
├── .npmignore
├── README.md
├── jest.config.js
├── package.json
├── src
├── EventDispatcher.ts
├── decorators
│ └── index.ts
├── index.ts
├── interfaces.ts
├── metadata
│ └── index.ts
└── utils
│ ├── Container.ts
│ ├── Logger.ts
│ ├── errors.ts
│ └── isPromise.ts
├── tests
└── functional
│ └── EventDispatcher.test.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | .idea
3 |
4 | # Below from: https://github.com/github/gitignore/blob/master/Node.gitignore
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # TypeScript v1 declaration files
50 | typings/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional REPL history
62 | .node_repl_history
63 |
64 | # Output of 'npm pack'
65 | *.tgz
66 |
67 | # Yarn Integrity file
68 | .yarn-integrity
69 |
70 | # dotenv environment variables file
71 | .env
72 | .env.test
73 |
74 | # parcel-bundler cache (https://parceljs.org/)
75 | .cache
76 |
77 | # next.js build output
78 | .next
79 |
80 | # nuxt.js build output
81 | .nuxt
82 |
83 | # gatsby files
84 | .cache/
85 | public
86 |
87 | # vuepress build output
88 | .vuepress/dist
89 |
90 | # Serverless directories
91 | .serverless/
92 |
93 | # FuseBox cache
94 | .fusebox/
95 |
96 | # DynamoDB Local files
97 | .dynamodb/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | coverage
3 | tests
4 | README.md
5 | tsconfig.json
6 | tslint.json
7 | *.spec.ts
8 | *.test.ts
9 | .prettierrc
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
type-events
2 |
3 | A simple @decorator based event dispatcher.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ### Basics
13 |
14 | `type-events` allows you to create simple ways dispatch and subscribe to events.
15 |
16 | ```typescript
17 | import { EventDispatcher, On } from 'type-events';
18 |
19 | class Conversion {
20 | constructor(public userAgent: string, public revenue: number) {}
21 | }
22 |
23 | class Impression {
24 | constructor(public userAgent: string) {}
25 | }
26 |
27 | export class TrackingSubscriber {
28 | @On(Conversion)
29 | async onConversion(event: Conversion): Promise {
30 | // do something with conversion events
31 | }
32 |
33 | // The higher the priority, the sooner it's processed.
34 | // Priority is not guaranteed for same-priority values.
35 | @On(Impression, { priority: 255 })
36 | async onImpression(event: Impression): Promise {
37 | // do something with impression events
38 | }
39 | }
40 |
41 | export class NotifySlack {
42 | // `background: true` makes this subscriber run after all other
43 | // subscribers and doesn't wait for the result to finish
44 | @On([Impression, Conversion], { background: true })
45 | async notify(event: Impression | Conversion): Promise {
46 | switch (event.constructor.name) {
47 | case 'Impression':
48 | // ...
49 | break;
50 | case 'Conversion':
51 | // ...
52 | break;
53 | }
54 | }
55 | }
56 |
57 | const dispatcher = new EventDispatcher({
58 | subscribers: [TrackingSubscriber, NotifySlack]
59 | });
60 |
61 | // then dispatch the events somewhere!
62 | dispatcher.dispatch(new Conversion('Chrome', 13.37));
63 | ```
64 |
65 | ### Custom Container (DI)
66 |
67 | Most of the time, you want to use some sort of dependency injection (DI) alongside event dispatching. Don't you worry, you can still do that.
68 | Just pass in an appropriate DI container with a valid `get` method.
69 |
70 | ```typescript
71 | import { Container } from 'inversify';
72 |
73 | const container = new Container();
74 | // container bindings go here
75 |
76 | const dispatcher = new EventDispatcher({
77 | subscribers: [...],
78 | container
79 | });
80 | ```
81 |
82 | ## Inheritance
83 |
84 | Events can extend base classes and subscribers can subscribe to those base classes.
85 |
86 | ```typescript
87 | import { EventDispatcher, On } from 'type-events';
88 |
89 | abstract class BaseEvent {
90 | // ...
91 | }
92 |
93 | class UserCreatedEvent extends BaseEvent {
94 | // ...
95 | }
96 |
97 | class LoggingSubscriber {
98 | @On(BaseEvent)
99 | async all(event: BaseEvent): Promise {
100 | console.log(event);
101 | }
102 | }
103 |
104 | // ...
105 |
106 | dispatcher.dispatch(new UserCreatedEvent());
107 | ```
108 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['/tests/**/*.test.ts'],
5 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '/lib/'],
6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
7 | globals: {
8 | 'ts-jest': {
9 | tsConfig: 'tsconfig.json'
10 | }
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "type-events",
3 | "version": "1.0.0-beta.2",
4 | "description": "An event dispatcher",
5 | "keywords": [
6 | "typescript",
7 | "ts",
8 | "javascript",
9 | "js",
10 | "event",
11 | "dispatcher",
12 | "ddd"
13 | ],
14 | "author": "Jordan Stout ",
15 | "license": "MIT",
16 | "files": [
17 | "lib/**/*"
18 | ],
19 | "main": "lib/index.js",
20 | "typings": "lib/index.d.ts",
21 | "scripts": {
22 | "build": "tsc -b . --force",
23 | "clean": "rimraf lib",
24 | "watch": "tsc -w",
25 | "prepublishOnly": "yarn clean && yarn build",
26 | "release:next": "npm publish --access public --tag next",
27 | "release": "release-it",
28 | "test": "jest --runInBand --verbose --coverage",
29 | "test:ci": "jest --verbose --coverage --ci --forceExit --detectOpenHandles --runInBand",
30 | "pretty": "prettier '{src,tests}/**/*.ts' --write"
31 | },
32 | "dependencies": {},
33 | "devDependencies": {
34 | "@sinonjs/fake-timers": "^7.0.2",
35 | "@types/jest": "^24.0.18",
36 | "@types/node": "^12.12.17",
37 | "@types/sinonjs__fake-timers": "^6.0.2",
38 | "husky": "^3.0.5",
39 | "jest": "^24.9.0",
40 | "prettier": "^1.18.2",
41 | "pretty-quick": "^1.11.1",
42 | "reflect-metadata": "^0.1.13",
43 | "release-it": "^14.2.2",
44 | "ts-jest": "^24.0.2",
45 | "typescript": "^3.6.2"
46 | },
47 | "peerDependencies": {
48 | "reflect-metadata": "^0.1.13"
49 | },
50 | "prettier": {
51 | "singleQuote": true
52 | },
53 | "husky": {
54 | "hooks": {
55 | "pre-commit": "pretty-quick --staged"
56 | }
57 | },
58 | "release-it": {
59 | "commitMessage": "chore: release v${version}",
60 | "github": {
61 | "release": true
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/EventDispatcher.ts:
--------------------------------------------------------------------------------
1 | import { Newable, ContainerLike, Handler, Loggable } from './interfaces';
2 | import { EventDispatcherMetadata, EventMetadata } from './metadata';
3 | import { Container } from './utils/Container';
4 | import { Logger } from './utils/Logger';
5 | import { EventDispatcherError } from './utils/errors';
6 |
7 | export interface EventDispatcherOptions {
8 | subscribers: Newable[];
9 | container?: ContainerLike;
10 | logger?: Loggable;
11 | }
12 |
13 | export interface EventListener extends EventMetadata {
14 | handler: Handler;
15 | }
16 |
17 | interface EventListeners {
18 | normal: EventListener[];
19 | background: EventListener[];
20 | }
21 |
22 | export class EventDispatcher {
23 | /**
24 | * All of the events that were built on EventDispatcher instantiation
25 | */
26 | protected readonly registered: Map = new Map();
27 |
28 | /**
29 | * A map containing all the ordered event handlers.
30 | *
31 | * The array contains prioritized synchronous events in index 0
32 | * and background events in index 1.
33 | */
34 | protected readonly listeners: Map = new Map();
35 |
36 | /**
37 | * How event subscribers are created.
38 | */
39 | protected readonly container: ContainerLike;
40 |
41 | /**
42 | * Custom logger for instances where a subscriber errors in the background.
43 | */
44 | protected readonly logger: Loggable;
45 |
46 | constructor(options: EventDispatcherOptions) {
47 | this.container = options.container || new Container();
48 | this.logger = options.logger || new Logger();
49 | this.buildListeners(options.subscribers);
50 | }
51 |
52 | /**
53 | * Dispatches the event to all the subscribers including all subscribers
54 | * listening on it's parents as well.
55 | */
56 | async dispatch(event: T): Promise {
57 | const { normal, background } = this.getListeners(event?.constructor);
58 | await this.handleListenersSerially(event, normal);
59 | await this.handleListenersConcurrently(event, background);
60 |
61 | return event;
62 | }
63 |
64 | /**
65 | * Processes non-background event listeners serially.
66 | */
67 | protected async handleListenersSerially(
68 | event: T,
69 | listeners: EventListener[]
70 | ): Promise {
71 | for (const listener of listeners) {
72 | await listener.handler(event);
73 | }
74 | }
75 |
76 | /**
77 | * Processes background event listeners.
78 | */
79 | protected async handleListenersConcurrently(
80 | event: T,
81 | listeners: EventListener[]
82 | ): Promise {
83 | if (!listeners.length) {
84 | return;
85 | }
86 |
87 | setImmediate(() => {
88 | Promise.all(listeners.map(listener => listener.handler(event)));
89 | });
90 | }
91 |
92 | /**
93 | * Registers the event listeners for the subscribers with a
94 | * computed "handler" method.
95 | */
96 | protected buildListeners(subscribers: Newable[]): void {
97 | subscribers.forEach(EventSubscriber => {
98 | if (!EventDispatcherMetadata.subscribers.has(EventSubscriber)) {
99 | throw new Error(
100 | `"${EventSubscriber.name}" is not a valid EventSubscriber`
101 | );
102 | }
103 |
104 | EventDispatcherMetadata.subscribers
105 | .get(EventSubscriber)
106 | .forEach(eventMetadata => {
107 | const { Event } = eventMetadata;
108 |
109 | if (!this.registered.has(Event)) {
110 | this.registered.set(Event, []);
111 | }
112 |
113 | this.registered.get(Event).push({
114 | ...eventMetadata,
115 | handler: this.createHandler(eventMetadata)
116 | });
117 | });
118 | });
119 | }
120 |
121 | /**
122 | * Computes the event's "handler" method.
123 | *
124 | * It sort of optimizes the call by creating different versions of the
125 | * dispatcher based on it's metadata.
126 | */
127 | protected createHandler(eventMetadata: EventMetadata): Handler {
128 | const { EventSubscriber, method } = eventMetadata;
129 |
130 | return async (event: any): Promise => {
131 | const subscriber = await Promise.resolve(
132 | this.container.get(EventSubscriber)
133 | );
134 | if (!subscriber) {
135 | throw new EventDispatcherError(
136 | `${EventSubscriber.name} not found in container`
137 | );
138 | }
139 |
140 | try {
141 | await Promise.resolve(subscriber[method](event));
142 | } catch (err) {
143 | this.logger.error(err);
144 | }
145 | };
146 | }
147 |
148 | /**
149 | * Generate the entire listener stack which includes it's
150 | * inherited event handlers.
151 | */
152 | protected getListeners(Event: any): EventListeners {
153 | if (this.listeners.has(Event)) {
154 | return this.listeners.get(Event);
155 | }
156 |
157 | const listeners: EventListeners = {
158 | normal: [],
159 | background: []
160 | };
161 |
162 | // check event and it's parents for any other registered events
163 | let CurrentEvent = Event;
164 | while (CurrentEvent) {
165 | if (this.registered.has(CurrentEvent)) {
166 | this.registered.get(CurrentEvent).forEach(e => {
167 | listeners[e.background ? 'background' : 'normal'].push(e);
168 | });
169 | }
170 |
171 | CurrentEvent = Object.getPrototypeOf(CurrentEvent);
172 | }
173 |
174 | const sort = a => a.sort((a, b) => b.priority - a.priority);
175 | sort(listeners.normal);
176 | sort(listeners.background);
177 |
178 | this.listeners.set(Event, listeners);
179 |
180 | return listeners;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/decorators/index.ts:
--------------------------------------------------------------------------------
1 | import { EventDispatcherMetadata } from '../metadata';
2 | import { Newable } from '../interfaces';
3 |
4 | interface OnOptions {
5 | priority?: number;
6 | background?: boolean;
7 | }
8 |
9 | export function On(
10 | Event: Newable,
11 | options?: OnOptions
12 | ): PropertyDecorator;
13 | export function On(
14 | Events: Newable[],
15 | options?: OnOptions
16 | ): PropertyDecorator;
17 | export function On(
18 | EventOrEvents: Newable | Newable[],
19 | options: OnOptions = {}
20 | ): PropertyDecorator {
21 | return (target: any, method: string) => {
22 | const Events = Array.isArray(EventOrEvents)
23 | ? EventOrEvents
24 | : [EventOrEvents];
25 |
26 | Events.map(Event => {
27 | EventDispatcherMetadata.addEventMetadata({
28 | Event,
29 | EventSubscriber: target.constructor,
30 | method,
31 | priority: options.priority || 0,
32 | background: options.background || false
33 | });
34 | });
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './EventDispatcher';
2 | export * from './decorators';
3 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface Newable {
2 | new (...args: any[]): T;
3 | }
4 |
5 | export interface ContainerLike {
6 | get: (service: any) => any;
7 | }
8 | export type Handler = (...args: any[]) => Promise;
9 |
10 | export interface Loggable {
11 | log(...data: any[]);
12 | error(message?: any, ...optionalParams: any[]): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/metadata/index.ts:
--------------------------------------------------------------------------------
1 | import { Newable } from '../interfaces';
2 |
3 | export interface EventMetadata {
4 | Event: T;
5 | EventSubscriber: Newable;
6 | method: string;
7 | priority: number;
8 | background: boolean;
9 | }
10 |
11 | export class EventDispatcherMetadata {
12 | static events: Map = new Map();
13 | static subscribers: Map = new Map();
14 |
15 | static addEventMetadata(meta: EventMetadata): void {
16 | // register event and it's subscriber
17 | if (!this.events.has(meta.Event)) {
18 | this.events.set(meta.Event, []);
19 | }
20 |
21 | // ignore duplicate event methods on subscribers
22 | if (
23 | !!this.events
24 | .get(meta.Event)
25 | .find(
26 | m =>
27 | m.EventSubscriber === meta.EventSubscriber &&
28 | m.method === meta.method
29 | )
30 | ) {
31 | return;
32 | }
33 |
34 | this.events.get(meta.Event).push(meta);
35 |
36 | // register subscriber and events it's subscribed to
37 | if (!this.subscribers.has(meta.EventSubscriber)) {
38 | this.subscribers.set(meta.EventSubscriber, []);
39 | }
40 | this.subscribers.get(meta.EventSubscriber).push(meta);
41 | }
42 |
43 | static assertEventExists(Event: Newable): void {
44 | if (!this.events.has(Event)) {
45 | throw new Error(`"${Event.name}" is not a valid decorated "@Event()"`);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/utils/Container.ts:
--------------------------------------------------------------------------------
1 | import { ContainerLike, Newable } from '../interfaces';
2 |
3 | /**
4 | * The default container for creating event subscribers.
5 | *
6 | * Subscribers, by default, are cached after initialization.
7 | */
8 | export class Container implements ContainerLike {
9 | public readonly subscribers: Map = new Map();
10 |
11 | get(EventSubscriber: Newable): T {
12 | if (!this.subscribers.has(EventSubscriber)) {
13 | this.subscribers.set(EventSubscriber, new EventSubscriber());
14 | }
15 |
16 | return this.subscribers.get(EventSubscriber);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/Logger.ts:
--------------------------------------------------------------------------------
1 | import { Loggable } from '../interfaces';
2 |
3 | /**
4 | * The default logger when one isn't configured.
5 | */
6 | export class Logger implements Loggable {
7 | log(...data: any[]) {
8 | console.log(...data);
9 | }
10 |
11 | error(message?: any, ...optionalParams: any[]): void {
12 | console.error(message, ...optionalParams);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/errors.ts:
--------------------------------------------------------------------------------
1 | export class EventDispatcherError extends Error {
2 | public name: string = 'EventDispatcherError';
3 |
4 | constructor(message: string) {
5 | super(message);
6 | Error.captureStackTrace(this, this.constructor);
7 | this.name = this.constructor.name;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/isPromise.ts:
--------------------------------------------------------------------------------
1 | const isObject = (value: any) =>
2 | value !== null && (typeof value === 'object' || typeof value === 'function');
3 |
4 | export function isPromise(value: any) {
5 | return (
6 | value instanceof Promise ||
7 | (isObject(value) &&
8 | typeof value.then === 'function' &&
9 | typeof value.catch === 'function')
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/tests/functional/EventDispatcher.test.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import FakeTimers from '@sinonjs/fake-timers';
3 | import { EventDispatcher, On } from '../../src';
4 |
5 | class BaseEvent {
6 | order: string[] = [];
7 | }
8 |
9 | class ImpressionOrConversionEvent extends BaseEvent {}
10 |
11 | class ImpressionEvent extends ImpressionOrConversionEvent {}
12 |
13 | class ConversionEvent extends ImpressionOrConversionEvent {}
14 |
15 | describe('DocumentManager', () => {
16 | let dispatcher: EventDispatcher;
17 | let clock: any;
18 |
19 | let spies = {
20 | onConversionOrImpression: jest.fn(),
21 | allEventLogger: jest.fn(),
22 | onConversion: jest.fn(),
23 | afterConversion: jest.fn(),
24 | beforeConversion: jest.fn(),
25 | onImpression: jest.fn(),
26 | onImpressionAsync: jest.fn()
27 | };
28 |
29 | beforeAll(async () => {
30 | clock = FakeTimers.install();
31 |
32 | class SlackSubscriber {
33 | @On(ImpressionOrConversionEvent, { priority: -99 })
34 | async onConversionOrImpression(event: ConversionEvent | ImpressionEvent) {
35 | event.order.push('onConversionOrImpression');
36 | spies.onConversionOrImpression(event);
37 | }
38 |
39 | @On(BaseEvent, { background: true })
40 | async allEventLogger(event: BaseEvent) {
41 | event.order.push('allEventLogger');
42 | spies.allEventLogger(event);
43 | }
44 | }
45 |
46 | class ConversionSubscriber {
47 | @On(ConversionEvent, { priority: 2 })
48 | onConversion(event: ConversionEvent) {
49 | event.order.push('onConversion');
50 | spies.onConversion(event);
51 | }
52 |
53 | @On(ConversionEvent, { priority: 1 })
54 | async afterConversion(event: ConversionEvent) {
55 | return new Promise(resolve => {
56 | event.order.push('afterConversion');
57 | spies.afterConversion(event);
58 | resolve();
59 | });
60 | }
61 |
62 | @On(ConversionEvent, { priority: 3 })
63 | async beforeConversion(event: ConversionEvent) {
64 | return new Promise(resolve => {
65 | event.order.push('beforeConversion');
66 | spies.beforeConversion(event);
67 | resolve();
68 | });
69 | }
70 | }
71 |
72 | class ImpressionSubscriber {
73 | @On(ImpressionEvent)
74 | async onImpression(event: ImpressionEvent) {
75 | return new Promise(resolve => {
76 | event.order.push('onImpression');
77 | spies.onImpression(event);
78 | resolve();
79 | });
80 | }
81 |
82 | // give this event a super high priority to prove it's being executed last even though
83 | // the promise starts first
84 | @On(ImpressionEvent, { background: true, priority: 255 })
85 | async onImpressionAsync(event: ImpressionEvent) {
86 | event.order.push('onImpressionAsync');
87 | spies.onImpressionAsync(event);
88 | return new Promise(resolve => {
89 | setTimeout(() => {
90 | event.order.push('onImpressionAsync:DONE');
91 | resolve();
92 | }, 100);
93 | });
94 | }
95 | }
96 |
97 | dispatcher = new EventDispatcher({
98 | subscribers: [SlackSubscriber, ConversionSubscriber, ImpressionSubscriber]
99 | });
100 | });
101 |
102 | afterEach(() => {
103 | clock.reset();
104 | Object.values(spies).forEach(spy => spy.mockClear());
105 | });
106 |
107 | afterAll(() => clock.uninstall());
108 |
109 | test('emits conversion events', async () => {
110 | const event = new ConversionEvent();
111 | await dispatcher.dispatch(event);
112 |
113 | expect(spies.onConversion).toBeCalledTimes(1);
114 | expect(spies.onConversion).toBeCalledWith(event);
115 | expect(spies.afterConversion).toBeCalledTimes(1);
116 | expect(spies.afterConversion).toBeCalledWith(event);
117 | expect(spies.onConversionOrImpression).toBeCalledTimes(1);
118 | expect(spies.onConversionOrImpression).toBeCalledWith(event);
119 | expect(spies.allEventLogger).toBeCalledTimes(0);
120 | expect(spies.onImpression).toBeCalledTimes(0);
121 | expect(event.order).toEqual([
122 | 'beforeConversion',
123 | 'onConversion',
124 | 'afterConversion',
125 | 'onConversionOrImpression'
126 | ]);
127 |
128 | // verify background tasks complete in another event loop
129 | await clock.runAllAsync();
130 | expect(spies.allEventLogger).toBeCalledTimes(1);
131 | expect(spies.allEventLogger).toBeCalledWith(event);
132 | expect(event.order).toEqual([
133 | 'beforeConversion',
134 | 'onConversion',
135 | 'afterConversion',
136 | 'onConversionOrImpression',
137 | 'allEventLogger'
138 | ]);
139 | });
140 |
141 | test('emits impression events', async () => {
142 | const event = new ImpressionEvent();
143 | await dispatcher.dispatch(event);
144 |
145 | expect(spies.onImpression).toBeCalledTimes(1);
146 | expect(spies.onImpression).toBeCalledWith(event);
147 | expect(spies.onConversionOrImpression).toBeCalledTimes(1);
148 | expect(spies.onConversionOrImpression).toBeCalledWith(event);
149 | expect(spies.allEventLogger).toBeCalledTimes(0);
150 | expect(spies.onConversion).toBeCalledTimes(0);
151 | expect(event.order).toEqual(['onImpression', 'onConversionOrImpression']);
152 |
153 | // verify background tasks complete in another event loop
154 | await clock.runAllAsync();
155 | expect(spies.onImpressionAsync).toBeCalledTimes(1);
156 | expect(spies.allEventLogger).toBeCalledTimes(1);
157 | expect(spies.allEventLogger).toBeCalledWith(event);
158 | expect(event.order).toEqual([
159 | 'onImpression',
160 | 'onConversionOrImpression',
161 | 'onImpressionAsync',
162 | 'allEventLogger',
163 | 'onImpressionAsync:DONE'
164 | ]);
165 | });
166 |
167 | test('with custom container', async () => {
168 | class CustomContainerSubscriber {
169 | @On(ConversionEvent)
170 | async onConversion(event: ConversionEvent) {
171 | spies.onConversion(event);
172 | }
173 | }
174 |
175 | const getFn = jest.fn();
176 |
177 | const dispatcherWithContainer = new EventDispatcher({
178 | subscribers: [CustomContainerSubscriber],
179 | container: {
180 | get(Service) {
181 | getFn();
182 |
183 | return new Service();
184 | }
185 | }
186 | });
187 |
188 | const event = new ConversionEvent();
189 | await dispatcherWithContainer.dispatch(event);
190 |
191 | expect(getFn).toBeCalledTimes(1);
192 | });
193 |
194 | test('with custom async container', async () => {
195 | class CustomContainerSubscriber {
196 | @On(ConversionEvent)
197 | async onConversion(event: ConversionEvent) {
198 | spies.onConversion(event);
199 | }
200 | }
201 |
202 | const getFn = jest.fn();
203 |
204 | const dispatcherWithContainer = new EventDispatcher({
205 | subscribers: [CustomContainerSubscriber],
206 | container: {
207 | async get(Service) {
208 | return new Promise(resolve => {
209 | getFn();
210 |
211 | resolve(new Service());
212 | });
213 | }
214 | }
215 | });
216 |
217 | const event = new ConversionEvent();
218 | await dispatcherWithContainer.dispatch(event);
219 |
220 | expect(getFn).toBeCalledTimes(1);
221 | });
222 |
223 | test('ignores events that do not have any subscribers', async () => {
224 | class Foo {}
225 | await dispatcher.dispatch(new Foo());
226 | Object.values(spies).forEach(spy => expect(spy).toBeCalledTimes(0));
227 | });
228 |
229 | test('throws errors on invalid subscriber', async () => {
230 | class InvalidSubscriber {}
231 |
232 | let e: Error;
233 |
234 | try {
235 | new EventDispatcher({ subscribers: [InvalidSubscriber] });
236 | } catch (err) {
237 | e = err;
238 | }
239 |
240 | expect(e).toBeInstanceOf(Error);
241 | });
242 | });
243 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationMap": true,
5 | "sourceMap": true,
6 | "moduleResolution": "node",
7 | "target": "es2016",
8 | "module": "commonjs",
9 | "lib": ["es6", "esnext"],
10 | "skipLibCheck": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "esModuleInterop": true,
14 | "experimentalDecorators": true,
15 | "emitDecoratorMetadata": true,
16 | "resolveJsonModule": true,
17 | "baseUrl": ".",
18 | "outDir": "./lib",
19 | "rootDir": "./src"
20 | },
21 | "include": ["src/**/*"]
22 | }
23 |
--------------------------------------------------------------------------------