@etsoo/shared
Version:
TypeScript shared utilities and functions
222 lines (199 loc) • 5.76 kB
text/typescript
import { DelayedExecutorType } from "./types/DelayedExecutorType";
const hasRequestAnimationFrame = typeof requestAnimationFrame === "function";
/**
* Extend utilities
*/
export namespace ExtendUtils {
/**
* Apply mixins, official suggested method
* https://www.typescriptlang.org/docs/handbook/mixins.html#understanding-the-sample
* @param derivedCtor Mixin target class
* @param baseCtors Mixin base classes
* @param override Override or not
*/
export function applyMixins(
derivedCtor: any,
baseCtors: any[],
override = false
) {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
if (
name !== "constructor" &&
(override || !derivedCtor.prototype[name])
) {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
}
});
});
}
/**
* Create delayed executor
* @param func Function
* @param delayMiliseconds Delay miliseconds
* @returns Result
*/
export function delayedExecutor<P extends any[]>(
func: (...args: P) => void,
delayMiliseconds: number
): DelayedExecutorType<P> {
let cancel: (() => void) | undefined;
return {
/**
* Call the function
* @param miliseconds Delayed miliseconds for this call
* @param args Args
*/
call(miliseconds?: number, ...args: P) {
this.clear();
cancel = waitFor(() => {
func(...args);
cancel = undefined;
}, miliseconds ?? delayMiliseconds);
},
/**
* Clear
*/
clear() {
if (this.isRunning()) {
if (cancel) cancel();
cancel = undefined;
}
},
/**
* Is running or not
* @returns Result
*/
isRunning() {
return cancel != null;
}
};
}
/**
* Promise handler to catch error
* @param promise Promise
*/
export const promiseHandler = (promise: Promise<unknown>) =>
promise
.then((value) => [value, undefined])
.catch((reason) => Promise.resolve([undefined, reason]));
/**
* Delay promise
* @param delay Delay miniseconds
*/
export function sleep(delay = 0) {
return new Promise<void>((resolve) => {
waitFor(resolve, delay);
});
}
/**
* Wait for condition meets and execute callback
* requestAnimationFrame to replace setTimeout
* @param callback Callback
* @param checkReady Check ready, when it's a number as miliseconds, similar to setTimeout
* @returns cancel callback
*/
export function waitFor(
callback: () => void,
checkReady: ((spanTime: number) => boolean) | number
) {
let requestID: number | undefined | NodeJS.Timeout;
if (hasRequestAnimationFrame) {
let lastTime: number | undefined;
function loop(timestamp: number) {
// Cancelled
if (requestID == null) return;
if (lastTime === undefined) {
lastTime = timestamp;
}
const elapsed = timestamp - lastTime;
const isReady =
typeof checkReady === "number"
? elapsed >= checkReady
: checkReady(elapsed);
if (isReady) {
callback();
} else if (requestID != null) {
// May also be cancelled in callback or somewhere
requestID = requestAnimationFrame(loop);
}
}
requestID = requestAnimationFrame(loop);
} else if (typeof checkReady === "number") {
requestID = setTimeout(callback, checkReady);
} else {
// Bad practice to use setTimeout in this way, only for compatibility
const ms = 20;
let spanTime = 0;
let cr = checkReady;
function loop() {
// Cancelled
if (requestID == null) return;
spanTime += ms;
if (cr(spanTime)) {
callback();
} else if (requestID != null) {
// May also be cancelled in callback or somewhere
requestID = setTimeout(loop, ms);
}
}
requestID = setTimeout(loop, ms);
}
return () => {
if (requestID) {
if (hasRequestAnimationFrame && typeof requestID === "number") {
cancelAnimationFrame(requestID);
} else {
clearTimeout(requestID);
}
requestID = undefined;
}
};
}
/**
* Repeat interval for callback
* @param callback Callback
* @param miliseconds Miliseconds
* @returns cancel callback
*/
export function intervalFor(callback: () => void, miliseconds: number) {
let requestID: number | undefined | NodeJS.Timeout;
if (hasRequestAnimationFrame) {
let lastTime: number | undefined;
function loop(timestamp: number) {
// Cancelled
if (requestID == null) return;
if (lastTime === undefined) {
lastTime = timestamp;
}
const elapsed = timestamp - lastTime;
if (elapsed >= miliseconds) {
lastTime = timestamp;
callback();
}
if (requestID != null) {
// May also be cancelled in callback or somewhere
requestID = requestAnimationFrame(loop);
}
}
requestID = requestAnimationFrame(loop);
} else {
requestID = setInterval(callback, miliseconds);
}
return () => {
if (requestID) {
if (hasRequestAnimationFrame && typeof requestID === "number") {
cancelAnimationFrame(requestID);
} else {
clearInterval(requestID);
}
requestID = undefined;
}
};
}
}