@igorskyflyer/zep
Version:
🧠 Zep is a zero-dependency, efficient debounce module. ⏰
226 lines (225 loc) • 6.79 kB
JavaScript
// Author: Igor Dimitrijević (@igorskyflyer)
export class Zep {
#timer;
#shouldCancel;
#shouldAbort;
#calls;
#isRunning;
#time;
#executionCount;
#isWaiting;
#wasCancelled;
#wasAborted;
#callback;
#onCancelled;
#onAborted;
#onBeforeRun;
#onAfterRun;
#onCompleted;
#onError;
/**
* Creates a new instance of Zep.
* @param callback The function/callback to debounce.
* @param time The time limit (in ms) for the debouncing.
*/
constructor(callback, time) {
this.#timer = 0;
this.#shouldCancel = false;
this.#shouldAbort = false;
this.#calls = 0;
this.#isRunning = false;
this.#callback = callback;
this.#time = time;
this.#executionCount = 0;
this.#isWaiting = false;
this.#wasCancelled = false;
this.#wasAborted = false;
this.#onCancelled = undefined;
this.#onAborted = undefined;
this.#onBeforeRun = undefined;
this.#onAfterRun = undefined;
this.#onCompleted = undefined;
this.#onError = undefined;
}
#deleteTimer() {
clearInterval(this.#timer);
this.#timer = 0;
}
/**
* Returns the number of callback executions.
*/
get executionCount() {
return this.#executionCount;
}
/**
* Indicates whether Zep is waiting for a Timer to finish its execution, if true, Zep.run() won’t create new Timers when called.
*/
get isWaiting() {
return this.#isWaiting;
}
/**
* Indicates whether a Timer is currently running the `callback` provided in the constructor.
*/
get isRunning() {
return this.#isRunning;
}
/**
* Indicates whether the execution of Zep.run() was cancelled. Execution can be cancelled by calling Zep.cancel().
*/
get wasCancelled() {
return this.#wasCancelled;
}
/**
* Indicates whether the execution of Zep.run() was aborted. Execution can be aborted by calling Zep.abort().
*/
get wasAborted() {
return this.#wasAborted;
}
/**
* A handler to call when the execution of Zep.run() has been cancelled.
*/
onCancelled(handler) {
this.#onCancelled = handler;
return this;
}
/**
* A handler to call when the execution of Zep.run() has been aborted.
*/
onAborted(handler) {
this.#onAborted = handler;
return this;
}
/**
* A handler to call before Zep.run().
*/
onBeforeRun(handler) {
this.#onBeforeRun = handler;
return this;
}
/**
* A handler to call after Zep.run().
*/
onAfterRun(handler) {
this.#onAfterRun = handler;
return this;
}
/**
* A handler to call after `Zep()` has finished running, i.e. no more calls to the `Zep.run()` method have been issued in the given time-frame.
*/
onCompleted(handler) {
this.#onCompleted = handler;
return this;
}
/**
* A handler to call when an error has occurred during execution.
*/
onError(handler) {
this.#onError = handler;
return this;
}
/**
* Stops the execution but NOT the current running Timer - if applicable.
* @see abort
*/
cancel() {
this.#shouldCancel = true;
}
/**
* Aborts the execution, stops Zep completely and - if applicable - the current running Timer without waiting for it to finish its execution.
* @see cancel
*/
abort() {
this.#shouldAbort = true;
}
/**
* Writes Zep statistical information to the console.
*/
writeStats() {
let percentageSaved;
// handles undefined and 0
if (this.#executionCount && this.#calls) {
percentageSaved = (100 -
(this.#executionCount / this.#calls) * 100).toFixed(2);
}
else {
percentageSaved = '0';
}
// biome-ignore lint/suspicious/noConsole: needed for DX
console.log(`🧠 [Zep]: invocations: ${this.#calls}, callback executions: ${this.#executionCount}, saving ${percentageSaved}% of calls.`);
}
/**
* Runs the callback defined in the constructor if necessary or else debounces it.
*/
run(...args) {
if (typeof this.#callback !== 'function') {
return this;
}
if (this.#shouldAbort) {
this.#deleteTimer();
this.#isRunning = false;
this.#shouldAbort = false;
this.#wasAborted = true;
if (typeof this.#onAborted === 'function') {
this.#onAborted();
}
// don't let the execution continue!
return this;
}
this.#calls++;
this.#wasCancelled = false;
this.#wasAborted = false;
this.#isWaiting = true;
this.#isRunning = true;
if (!this.#time) {
try {
this.#callback(...args);
}
catch (e) {
if (typeof this.#onError === 'function') {
this.#onError(e);
}
}
this.#executionCount++;
this.#isWaiting = false;
this.#isRunning = false;
return this;
}
if (!this.#timer) {
this.#timer = setInterval(() => {
if (!this.#isRunning) {
this.#deleteTimer();
this.#isWaiting = false;
this.#isRunning = false;
if (!this.#wasCancelled && typeof this.#onCompleted === 'function') {
this.#onCompleted();
}
return;
}
if (this.#shouldCancel && typeof this.#onCancelled === 'function') {
this.#isRunning = false;
this.#shouldCancel = false;
this.#wasCancelled = true;
this.#onCancelled.call(this);
}
if (typeof this.#onBeforeRun === 'function') {
this.#onBeforeRun();
}
try {
this.#callback(...args);
}
catch (e) {
if (typeof this.#onError === 'function') {
this.#onError(e);
}
}
if (typeof this.#onAfterRun === 'function') {
this.#onAfterRun();
}
this.#executionCount++;
this.#isWaiting = false;
this.#isRunning = false;
}, this.#time);
}
return this;
}
}