-
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.
cleaned up code-base and added support for event inheritance
- Loading branch information
Showing
9 changed files
with
382 additions
and
181 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
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,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); | ||
}; | ||
} | ||
} |
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
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
Oops, something went wrong.