UNPKG

@cleanweb/utils

Version:

Simple, tiny, straight-forward utils for everyday Typescript needs.

151 lines (125 loc) 4.1 kB
/** * Holds methods for sending updates to all subscribers. */ export interface IPublisher<DataType> { /** * Calls the `next` callback of all current subscribers immediately, * passing the new data to them. */ publish: (data: DataType) => void, /** * Calls the `done` callback of all current subscribers immediately, * indicating that no new data is expected to be published to this instance. */ done: VoidFunction, } /** * A callback to signal the owner when the first subscriber is added. */ type StartCallback<DataType> = ( /** An object with methods for sending updates to all subscribers. */ publisher: IPublisher<DataType>, ) => void; type ConstructorParams<DataType> = [ /** * Called when the first subscriber is added. * Can be used as a signal to know when to start * generating data. */ start: StartCallback<DataType>, /** * Called when there are no subscribers left on the instance. * Can be used as a signal to pause generation of data. * * the `start` callback will be called again if a new subscriber is added. */ stop: VoidFunction, /** * An initial value for the "lastPublished" data. * "lastPublished" is published to new subscribers immediately, even if * `publish` has not been called yet. * * Subscribers can opt out of receiving this using the `freshOnly` argument. * If so, they will only receive values published after they subscribed. */ initialData: DataType, ] type TSubscribe<DataType> = ( /** A callback to be called with the latest data each time there is an update. */ onPublish: (data: DataType) => void, /** A callback to be called when the publisher indicates that no further updates will be published. */ onComplete: VoidFunction, /** * Set to true to opt out of receiving the data that was last published at the time of subscription. * If false or omitted, your `onPublish` callback will be called with the * last published data immediately you subscribe. * If true, your callback will only receive new data that was published after you subscribed. */ freshOnly: boolean, ) => VoidFunction; interface ISubscriber<DataType> { /** Called to notify the subscriber of new data. */ next: (data: DataType) => void, /** Called to notify the subscriber that there will be no more new data published on this instance. */ onComplete: VoidFunction } export default class Subscribable<DataType> { private _start; private _pause; private _lastPublishedData: DataType | undefined; private _subscribers: Record<string, ISubscriber<DataType>> = {}; private _allTimeSubscribersCount = 0; /** @inheritdoc {@link IPublisher} */ private _publisher: IPublisher<DataType> = { publish: (data: DataType) => this._publish(data), done: () => this._close(), }; private _publish = (data: DataType) => { Object.values(this._subscribers).forEach((subscriber) => { subscriber.next(data); }); this._lastPublishedData = data; } private _close = () => { Object.values(this._subscribers).forEach((subscriber) => { subscriber.onComplete(); }); this._lastPublishedData = undefined; this._allTimeSubscribersCount = 0; } constructor (...params: ConstructorParams<DataType>) { const [ start, pause, initialData ] = params; this._start = () => { start(this._publisher); }; this._pause = () => pause(); this._lastPublishedData = initialData; } subscribe: TSubscribe<DataType> = (onPublish, onComplete, freshOnly) => { const position = this._allTimeSubscribersCount++; this._subscribers[position] = { next: (data: DataType) => { onPublish(data); }, onComplete: () => { onComplete?.(); }, }; if (Object.keys(this._subscribers).length === 1) { this._start(); }; if (!freshOnly && this._lastPublishedData !== undefined) { onPublish(this._lastPublishedData); } return () => { delete this._subscribers[position]; if (Object.keys(this._subscribers).length === 0) { this._pause(); } }; } /** Returns the data that was last published. */ get snapshot() { return this._lastPublishedData; } }