publisher-subscriber-pattern
Version:
Publisher subscriber pattern that can be used with different event emitters including browser window
123 lines (99 loc) • 3.66 kB
text/typescript
import {
EventCallback,
EventData,
EventName,
PublisherProps,
SubscriberInstance,
SubscriptionFunctions,
UnsubscribeFunction,
isValidEmitter
} from './_types';
import { Subscriber } from './subscriber';
export class Publisher {
private eventData: Map< EventName, EventData>;
private addEventListener: SubscriptionFunctions;
private removeEventListener: SubscriptionFunctions;
constructor (...args: PublisherProps) {
const [ emitterInstance, addListenerMethodName, removeListenerMethodName ] = args;
if (!isValidEmitter(emitterInstance, addListenerMethodName, removeListenerMethodName)) {
throw new Error('Publisher received incorrect arguments');
}
this.eventData = new Map([]);
this.addEventListener =
// @ts-ignore
( emitterInstance[ addListenerMethodName ] as SubscriptionFunctions).bind(emitterInstance);
this.removeEventListener =
// @ts-ignore
( emitterInstance[ removeListenerMethodName ] as SubscriptionFunctions).bind(emitterInstance);
}
public subscribe = (
eventName: EventName,
eventCallback: EventCallback,
subscriberInstance?: SubscriberInstance
): () => void => {
const subscriber = new Subscriber(eventCallback, subscriberInstance);
let eventData = this.getEventData(eventName);
if (!eventData) {
eventData = this.eventData
.set( eventName, [ this.buildInformSubscribers(eventName), [] ])
.get(eventName) as EventData;
this.observeEvent(eventName, eventData);
}
eventData[ 1 ].push(subscriber);
return this.unsubscribe(eventName, subscriber);
}
public unsubscribeAll = (): void => {
Array.from(this.eventData.keys()).forEach(eventName => {
this.unobserveEvent(eventName);
});
}
public eventSubscribersCount = (eventName: EventName): number => {
return (this.getSubscribers(eventName) || []).length;
}
public subscribersCount = (): number => {
return Array.from(this.eventData.values()).reduce((count = 0, eventData) => {
count += eventData[ 1 ].length;
return count;
}, 0);
}
private getEventData (eventName: EventName): EventData | undefined {
return this.eventData.get(eventName);
}
private getEventCallback (eventName: EventName): EventCallback | undefined {
return (this.getEventData(eventName) || [])[ 0 ];
}
private getSubscribers (eventName: EventName): Subscriber[] | undefined {
return (this.getEventData(eventName) || [])[ 1 ];
}
private buildInformSubscribers = (eventName: EventName): EventCallback => (
(event: Event): void => {
(this.getSubscribers(eventName) || [])
.forEach(subscriber => subscriber.eventCallback(event));
}
)
private unsubscribe = (eventName: EventName, subscriber: Subscriber): UnsubscribeFunction => {
return (): void => {
const subscribersArray = (this.getEventData(eventName) || [])[ 1 ];
if (subscribersArray) {
subscribersArray
.splice(subscribersArray.findIndex(item => item === subscriber), 1);
if (!subscribersArray.length) {
this.unobserveEvent(eventName);
}
}
return;
};
}
private observeEvent = (eventName: EventName, eventData?: EventData): void => {
const eventCallback = eventData
? eventData[ 0 ]
: this.getEventCallback(eventName);
if (eventCallback) {
this.addEventListener(eventName, eventCallback);
}
}
private unobserveEvent = (eventName: EventName): void => {
this.removeEventListener(eventName, this.getEventCallback(eventName) as EventCallback);
this.eventData.delete(eventName);
}
}