(function () { 'use strict'; // ========================================================================= // 0000-a1111-fix/javascript/fix.js — v4.2 // // Порядок загрузки гарантируется двумя механизмами: // // 1. Имя '!0000-a1111-fix' — A1111 сортирует user extensions alphabetically // (sorted(os.listdir(...))), '!' ставит нас первыми среди пользовательских // расширений. // // 2. metadata.ini [javascript/fix.js] Before = sd-webui-tabs-extension ... // A1111 строит topological sort через list_scripts() и учитывает // Before/After зависимости для JS-файлов. // // Что НЕ покрывается: // Builtin extensions (extensions-builtin/) грузятся ДО пользовательских. // Три builtin используют onUiLoaded: canvas-zoom-and-pan, mobile, // prompt-bracket-checker. Все три исторически короткие, но реальная длительность зависит от среды. // Источник медленных callbacks — пользовательские расширения (ControlNet и др.). // // Четыре исправления: // // [FIX-0] Idempotence guard. // Гарантирует однократное применение патча при любых условиях. // // [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce, max-wait 1000ms. // Оригинал (script.js:111-115): clearTimeout + setTimeout 200ms. // При 50+ расширениях DOM мутирует 2-5 сек непрерывно — uiAfterUpdateTimeout // сбрасывается каждый раз, uiAfterUpdateCallbacks так и не вызываются. // Фикс: добавляем maxWaitTimer, который форсирует вызов через MAX_WAIT_MS // независимо от новых мутаций. uiAfterUpdateTimeout продолжает жить как // глобал core (совместимость сохранена). // Re-entry guard защищает от рекурсии если callback сам вызывает мутацию. // // [FIX-2] onUiLoaded timing — полный охват existing + future callbacks. // Оригинал (script.js:60-62): uiLoadedCallbacks.push(callback). // executeCallbacks (script.js:95-102) — синхронный, не cancelable. // v3: оборачивал только БУДУЩИЕ вызовы onUiLoaded — builtin callbacks // уже в uiLoadedCallbacks ДО нашего патча → не покрыты. // v4: оборачиваем уже существующие элементы uiLoadedCallbacks + будущие. // Symbol(WRAPPED) предотвращает двойную обёртку при любом сценарии. // Логирование только первого краша на callback (Set по имени). // // [FIX-3] inputAccordionChecked — resilient resolver для tabs extension. // tabs_parser.js (строки 92-100): если accordion.id не null и не // начинается с "component-": // content.id = accordion.id; // accordion.id = '.' + accordion.id; // content.visibleCheckbox = {}; <- plain Object, НЕ HTMLInputElement // content.onVisibleCheckboxChange = () => {}; // Кроме того, в оригинальный accordion вставляется клонированный checkbox // (строки 73-85) — т.е. .input-accordion-checkbox остаётся В accordion'е. // // Resolver (resolveRealAccordion) проверяет три пути: // 1. getElementById(id) → если реальный accordion (не перемещён) // 2. getElementById('.' + id) → tabs: оригинальный accordion переименован // 3. querySelector .input-accordion-checkbox внутри el → fallback на случай // если tabs extension изменит схему именования id в будущих версиях // Log-once через Set предотвращает spam в консоли. // ========================================================================= // ─── [FIX-0] Idempotence guard ─────────────────────────────────────────── if (window.__a1111_fix_v4_applied) { console.warn('[a1111-fix] already applied, skipping duplicate load'); return; } window.__a1111_fix_v4_applied = true; // ─── [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce ─────────── function applyFix1() { if (typeof scheduleAfterUiUpdateCallbacks === 'undefined' || typeof executeCallbacks === 'undefined' || typeof uiAfterUpdateCallbacks === 'undefined') { console.warn('[a1111-fix] scheduleAfterUiUpdateCallbacks/executeCallbacks/uiAfterUpdateCallbacks not found, skipping FIX-1'); return; } const DEBOUNCE_MS = 200; const MAX_WAIT_MS = 1000; let maxWaitTimer = null; let running = false; function runAfterUpdateCallbacks() { // uiAfterUpdateTimeout: продолжаем вести как глобал core (совместимость) clearTimeout(uiAfterUpdateTimeout); clearTimeout(maxWaitTimer); window.uiAfterUpdateTimeout = null; maxWaitTimer = null; if (running) return; running = true; try { executeCallbacks(uiAfterUpdateCallbacks); } finally { running = false; } } window.scheduleAfterUiUpdateCallbacks = function () { clearTimeout(uiAfterUpdateTimeout); // uiAfterUpdateTimeout: сохраняем глобал, как делает оригинал (script.js:112-113) window.uiAfterUpdateTimeout = setTimeout(runAfterUpdateCallbacks, DEBOUNCE_MS); if (!maxWaitTimer) { maxWaitTimer = setTimeout(runAfterUpdateCallbacks, MAX_WAIT_MS); } }; console.log('[a1111-fix] FIX-1: bounded debounce max-wait=' + MAX_WAIT_MS + 'ms applied'); } // ─── [FIX-2] onUiLoaded timing — existing + future callbacks ───────────── function applyFix2() { if (typeof onUiLoaded === 'undefined' || typeof uiLoadedCallbacks === 'undefined') { console.warn('[a1111-fix] onUiLoaded or uiLoadedCallbacks not found, skipping FIX-2'); return; } const SLOW_THRESHOLD_MS = 100; const WRAPPED = Symbol('a1111FixWrapped'); // WeakSet по исходному callback (не по имени): Set глушил бы все // '(anonymous)' callbacks или однофамильцев — один краш скрывал бы другие. const crashedOnce = new WeakSet(); function wrapCallback(callback) { if (typeof callback !== 'function') return callback; if (callback[WRAPPED]) return callback; // уже обёрнут — не дублируем const wrapped = function (arg) { const t0 = performance.now(); try { return callback(arg); } catch (e) { // Не бросаем исключение дальше: core executeCallbacks (script.js:95-102) // сам ловит ошибки и пишет console.error — двойное логирование не нужно. // WeakSet по callback — каждый упавший callback логируется ровно один раз. if (!crashedOnce.has(callback)) { crashedOnce.add(callback); console.error('[a1111-fix] onUiLoaded callback crashed:', callback.name || '(anonymous)', e); } } finally { const elapsed = performance.now() - t0; if (elapsed > SLOW_THRESHOLD_MS) { console.warn( '[a1111-fix] SLOW onUiLoaded: "' + (callback.name || '(anonymous)') + '" took ' + elapsed.toFixed(0) + 'ms' ); } } }; wrapped[WRAPPED] = true; wrapped.__a1111_fix_original = callback; return wrapped; } // 1) Уже зарегистрированные callbacks (builtin extensions и ранние user extensions) // uiLoadedCallbacks — глобальный массив (script.js:29), изменяем на месте. let existingCount = 0; for (let i = 0; i < uiLoadedCallbacks.length; i++) { if (typeof uiLoadedCallbacks[i] === 'function') { uiLoadedCallbacks[i] = wrapCallback(uiLoadedCallbacks[i]); existingCount++; } } // 2) Будущие callbacks через onUiLoaded(fn) const _originalOnUiLoaded = window.onUiLoaded; window.onUiLoaded = function (callback) { _originalOnUiLoaded(wrapCallback(callback)); }; console.log( '[a1111-fix] FIX-2: timing applied to ' + existingCount + ' existing + all future onUiLoaded callbacks' ); } // ─── [FIX-3] inputAccordionChecked — resilient accordion resolver ───────── function applyFix3() { if (typeof inputAccordionChecked === 'undefined') { console.warn('[a1111-fix] inputAccordionChecked not found, skipping FIX-3'); return; } const warnedOnce = new Set(); /** * Проверка, является ли el реальным setupAccordion-элементом. * Реальный accordion после setupAccordion (inputAccordion.js:41-43): * accordion.visibleCheckbox = visibleCheckbox (document.createElement('INPUT')) * accordion.onVisibleCheckboxChange = function() {...} * Стаб tabs_parser.js (строки 98-100): * content.visibleCheckbox = {} <- plain Object * content.onVisibleCheckboxChange = () => {} <- no-op */ function isRealAccordion(el) { return !!( el && el.visibleCheckbox instanceof HTMLInputElement && typeof el.onVisibleCheckboxChange === 'function' ); } /** * Три пути поиска реального accordion'а: * * 1. getElementById(id) — стандартный путь, работает если tabs extension * не трогал этот accordion (нет isInput, или id начинается с "component-"). * * 2. getElementById('.' + id) — tabs_parser.js переименовывает оригинальный * accordion: accordion.id = '.' + originalId (строка 94). * * 3. querySelector('.input-accordion-checkbox') внутри el — tabs_parser.js * вставляет клонированный checkbox в content (строки 73-85), но оригинальный * .input-accordion-checkbox остаётся внутри переименованного accordion. * Этот путь — fallback на случай изменения схемы id в будущих версиях. */ function resolveRealAccordion(id) { const direct = gradioApp().getElementById(id); if (isRealAccordion(direct)) return direct; // tabs extension: accordion переименован в '.' + id const dotted = gradioApp().getElementById('.' + id); if (isRealAccordion(dotted)) return dotted; // Fallback: поиск .input-accordion-checkbox в ближайшем контейнере // На случай изменения схемы tabs extension или нестандартных layouts if (direct) { const nestedCheckbox = direct.querySelector('.input-accordion-checkbox'); if (nestedCheckbox) { // Поднимаемся к ближайшему .input-accordion const parentAccordion = nestedCheckbox.closest('.input-accordion'); if (isRealAccordion(parentAccordion)) return parentAccordion; } } return null; } window.inputAccordionChecked = function (id, checked) { const realEl = resolveRealAccordion(id); if (!realEl) { if (!warnedOnce.has(id)) { warnedOnce.add(id); const direct = gradioApp().getElementById(id); console.error( '[a1111-fix] FIX-3: cannot resolve accordion for id:', id, '\n direct el:', direct, '\n direct.visibleCheckbox:', direct && direct.visibleCheckbox, '\n direct.visibleCheckbox instanceof HTMLInputElement:', direct && (direct.visibleCheckbox instanceof HTMLInputElement), '\n dotted el:', gradioApp().getElementById('.' + id) ); } return; } realEl.visibleCheckbox.checked = checked; realEl.onVisibleCheckboxChange(); }; console.log('[a1111-fix] FIX-3: resilient accordion resolver applied'); } // ─── Apply ──────────────────────────────────────────────────────────────── applyFix1(); applyFix2(); applyFix3(); console.log('[a1111-fix] v4.2 ready'); })();