|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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};
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>;
|
|
|
}
|
|
|
|
|
|
|
|
|
const DEBOUNCE_FN_TO_PROMISE: WeakMap<Function, Promise<void>> = new WeakMap();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
|
|
|
export function check(value: any, msg = "", ...args: any[]): asserts value {
|
|
|
if (!value) {
|
|
|
console.error(msg, ...(args || []));
|
|
|
throw new Error(msg || "Error");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
export function wait(ms = 16): Promise<void> {
|
|
|
|
|
|
if (ms === 16) {
|
|
|
return new Promise((resolve) => {
|
|
|
requestAnimationFrame(() => {
|
|
|
resolve();
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
return new Promise((resolve) => {
|
|
|
setTimeout(() => {
|
|
|
resolve();
|
|
|
}, ms);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
export function deepFreeze<T extends Object>(obj: T): T {
|
|
|
|
|
|
const propNames = Reflect.ownKeys(obj);
|
|
|
|
|
|
|
|
|
for (const name of propNames) {
|
|
|
const value = (obj as any)[name];
|
|
|
if ((value && typeof value === "object") || typeof value === "function") {
|
|
|
deepFreeze(value);
|
|
|
}
|
|
|
}
|
|
|
return Object.freeze(obj);
|
|
|
}
|
|
|
|
|
|
function dec2hex(dec: number) {
|
|
|
return dec.toString(16).padStart(2, "0");
|
|
|
}
|
|
|
|
|
|
|
|
|
export function generateId(length: number) {
|
|
|
const arr = new Uint8Array(length / 2);
|
|
|
crypto.getRandomValues(arr);
|
|
|
return Array.from(arr, dec2hex).join("");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getObjectValue(obj: {[key: string]: 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;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function moveArrayItem(arr: any[], itemOrFrom: any, to: number) {
|
|
|
const from = typeof itemOrFrom === "number" ? itemOrFrom : arr.indexOf(itemOrFrom);
|
|
|
arr.splice(to, 0, arr.splice(from, 1)[0]!);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function removeArrayItem<T>(arr: T[], itemOrIndex: T | number) {
|
|
|
const index = typeof itemOrIndex === "number" ? itemOrIndex : arr.indexOf(itemOrIndex);
|
|
|
arr.splice(index, 1);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function defineProperty(instance: any, property: string, desc: PropertyDescriptor) {
|
|
|
const existingDesc = Object.getOwnPropertyDescriptor(instance, property);
|
|
|
if (existingDesc?.configurable === false) {
|
|
|
throw new Error(`Error: rgthree-comfy cannot define un-configurable property "${property}"`);
|
|
|
}
|
|
|
|
|
|
if (existingDesc?.get && desc.get) {
|
|
|
const descGet = desc.get;
|
|
|
desc.get = () => {
|
|
|
existingDesc.get!.apply(instance, []);
|
|
|
return descGet!.apply(instance, []);
|
|
|
};
|
|
|
}
|
|
|
if (existingDesc?.set && desc.set) {
|
|
|
const descSet = desc.set;
|
|
|
desc.set = (v: any) => {
|
|
|
existingDesc.set!.apply(instance, [v]);
|
|
|
return descSet!.apply(instance, [v]);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
desc.enumerable = desc.enumerable ?? existingDesc?.enumerable ?? true;
|
|
|
desc.configurable = desc.configurable ?? existingDesc?.configurable ?? true;
|
|
|
if (!desc.get && !desc.set) {
|
|
|
desc.writable = desc.writable ?? existingDesc?.writable ?? true;
|
|
|
}
|
|
|
return Object.defineProperty(instance, property, desc);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function areDataViewsEqual(a: DataView, b: DataView) {
|
|
|
if (a.byteLength !== b.byteLength) {
|
|
|
return false;
|
|
|
}
|
|
|
for (let i = 0; i < a.byteLength; i++) {
|
|
|
if (a.getUint8(i) !== b.getUint8(i)) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function looksLikeBase64(source: string) {
|
|
|
return source.length > 500 || source.startsWith("data:");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function areArrayBuffersEqual(a?: ArrayBuffer | null, b?: ArrayBuffer | null) {
|
|
|
if (a == b || !a || !b) {
|
|
|
return a == b;
|
|
|
}
|
|
|
return areDataViewsEqual(new DataView(a), new DataView(b));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getCanvasImageData(
|
|
|
image: HTMLImageElement,
|
|
|
): [HTMLCanvasElement, CanvasRenderingContext2D, ImageData] {
|
|
|
const canvas = document.createElement("canvas");
|
|
|
const ctx = canvas.getContext("2d")!;
|
|
|
canvas.width = image.width;
|
|
|
canvas.height = image.height;
|
|
|
ctx.drawImage(image, 0, 0);
|
|
|
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
|
|
return [canvas, ctx, imageData];
|
|
|
}
|
|
|
|
|
|
|
|
|
type ImageConverstionTypes = string | Blob | ArrayBuffer | HTMLImageElement | HTMLCanvasElement;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function convertToBase64(
|
|
|
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
|
|
): Promise<string> {
|
|
|
if (source instanceof Promise) {
|
|
|
source = await source;
|
|
|
}
|
|
|
if (typeof source === "string" && looksLikeBase64(source)) {
|
|
|
return source;
|
|
|
}
|
|
|
if (typeof source === "string" || source instanceof Blob || source instanceof ArrayBuffer) {
|
|
|
return convertToBase64(await loadImage(source));
|
|
|
}
|
|
|
if (source instanceof HTMLImageElement) {
|
|
|
const [canvas, ctx, imageData] = getCanvasImageData(source);
|
|
|
return convertToBase64(canvas);
|
|
|
}
|
|
|
if (source instanceof HTMLCanvasElement) {
|
|
|
return source.toDataURL("image/png");
|
|
|
}
|
|
|
throw Error("Unknown source to convert to base64.");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function convertToArrayBuffer(
|
|
|
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
|
|
): Promise<ArrayBuffer> {
|
|
|
if (source instanceof Promise) {
|
|
|
source = await source;
|
|
|
}
|
|
|
if (source instanceof ArrayBuffer) {
|
|
|
return source;
|
|
|
}
|
|
|
if (typeof source === "string") {
|
|
|
if (looksLikeBase64(source)) {
|
|
|
var binaryString = atob(source.replace(/^.*?;base64,/, ""));
|
|
|
var bytes = new Uint8Array(binaryString.length);
|
|
|
for (var i = 0; i < binaryString.length; i++) {
|
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
|
}
|
|
|
return bytes.buffer;
|
|
|
}
|
|
|
return convertToArrayBuffer(await loadImage(source));
|
|
|
}
|
|
|
if (source instanceof HTMLImageElement) {
|
|
|
const [canvas, ctx, imageData] = getCanvasImageData(source);
|
|
|
return convertToArrayBuffer(canvas);
|
|
|
}
|
|
|
if (source instanceof HTMLCanvasElement) {
|
|
|
return convertToArrayBuffer(source.toDataURL());
|
|
|
}
|
|
|
if (source instanceof Blob) {
|
|
|
return source.arrayBuffer();
|
|
|
}
|
|
|
throw Error("Unknown source to convert to arraybuffer.");
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function loadImage(
|
|
|
source: ImageConverstionTypes | Promise<ImageConverstionTypes>,
|
|
|
): Promise<HTMLImageElement> {
|
|
|
if (source instanceof Promise) {
|
|
|
source = await source;
|
|
|
}
|
|
|
if (source instanceof HTMLImageElement) {
|
|
|
return loadImage(source.src);
|
|
|
}
|
|
|
if (source instanceof Blob) {
|
|
|
return loadImage(source.arrayBuffer());
|
|
|
}
|
|
|
if (source instanceof HTMLCanvasElement) {
|
|
|
return loadImage(source.toDataURL());
|
|
|
}
|
|
|
if (source instanceof ArrayBuffer) {
|
|
|
var binary = "";
|
|
|
var bytes = new Uint8Array(source);
|
|
|
var len = bytes.byteLength;
|
|
|
for (var i = 0; i < len; i++) {
|
|
|
binary += String.fromCharCode(bytes[i]!);
|
|
|
}
|
|
|
return loadImage(`data:${getMimeTypeFromArrayBuffer(bytes)};base64,${btoa(binary)}`);
|
|
|
}
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const img = new Image();
|
|
|
img.addEventListener("load", () => {
|
|
|
resolve(img);
|
|
|
});
|
|
|
img.addEventListener("error", () => {
|
|
|
reject(img);
|
|
|
});
|
|
|
img.src = source;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getMimeTypeFromArrayBuffer(buffer: Uint8Array) {
|
|
|
const len = 4;
|
|
|
if (buffer.length >= len) {
|
|
|
let signatureArr = new Array(len);
|
|
|
for (let i = 0; i < len; i++) signatureArr[i] = buffer[i]!.toString(16);
|
|
|
const signature = signatureArr.join("").toUpperCase();
|
|
|
switch (signature) {
|
|
|
case "89504E47":
|
|
|
return "image/png";
|
|
|
case "47494638":
|
|
|
return "image/gif";
|
|
|
case "25504446":
|
|
|
return "application/pdf";
|
|
|
case "FFD8FFDB":
|
|
|
case "FFD8FFE0":
|
|
|
return "image/jpeg";
|
|
|
case "504B0304":
|
|
|
return "application/zip";
|
|
|
default:
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
type BroadcasterMessage<T extends {}> = {
|
|
|
id: string;
|
|
|
replyId?: string;
|
|
|
action: string;
|
|
|
window: Window;
|
|
|
port: MessagePort;
|
|
|
payload?: T;
|
|
|
};
|
|
|
|
|
|
type BroadcasterMessageOptions = {
|
|
|
timeout?: number;
|
|
|
listenForReply?: boolean;
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class Broadcaster<OutPayload extends {}, InPayload extends {}> extends EventTarget {
|
|
|
private channel: BroadcastChannel;
|
|
|
private queue: {[key: string]: Resolver<InPayload[]>} = {};
|
|
|
|
|
|
constructor(channelName: string) {
|
|
|
super();
|
|
|
this.queue = {};
|
|
|
this.channel = new BroadcastChannel(channelName);
|
|
|
this.channel.addEventListener("message", (e) => {
|
|
|
this.onMessage(e);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private getId() {
|
|
|
let id: string;
|
|
|
do {
|
|
|
id = generateId(6);
|
|
|
} while (this.queue[id]);
|
|
|
return id;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async broadcastAndWait(
|
|
|
action: string,
|
|
|
payload?: OutPayload,
|
|
|
options?: BroadcasterMessageOptions,
|
|
|
): Promise<InPayload[]> {
|
|
|
const id = this.getId();
|
|
|
this.queue[id] = getResolver<InPayload[]>(options?.timeout);
|
|
|
this.channel.postMessage({
|
|
|
id,
|
|
|
action,
|
|
|
payload,
|
|
|
});
|
|
|
let response: InPayload[];
|
|
|
try {
|
|
|
response = await this.queue[id]!.promise;
|
|
|
} catch (e) {
|
|
|
console.log("CAUGHT", e);
|
|
|
response = [];
|
|
|
}
|
|
|
return response;
|
|
|
}
|
|
|
|
|
|
broadcast(action: string, payload?: OutPayload) {
|
|
|
this.channel.postMessage({
|
|
|
id: this.getId(),
|
|
|
action,
|
|
|
payload,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
reply(replyId: string, action: string, payload?: OutPayload) {
|
|
|
this.channel.postMessage({
|
|
|
id: this.getId(),
|
|
|
replyId,
|
|
|
action,
|
|
|
payload,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
openWindowAndWaitForMessage(rgthreePath: string, windowName?: string) {
|
|
|
const id = this.getId();
|
|
|
this.queue[id] = getResolver();
|
|
|
const win = window.open(`/rgthree/${rgthreePath}#broadcastLoadMsgId=${id}`, windowName);
|
|
|
return {window: win, promise: this.queue[id]!.promise};
|
|
|
}
|
|
|
|
|
|
onMessage(e: MessageEvent<BroadcasterMessage<InPayload>>) {
|
|
|
const msgId = e.data?.replyId || "";
|
|
|
const queueItem = this.queue[msgId];
|
|
|
if (queueItem) {
|
|
|
if (queueItem.completed) {
|
|
|
console.error(`${msgId} already completed..`);
|
|
|
}
|
|
|
queueItem.deferment = queueItem.deferment || {data: []};
|
|
|
queueItem.deferment.data.push(e.data.payload);
|
|
|
queueItem.deferment.timeout && clearTimeout(queueItem.deferment.timeout);
|
|
|
queueItem.deferment.timeout = setTimeout(() => {
|
|
|
queueItem.resolve(queueItem.deferment!.data);
|
|
|
}, 250);
|
|
|
} else {
|
|
|
this.dispatchEvent(
|
|
|
new CustomEvent("rgthree-broadcast-message", {
|
|
|
detail: Object.assign({replyTo: e.data?.id}, e.data),
|
|
|
}),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
addMessageListener(callback: EventListener, options?: any) {
|
|
|
return super.addEventListener("rgthree-broadcast-message", callback, options);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const broadcastChannelMap: Map<BroadcastChannel, {[key: string]: Resolver<any>}> = new Map();
|
|
|
|
|
|
export function broadcastOnChannel<T extends {}>(
|
|
|
channel: BroadcastChannel,
|
|
|
action: string,
|
|
|
payload?: T,
|
|
|
) {
|
|
|
let queue = broadcastChannelMap.get(channel);
|
|
|
if (!queue) {
|
|
|
broadcastChannelMap.set(channel, {});
|
|
|
queue = broadcastChannelMap.get(channel)!;
|
|
|
}
|
|
|
let id: string;
|
|
|
do {
|
|
|
id = generateId(6);
|
|
|
} while (queue[id]);
|
|
|
queue[id] = getResolver();
|
|
|
channel.postMessage({
|
|
|
id,
|
|
|
action,
|
|
|
payload,
|
|
|
});
|
|
|
return queue[id]!.promise;
|
|
|
}
|
|
|
|