import { StringUtils } from "./StringUtils.js"; /** * Define os tipos de operação que o locker pode controlar */ export enum LockManagerOperation { /** Operação de lock utilizada para controlar cliques nos botoes da taskbar. */ TASKBAR_CLICK = "taskbar_click", /** Operação de lock utilizada para controlar carregamento da aplicação. */ APP_LOADING = "app_loading" } type Lock = { promise?: Promise, resolve?: () => void, done: boolean } export class LockManager { private static _locks = new Map>(); /** * Nome do atributo que será utilizado para controlar contexto de locks nos elementos da DOM. */ public static ATTRIBUTE_NAME = "data-locker-manger-context-id"; private static buildContextID(): string { return StringUtils.generateUUID(); } private static buildLockerID(ctxId: string | HTMLElement, operation: LockManagerOperation): string | undefined { if (ctxId == undefined) return undefined; let resolvedID: any = ctxId; if (resolvedID instanceof HTMLElement) { resolvedID = (ctxId as HTMLElement).getAttribute(LockManager.ATTRIBUTE_NAME); if (!resolvedID) return undefined; } return `${resolvedID}_${operation}`; } private static findExistingCtxId = (element: HTMLElement): string | null => { let currentElement: HTMLElement | null = element; while (currentElement) { if (currentElement.hasAttribute(LockManager.ATTRIBUTE_NAME)) { return currentElement.getAttribute(LockManager.ATTRIBUTE_NAME); } const childWithCtxId = Array.from(currentElement.children).find(child => (child instanceof HTMLElement) && child.hasAttribute(LockManager.ATTRIBUTE_NAME) ) as HTMLElement | undefined; if (childWithCtxId) { return childWithCtxId.getAttribute(LockManager.ATTRIBUTE_NAME); } currentElement = currentElement.parentElement; } return null; } private static traverseAndAddAttr = (element: HTMLElement, ctxId: string): void => { if (element.tagName.startsWith('EZ-') || element.tagName.startsWith('SNK-')) { element.setAttribute(LockManager.ATTRIBUTE_NAME, ctxId); } Array.from(element.children).forEach((child) => { if (child instanceof HTMLElement) { LockManager.traverseAndAddAttr(child, ctxId); } }); }; /** * Cria um contexto de locker, caso nao exista, para todos elementos pais iniciados com ez- ou snk-. * * @param startElement - Elemento de de onde o lock deve começar. * * @returns - O id do locker, que pode ser usado para iniciar ou aguardar um lock do contexto. */ public static addLockManagerCtxId(startElement: HTMLElement): string { try { if (!startElement) { console.error("Elemento inicial não fornecido."); return ""; } const ctxId = LockManager.findExistingCtxId(startElement) ?? LockManager.buildContextID(); let currentElement: HTMLElement | null = startElement; LockManager.traverseAndAddAttr(currentElement, ctxId); return ctxId; } catch (err) { console.warn(`Erro ao registrar locks para o elemento: ${startElement?.tagName}`, err); return ""; } } /** * Reseta todos os locks existentes para um determinado contexto e operação de forma assíncrona * @param id - ID do contexto ou elemento HTML contendo contexto * @param operation - Operação específica para resetar os locks * @returns Promise que será resolvida quando todos os locks forem resetados */ public static async resetLocks(id: string | HTMLElement, operation: LockManagerOperation): Promise { const lockerId = this.buildLockerID(id, operation); if (!lockerId) return; const currentLocks = this._locks.get(lockerId); if (currentLocks?.length) { await Promise.all(currentLocks.map(lock => { lock.done = true; lock.resolve?.(); return lock.promise; })); this._locks.delete(lockerId); } } /** * Inicia um locker baseado em um contexto e uma operação. * * @param id - Pode ser um ID do contexto de locker, ou, o elemento contendo um contexto de locker. * @param operation - Operação do contexto que o lock deve ser feito. * * @returns - Uma função que fara a liberação do lock. */ public static lock(id: string | HTMLElement, operation: LockManagerOperation): () => void { const lockerId = LockManager.buildLockerID(id, operation); if (!lockerId) return () => { }; const lock: Lock = { done: false }; const promise = new Promise(resolve => lock.resolve = resolve); lock.promise = promise; const currentLocks = LockManager._locks.get(lockerId) ?? []; currentLocks.push(lock); LockManager._locks.set(lockerId, currentLocks); return () => { lock.done = true; lock.resolve?.(); } } /** * Aguarda todos os lockers de um contexto e operação serem resolvidos. * * @param id - Pode ser um ID do contexto de locker, ou, o elemento contendo um contexto de locker. * @param operation - Operação do contexto que devera aguardar. * * @returns - Promise que será resolvida quando todos lockers forem finalizados. */ public static async whenResolve(id: string | HTMLElement, operation: LockManagerOperation, debounce?: number, timeOut?: number): Promise { const lockerId = LockManager.buildLockerID(id, operation); if (!lockerId) return; if (debounce) await new Promise(resolve => setTimeout(resolve, debounce)); const startTime = Date.now(); while (LockManager._locks.get(lockerId)?.length) { if (timeOut && Date.now() - startTime >= timeOut) { await this.resetLocks(id, operation); return; } const locks: Array = LockManager._locks.get(lockerId) ?? []; await Promise.all(locks.map(lock => lock.promise)); //Aguarda listeners da tela reagirem as mudancas de estado do dataunit await new Promise(resolve => setTimeout(resolve, debounce || 200)); LockManager._locks.set(lockerId, locks.filter(lock => !lock.done)); } } public static async whenHasLock(id: string | HTMLElement, operation: LockManagerOperation, timeOut?: number): Promise { const lockerId = LockManager.buildLockerID(id, operation); if (!lockerId) return; const startTime = Date.now(); while (!LockManager._locks.get(lockerId)?.length) { if (timeOut && Date.now() - startTime >= timeOut) { await this.resetLocks(id, operation); return; } await new Promise(resolve => setTimeout(resolve, 200)); } } }