@cleanweb/utils
Version:
Simple, tiny, straight-forward utils for everyday Typescript needs.
151 lines (125 loc) • 4.1 kB
text/typescript
/**
* 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;
}
}