Skip to content

Commit

Permalink
feat: complete refactoring
Browse files Browse the repository at this point in the history
cleaned up code-base and added support for event inheritance
  • Loading branch information
j committed Jan 27, 2021
1 parent 5fd4209 commit 2f34953
Show file tree
Hide file tree
Showing 9 changed files with 382 additions and 181 deletions.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
`type-events` allows you to create simple ways dispatch and subscribe to events.

```typescript
import { EventDispatcher, EventSubscriber, On } from 'type-events';
import { EventDispatcher, On } from 'type-events';

class Conversion {
constructor(public userAgent: string, public revenue: number) {}
Expand All @@ -24,7 +24,6 @@ class Impression {
constructor(public userAgent: string) {}
}

@EventSubscriber()
export class TrackingSubscriber {
@On(Conversion)
async onConversion(event: Conversion): Promise<void> {
Expand All @@ -39,7 +38,6 @@ export class TrackingSubscriber {
}
}

@EventSubscriber()
export class NotifySlack {
// `background: true` makes this subscriber run after all other
// subscribers and doesn't wait for the result to finish
Expand All @@ -64,7 +62,7 @@ const dispatcher = new EventDispatcher({
dispatcher.dispatch(new Conversion('Chrome', 13.37));
```

### Advanced
### Custom Container (DI)

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.
Just pass in an appropriate DI container with a valid `get` method.
Expand All @@ -80,3 +78,30 @@ const dispatcher = new EventDispatcher({
container
});
```

## Inheritance

Events can extend base classes and subscribers can subscribe to those base classes.

```typescript
import { EventDispatcher, On } from 'type-events';

abstract class BaseEvent {
// ...
}

class UserCreatedEvent extends BaseEvent {
// ...
}

class LoggingSubscriber {
@On(BaseEvent)
async all(event: BaseEvent): Promise<void> {
console.log(event);
}
}

// ...

dispatcher.dispatch(new UserCreatedEvent());
```
211 changes: 142 additions & 69 deletions src/EventDispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,171 @@
import { Newable, ContainerLike } from './interfaces';
import { EventSubscriberMetadataBuilder } from './metadata';
import { Newable, ContainerLike, DispatchEvent, Loggable } from './interfaces';
import { EventDispatcherMetadata, EventMetadata } from './metadata';
import { isPromise } from './utils/isPromise';
import { Container } from './utils/Container';
import { Logger } from './utils/Logger';
import { EventDispatcherError } from './utils/errors';

export type Handler = <T>(event: T) => Promise<void>;

export interface HandlerConfig {
EventSubscriber: Newable;
method: string;
priority?: number;
background?: boolean;
}

export interface Loggable {
log(...data: any[]);
}

export interface EventDispatcherConfig {
export interface EventDispatcherOptions {
subscribers: Newable[];
container?: ContainerLike;
logger?: Loggable;
}

const defaultContainer = {
get: <T>(EventSubscriber: new (...args: any[]) => T) => new EventSubscriber()
};
export interface DispatchableEvent extends EventMetadata {
dispatch: DispatchEvent;
}

export class EventDispatcher {
protected container: ContainerLike;
protected handlers: Map<Newable, HandlerConfig[]> = new Map();
protected logger: Loggable;

constructor(config: EventDispatcherConfig) {
this.container = config.container || defaultContainer;
this.logger = config.logger || { log: console.log };

EventSubscriberMetadataBuilder.build({ dispatcher: this, ...config });
/**
* All of the events that were built on EventDispatcher instantiation
*/
protected readonly events: Map<Newable, DispatchableEvent[]> = new Map();

/**
* Subscribers that were generated upon an event dispatch that contain
* all of the subscriber handlers in proper order.
*/
protected readonly subscribers: Map<Newable, DispatchableEvent[]> = new Map();

/**
* How event subscribers are created.
*/
protected readonly container: ContainerLike;

/**
* Custom logger for instances where a subscriber errors in the background.
*/
protected readonly logger: Loggable;

constructor(options: EventDispatcherOptions) {
this.container = options.container || new Container();
this.logger = options.logger || new Logger();
this.build(options);
}

async dispatch<T>(event: T): Promise<void> {
const Newable = (event as any).constructor;
/**
* Dispatches the event to all the subscribers.
*/
async dispatch<T>(event: T): Promise<T> {
const events = this.getEventSubscribers((event as any).constructor);

if (!this.handlers.has(Newable)) {
return;
for (const e of events) {
const result = e.dispatch(event);
if (isPromise(result)) {
await result;
}
}

const deferred: Array<() => Promise<void>> = [];
return event;
}

for (let config of this.handlers.get(Newable)) {
const serviceOrPromise = this.container.get(config.EventSubscriber);
/**
* Builds the events for the given subscribers and pre-computes
* it's dispatch function.
*/
private build(options: EventDispatcherOptions): void {
options.subscribers.forEach(EventSubscriber => {
if (!EventDispatcherMetadata.subscribers.has(EventSubscriber)) {
throw new Error(
`"${EventSubscriber.name}" is not a valid EventSubscriber`
);
}

if (serviceOrPromise) {
const service = isPromise(serviceOrPromise)
? await serviceOrPromise
: serviceOrPromise;
EventDispatcherMetadata.subscribers
.get(EventSubscriber)
.forEach(eventMetadata => {
const { Event } = eventMetadata;

if (!this.events.has(Event)) {
this.events.set(Event, []);
}

this.events.get(Event).push({
...eventMetadata,
dispatch: this.createDispatchFunction(eventMetadata)
});
});
});
}

if (config.background) {
deferred.push(() => service[config.method](event));
} else {
await service[config.method](event);
}
}
/**
* Creates a tree of all the subscribed events for the given Event and
* it's inherited parents.
*
* Returns the cached version if it was already computed.
*/
protected getEventSubscribers(Event: Newable): DispatchableEvent[] {
if (this.subscribers.has(Event)) {
return this.subscribers.get(Event);
}

Promise.all(deferred.map(fn => fn())).catch(err => this.logger.log(err));
}
const events: DispatchableEvent[] = [];

addSubscriber<T>(Newable: Newable<T>, subscriber: HandlerConfig): void {
const subscribers = this.getEventSubscribers(Newable);
// check event and it's parents for any other registered events
let CurrentEvent = Event;
while (CurrentEvent) {
if (this.events.has(CurrentEvent)) {
this.events.get(CurrentEvent).forEach(e => events.push(e));
}

subscriber.priority =
typeof subscriber.priority !== 'undefined' ? subscriber.priority : 0;
subscriber.background =
typeof subscriber.background !== 'undefined'
? subscriber.background
: false;
CurrentEvent = Object.getPrototypeOf(CurrentEvent);
}

subscribers.push(subscriber);
this.sortSubscribers(subscribers);
events.sort((a, b) => b.priority - a.priority);

this.subscribers.set(Event, events);

return events;
}

private getEventSubscribers(Newable: Newable): HandlerConfig[] {
let handlers: HandlerConfig[];
/**
* Computes the event's "dispatch" method.
*
* It sort of optimizes the call by creating different versions of the
* dispatcher based on it's metadata.
*/
protected createDispatchFunction(
eventMetadata: EventMetadata
): DispatchEvent {
const { EventSubscriber, background, method } = eventMetadata;

const dispatchEvent = (event: any): Promise<any> | any => {
// containers can potentially resolve dependencies asynchronously
const subscriber = this.container.get(EventSubscriber);
if (!subscriber) {
throw new EventDispatcherError(
`${EventSubscriber.name} not found in container`
);
}

if (!this.handlers.has(Newable)) {
handlers = [];
this.handlers.set(Newable, handlers);
} else {
handlers = this.handlers.get(Newable);
}
// resolve container & emit event if container is a promise
if (isPromise(subscriber)) {
return subscriber
.then(s => s[method](event))
.catch(err => this.logger.error(err));
}

return handlers;
}
try {
const result = subscriber[method](event);
if (isPromise(result)) {
result.catch(err => this.logger.error(err));
}
return result;
} catch (err) {
this.logger.error(err);
}
};

if (background) {
return (event: any): any => {
process.nextTick(() => {
dispatchEvent(event);
});
};
}

private sortSubscribers(subscribers: HandlerConfig[]) {
subscribers.sort((a, b) => b.priority - a.priority);
return (event: any): any => {
return dispatchEvent(event);
};
}
}
47 changes: 21 additions & 26 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
import { EventSubscriberMetadataBuilder } from '../metadata';
import { EventDispatcherMetadata } from '../metadata';
import { Newable } from '../interfaces';

export function EventSubscriber(): ClassDecorator {
return (target: any) => {
EventSubscriberMetadataBuilder.getOrCreateSubscriberMetadata(target);
};
}

interface OnConfig {
interface OnOptions {
priority?: number;
background?: boolean;
}

export function On<T>(
eventOrEvents: Newable<T> | Newable<T>[],
config: OnConfig = {}
Event: Newable<T>,
options?: OnOptions
): PropertyDecorator;
export function On<T>(
Events: Newable<T>[],
options?: OnOptions
): PropertyDecorator;
export function On<T>(
EventOrEvents: Newable<T> | Newable<T>[],
options: OnOptions = {}
): PropertyDecorator {
return (target: any, method: string) => {
const metadata = EventSubscriberMetadataBuilder.getOrCreateSubscriberMetadata(
target.constructor
);

if (!metadata.methods.has(method)) {
metadata.methods.set(method, new Map());
}

const events = Array.isArray(eventOrEvents)
? eventOrEvents
: [eventOrEvents];
const Events = Array.isArray(EventOrEvents)
? EventOrEvents
: [EventOrEvents];

events.forEach(event => {
metadata.methods.get(method).set(event, {
Events.map(Event => {
EventDispatcherMetadata.addEventMetadata({
Event,
EventSubscriber: target.constructor,
method,
event,
priority: config.priority,
background: config.background
priority: options.priority || 0,
background: options.background || false
});
});
};
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export interface Newable<T = any> {
export interface ContainerLike {
get: (service: any) => any;
}

export type DispatchEvent<T = any> = (...args: any[]) => Promise<T>;

export interface Loggable {
log(...data: any[]);
error(message?: any, ...optionalParams: any[]): void;
}
Loading

0 comments on commit 2f34953

Please sign in to comment.