180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
/**
|
|
* @fileoverview
|
|
* A bunch of shared utils that can be used in ComfyUI, as well as in any single-HTML pages.
|
|
*/
|
|
|
|
export type Resolver<T> = {
|
|
id: string;
|
|
completed: boolean;
|
|
resolved: boolean;
|
|
rejected: boolean;
|
|
promise: Promise<T>;
|
|
resolve: (data: T) => void;
|
|
reject: (e?: Error) => void;
|
|
timeout: number | null;
|
|
deferment?: {data?: any, timeout?: number|null, signal?: string};
|
|
};
|
|
|
|
/**
|
|
* Returns a new `Resolver` type that allows creating a "disconnected" `Promise` that can be
|
|
* returned and resolved separately.
|
|
*/
|
|
export function getResolver<T>(timeout: number = 5000): Resolver<T> {
|
|
const resolver: Partial<Resolver<T>> = {};
|
|
resolver.id = generateId(8);
|
|
resolver.completed = false;
|
|
resolver.resolved = false;
|
|
resolver.rejected = false;
|
|
resolver.promise = new Promise((resolve, reject) => {
|
|
resolver.reject = (e?: Error) => {
|
|
resolver.completed = true;
|
|
resolver.rejected = true;
|
|
reject(e);
|
|
};
|
|
resolver.resolve = (data: T) => {
|
|
resolver.completed = true;
|
|
resolver.resolved = true;
|
|
resolve(data);
|
|
};
|
|
});
|
|
resolver.timeout = setTimeout(() => {
|
|
if (!resolver.completed) {
|
|
resolver.reject!();
|
|
}
|
|
}, timeout);
|
|
return resolver as Resolver<T>;
|
|
}
|
|
|
|
/** The WeakMap for debounced functions. */
|
|
const DEBOUNCE_FN_TO_PROMISE: WeakMap<Function, Promise<void>> = new WeakMap();
|
|
|
|
/**
|
|
* Debounces a function call so it is only called once in the initially provided ms even if asked
|
|
* to be called multiple times within that period.
|
|
*/
|
|
export function debounce(fn: Function, ms = 64) {
|
|
if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) {
|
|
DEBOUNCE_FN_TO_PROMISE.set(
|
|
fn,
|
|
wait(ms).then(() => {
|
|
DEBOUNCE_FN_TO_PROMISE.delete(fn);
|
|
fn();
|
|
}),
|
|
);
|
|
}
|
|
return DEBOUNCE_FN_TO_PROMISE.get(fn);
|
|
}
|
|
|
|
/** Waits a certain number of ms, as a `Promise.` */
|
|
export function wait(ms = 16): Promise<void> {
|
|
// Special logic, if we're waiting 16ms, then trigger on next frame.
|
|
if (ms === 16) {
|
|
return new Promise((resolve) => {
|
|
requestAnimationFrame(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, ms);
|
|
});
|
|
}
|
|
|
|
function dec2hex(dec: number) {
|
|
return dec.toString(16).padStart(2, "0");
|
|
}
|
|
|
|
/** Generates an unique id of a specific length. */
|
|
export function generateId(length: number) {
|
|
const arr = new Uint8Array(length / 2);
|
|
crypto.getRandomValues(arr);
|
|
return Array.from(arr, dec2hex).join("");
|
|
}
|
|
|
|
/**
|
|
* Returns the deep value of an object given a dot-delimited key.
|
|
*/
|
|
export function getObjectValue(obj: any, objKey: string, def?: any) {
|
|
if (!obj || !objKey) return def;
|
|
|
|
const keys = objKey.split(".");
|
|
const key = keys.shift()!;
|
|
const found = obj[key];
|
|
if (keys.length) {
|
|
return getObjectValue(found, keys.join("."), def);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Sets the deep value of an object given a dot-delimited key.
|
|
*
|
|
* By default, missing objects will be created while settng the path. If `createMissingObjects` is
|
|
* set to false, then the setting will be abandoned if the key path is missing an intermediate
|
|
* value. For example:
|
|
*
|
|
* setObjectValue({a: {z: false}}, 'a.b.c', true); // {a: {z: false, b: {c: true } } }
|
|
* setObjectValue({a: {z: false}}, 'a.b.c', true, false); // {a: {z: false}}
|
|
*
|
|
*/
|
|
export function setObjectValue(obj: any, objKey: string, value: any, createMissingObjects = true) {
|
|
if (!obj || !objKey) return obj;
|
|
|
|
const keys = objKey.split(".");
|
|
const key = keys.shift()!;
|
|
if (obj[key] === undefined) {
|
|
if (!createMissingObjects) {
|
|
return;
|
|
}
|
|
obj[key] = {};
|
|
}
|
|
if (!keys.length) {
|
|
obj[key] = value;
|
|
} else {
|
|
if (typeof obj[key] != "object") {
|
|
obj[key] = {};
|
|
}
|
|
setObjectValue(obj[key], keys.join("."), value, createMissingObjects);
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Moves an item in an array (by item or its index) to another index.
|
|
*/
|
|
export function moveArrayItem<T>(arr: T[], itemOrFrom: T | number, to: number) {
|
|
const from = typeof itemOrFrom === "number" ? itemOrFrom : arr.indexOf(itemOrFrom);
|
|
arr.splice(to, 0, arr.splice(from, 1)[0]!);
|
|
}
|
|
|
|
/**
|
|
* Moves an item in an array (by item or its index) to another index.
|
|
*/
|
|
export function removeArrayItem<T>(arr: T[], itemOrIndex: T | number) {
|
|
const index = typeof itemOrIndex === "number" ? itemOrIndex : arr.indexOf(itemOrIndex);
|
|
arr.splice(index, 1);
|
|
}
|
|
|
|
/**
|
|
* Injects CSS into the page with a promise when complete.
|
|
*/
|
|
export function injectCss(href: string): Promise<void> {
|
|
if (document.querySelector(`link[href^="${href}"]`)) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise((resolve) => {
|
|
const link = document.createElement("link");
|
|
link.setAttribute("rel", "stylesheet");
|
|
link.setAttribute("type", "text/css");
|
|
const timeout = setTimeout(resolve, 1000);
|
|
link.addEventListener("load", (e) => {
|
|
clearInterval(timeout);
|
|
resolve();
|
|
});
|
|
link.href = href;
|
|
document.head.appendChild(link);
|
|
});
|
|
}
|