retrobus
Version:
An event bus that allows listeners to be retroactive
206 lines (178 loc) • 5.06 kB
text/typescript
type Callback = (...args: any[]) => void
const CACHE_EMITTED_EVENT_LIMIT = 1000
interface Options {
/**
* Call retroactively the callback if the event
* was emitted before the listener
*/
retro?: boolean
/**
* Define the strategy when calling previous emitted
* events.
* If `retroStrategy` is set to `all`, every emitted
* events will be called, from oldest to newest.
* If `retroStrategy` is set to `last-one`, only the
* last emitted event will be retractively called.
* Default to `last-one`.
* Ignored if `retro` is `false`.
*/
retroStrategy?: 'last-one' | 'all'
/**
* Remove the callback right after being called.
* If `retro` is `true` and if the event was
* previously emitted, the callback is directly
* called then removed.
*/
once?: boolean
/**
* Make sure the callback is only added once.
*/
unique?: boolean
}
interface Params extends Options {
callback: Callback
}
const emittedEvents: Map<string | Symbol, any[][]> = new Map()
const eventListeners: Map<string | Symbol, Params[]> = new Map()
const defaultOptions: Options = {
retro: false,
retroStrategy: 'last-one',
once: false,
unique: false
}
/**
* Add a listener to a specific event.
* @param name name of the event
* @param callback the method who will be called when the event is emitted.
* @param options option parameters to change callback behavior
*/
export const addEventBusListener = (
name: string | Symbol,
callback: Callback,
options: Options = defaultOptions
): (() => void) => {
const unsubscribe = () => removeEventBusListener(name, callback)
const listeners = eventListeners.get(name)
if (options.retro && emittedEvents.has(name)) {
const emittedEventArgs = emittedEvents.get(name)!
switch (options.retroStrategy) {
case 'all': {
for (const args of emittedEventArgs) {
callback(...args)
}
break
}
case 'last-one':
default: {
const args = emittedEventArgs[emittedEventArgs.length - 1]
callback(...args)
break
}
}
if (options.once) {
return unsubscribe
}
}
const listener = { callback, ...options }
if (!listeners) {
eventListeners.set(name, [listener])
return unsubscribe
}
if (options.unique && listeners.find((c) => c.callback === callback)) {
return unsubscribe
}
eventListeners.set(name, listeners.concat([listener]))
return unsubscribe
}
/**
* Remove a callback to be called when event is emitted.
* @param name name of the event.
* @param callback callback you don't want anymore to trigger when event is emitted.
*/
export const removeEventBusListener = (
name: string | Symbol,
callback: Callback
) => {
const calls = eventListeners.get(name)
if (!calls) {
return
}
eventListeners.set(
name,
calls.filter((call) => call.callback !== callback)
)
}
/**
* Clear all listeners from an event.
* @param name event name to clear all its listeners.
*/
export const clearEventBusListeners = (name?: string | Symbol) => {
if (name === undefined) {
eventListeners.clear()
return
}
eventListeners.delete(name)
}
/**
* Limit the array of emitted events we want to cache
* @param emittedEventArgs emitted event arguments we want to limit
* @returns array of limited emitted event arguments
*/
const getLimitedHistoryOfEmittedEventArgs = <T>(
emittedEventArgs: T[][]
): T[][] => {
if (emittedEventArgs.length > CACHE_EMITTED_EVENT_LIMIT) {
return [...emittedEventArgs.slice(-CACHE_EMITTED_EVENT_LIMIT)]
}
return emittedEventArgs
}
/**
* Emit an event.
* @param name name of the event to emit.
* @param args arguments to be passed to all listeners.
*/
export const emit = <T extends any>(name: string | Symbol, ...args: T[]) => {
const listeners = eventListeners.get(name)
if (emittedEvents.has(name)) {
const emittedEventArgs = emittedEvents.get(name)!
emittedEvents.set(
name,
getLimitedHistoryOfEmittedEventArgs(emittedEventArgs.concat([args]))
)
} else {
emittedEvents.set(name, [args])
}
if (!listeners) {
return
}
listeners.map((call) => call.callback(...args))
eventListeners.set(
name,
listeners.filter((call) => !call.once)
)
}
/**
* Clear all emitted events.
* @param name event name.
*/
export const clearEmittedEvents = (name?: string | Symbol) => {
if (name === undefined) {
emittedEvents.clear()
return
}
emittedEvents.delete(name)
}
/**
* Create an event bus to type listeners' payload
* as the same as emit method's payload.
* @param event event name
*/
export const createEventBus = <T>(event: string | Symbol = Symbol()) => {
return {
emit: (payload: T) => emit<T>(event, payload),
clearEmittedEvents: () => clearEmittedEvents(event),
addEventBusListener: (callback: (payload: T) => void, options?: Options) =>
addEventBusListener(event, callback, options),
clearEventBusListeners: () => clearEventBusListeners(event)
}
}