UNPKG

bitmovin-player-ui

Version:
271 lines (230 loc) 8.46 kB
import { ArrayUtils } from './utils/ArrayUtils'; import { Timeout } from './utils/Timeout'; /** * Function interface for event listeners on the {@link EventDispatcher}. */ export interface EventListener<Sender, Args> { (sender: Sender, args: Args): void; } /** * Empty type for creating {@link EventDispatcher event dispatchers} that do not carry any arguments. */ export interface NoArgs {} /** * Event args for an event that can be canceled. */ export interface CancelEventArgs extends NoArgs { /** * Gets or sets a flag whether the event should be canceled. */ cancel?: boolean; } /** * Public interface that represents an event. Can be used to subscribe to and unsubscribe from events. */ export interface Event<Sender, Args> { /** * Subscribes an event listener to this event dispatcher. * @param listener the listener to add */ subscribe(listener: EventListener<Sender, Args>): void; /** * Subscribes an event listener to this event dispatcher that is only called once. * @param listener the listener to add */ subscribeOnce(listener: EventListener<Sender, Args>): void; /** * Subscribes an event listener to this event dispatcher that will be called at a limited rate with a minimum * interval of the specified milliseconds. * @param listener the listener to add * @param rateMs the rate in milliseconds to which calling of the listeners should be limited */ subscribeRateLimited(listener: EventListener<Sender, Args>, rateMs: number): void; /** * Unsubscribes a subscribed event listener from this dispatcher. * @param listener the listener to remove * @returns {boolean} true if the listener was successfully unsubscribed, false if it isn't subscribed on this * dispatcher */ unsubscribe(listener: EventListener<Sender, Args>): boolean; } /** * Event dispatcher to subscribe and trigger events. Each event should have its own dispatcher. */ export class EventDispatcher<Sender, Args> implements Event<Sender, Args> { private listeners: EventListenerWrapper<Sender, Args>[] = []; constructor() {} /** * {@inheritDoc} */ subscribe(listener: EventListener<Sender, Args>) { this.listeners.push(new EventListenerWrapper(listener)); } /** * {@inheritDoc} */ subscribeOnce(listener: EventListener<Sender, Args>) { this.listeners.push(new EventListenerWrapper(listener, true)); } /** * {@inheritDoc} */ subscribeRateLimited(listener: EventListener<Sender, Args>, rateMs: number) { this.listeners.push(new RateLimitedEventListenerWrapper(listener, rateMs)); } /** * {@inheritDoc} */ unsubscribe(listener: EventListener<Sender, Args>): boolean { // Iterate through listeners, compare with parameter, and remove if found // NOTE: In case we ever remove all matching listeners instead of just the first, we need to reverse-iterate here for (let i = 0; i < this.listeners.length; i++) { const subscribedListener = this.listeners[i]; if (subscribedListener.listener === listener) { subscribedListener.clear(); ArrayUtils.remove(this.listeners, subscribedListener); return true; } } return false; } /** * Removes all listeners from this dispatcher. */ unsubscribeAll(): void { // In case of RateLimitedEventListenerWrapper we need to make sure that the timeout callback won't be called for (const listener of this.listeners) { listener.clear(); } this.listeners = []; } /** * Dispatches an event to all subscribed listeners. * @param sender the source of the event * @param args the arguments for the event */ dispatch(sender: Sender, args: Args = null) { const listenersToRemove = []; // Call every listener // We iterate over a copy of the array of listeners to avoid the case where events are not fired on listeners when // listeners are unsubscribed from within the event handlers during a dispatch (because the indices change and // listeners are shifted within the array). // This means that listener x+1 will still be called if unsubscribed from within the handler of listener x, as well // as listener y+1 will not be called when subscribed from within the handler of listener y. // Array.slice(0) is the fastest array copy method according to: https://stackoverflow.com/a/21514254/370252 const listeners = this.listeners.slice(0); for (const listener of listeners) { listener.fire(sender, args); if (listener.isOnce()) { listenersToRemove.push(listener); } } // Remove one-time listener for (const listenerToRemove of listenersToRemove) { ArrayUtils.remove(this.listeners, listenerToRemove); } } /** * Returns the event that this dispatcher manages and on which listeners can subscribe and unsubscribe event handlers. * @returns {Event} */ getEvent(): Event<Sender, Args> { // For now, just cast the event dispatcher to the event interface. At some point in the future when the // codebase grows, it might make sense to split the dispatcher into separate dispatcher and event classes. return <Event<Sender, Args>>this; } } /** * A basic event listener wrapper to manage listeners within the {@link EventDispatcher}. This is a 'private' class * for internal dispatcher use and it is therefore not exported. */ class EventListenerWrapper<Sender, Args> { private eventListener: EventListener<Sender, Args>; private once: boolean; constructor(listener: EventListener<Sender, Args>, once: boolean = false) { this.eventListener = listener; this.once = once; } /** * Returns the wrapped event listener. * @returns {EventListener<Sender, Args>} */ get listener(): EventListener<Sender, Args> { return this.eventListener; } /** * Fires the wrapped event listener with the given arguments. * @param sender * @param args */ fire(sender: Sender, args: Args) { this.eventListener(sender, args); } /** * Checks if this listener is scheduled to be called only once. * @returns {boolean} once if true */ isOnce(): boolean { return this.once; } clear(): void {} } interface EventAttributes<Sender, Args> { sender: Sender; args: Args; } /** * Extends the basic {@link EventListenerWrapper} with rate-limiting functionality. */ class RateLimitedEventListenerWrapper<Sender, Args> extends EventListenerWrapper<Sender, Args> { private readonly rateMs: number; private readonly rateLimitingEventListener: EventListener<Sender, Args>; // save last seen event attributes private lastSeenEvent: EventAttributes<Sender, Args>; private rateLimitTimout: Timeout; constructor(listener: EventListener<Sender, Args>, rateMs: number) { super(listener); // sets the event listener sink this.rateMs = rateMs; // starting limiting the events to the given value const startRateLimiting = () => { this.rateLimitTimout.start(); }; // timout for limiting the events this.rateLimitTimout = new Timeout(this.rateMs, () => { if (this.lastSeenEvent) { this.fireSuper(this.lastSeenEvent.sender, this.lastSeenEvent.args); startRateLimiting(); // start rateLimiting again to keep rate limit active even after firing the last seen event this.lastSeenEvent = null; } }); // In case the events stopping during the rateLimiting we need to track the last seen one and delegate after the // rate limiting is finished. This prevents missing the last update due to the rate limit. this.rateLimitingEventListener = (sender: Sender, args: Args) => { // only fire events if the rateLimiting is not running if (this.shouldFireEvent()) { this.fireSuper(sender, args); startRateLimiting(); return; } this.lastSeenEvent = { sender: sender, args: args, }; }; } private shouldFireEvent(): boolean { return !this.rateLimitTimout.isActive(); } private fireSuper(sender: Sender, args: Args) { // Fire the actual external event listener super.fire(sender, args); } fire(sender: Sender, args: Args) { // Fire the internal rate-limiting listener instead of the external event listener this.rateLimitingEventListener(sender, args); } clear(): void { super.clear(); this.rateLimitTimout.clear(); } }