| |
| |
| |
|
|
|
|
| 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:") || source.includes(";base64,");
|
| }
|
|
|
| |
| |
|
|
| 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 newCanvas(
|
| widthOrPtOrImage: number | {width: number; height: number} | HTMLImageElement,
|
| height?: number,
|
| ) {
|
| let width: number;
|
| if (typeof widthOrPtOrImage !== "number") {
|
| width = widthOrPtOrImage.width;
|
| height = widthOrPtOrImage.height;
|
| } else {
|
| width = widthOrPtOrImage;
|
| height = height;
|
| }
|
| if (height == null) {
|
| throw new Error("Invalid height supplied when creating new canvas object.");
|
| }
|
| const canvas = document.createElement("canvas");
|
| canvas.width = width;
|
| canvas.height = height;
|
| if (widthOrPtOrImage instanceof HTMLImageElement) {
|
| const ctx = canvas.getContext("2d")!;
|
| ctx.drawImage(widthOrPtOrImage, 0, 0, width, height);
|
| }
|
| return canvas;
|
| }
|
|
|
| |
| |
|
|
| export function getCanvasImageData(
|
| image: HTMLImageElement,
|
| ): [HTMLCanvasElement, CanvasRenderingContext2D, ImageData] {
|
| const canvas = newCanvas(image);
|
| const ctx = canvas.getContext("2d")!;
|
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.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) {
|
| if (looksLikeBase64(source.src)) {
|
| return source.src;
|
| }
|
| 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;
|
| }
|
|
|