tts-voice-creator-clone-and.../static/loader.js
mARTin-B78 560b82b0ef perf(ui): lazy-render big sections to stay within the DOM node budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:13:19 +02:00

295 lines
14 KiB
JavaScript

(async function () {
'use strict';
// Hard safety net: never show skeleton longer than 8 s regardless of what
// fails below. A partial UI beats an indefinite spinner.
var _skTimeout = setTimeout(function () {
var sk = document.getElementById('app-skeleton');
if (sk) {
sk.classList.add('sk-hidden');
// Fully remove from DOM — otherwise the (display:none) skeleton still sits
// in the document and earlier reveal logic would skip removal.
setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300);
document.querySelectorAll('.page-section').forEach(function (s) { s.style.display = ''; });
}
}, 8000);
const SECTIONS = [
's-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser',
's-reader', 's-caststudio', 's-performance', 's-routing', 's-connect', 's-settings',
's-llms', 's-conversation', 's-library', 's-magazine',
];
function _storedSection() {
try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; }
}
function _hashSection() {
var raw = String(location.hash || '').replace(/^#/, '').trim();
if (!raw) return '';
if (raw.indexOf('section=') === 0) raw = raw.slice(8);
raw = decodeURIComponent(raw).replace(/^\/?/, '');
return SECTIONS.indexOf(raw) !== -1 ? raw : '';
}
// Determine which section to show first (same logic as nav.js): hash > stored > My Voices.
var _savedSection = _storedSection();
var _activeSection = _hashSection() || ((_savedSection && SECTIONS.indexOf(_savedSection) !== -1) ? _savedSection : 's-voices');
// ── App version for cache-busting ────────────────────────────────────────
// Read from the already-loaded style.css href — no extra network round trip.
// Falls back to fetching /api/version only if the tag isn't found.
var _appVersion = 'dev';
var _styleHref = (document.querySelector('link[href*="style.css"]') || {}).getAttribute ? document.querySelector('link[href*="style.css"]').getAttribute('href') : '';
var _styleMatch = _styleHref && _styleHref.match(/[?&]v=([^&]+)/);
if (_styleMatch) {
_appVersion = _styleMatch[1];
} else {
// Fallback: fetch from API (only on first ever load before style tag exists)
try {
var _vr = await fetch('/api/version');
var _vd = await _vr.json();
_appVersion = _vd.version || _appVersion;
} catch (_) {}
}
// Session timestamp used ONLY for section HTML files (never cached by SW anyway).
// JS/CSS use the version string so the service worker cache stays valid across reloads.
var _ts = Date.now();
// Sections whose only consumer is a lazy feature group: their markup is not
// fetched/injected at startup (DOM-node budget, tests/constrained.spec.js);
// it is injected on first navigation, before that group's modules run.
var LAZY_HTML_SECTIONS = ['s-magazine'];
var _uiReady = false;
var _sectionHtml = {};
function _inject(id, html) {
var el = document.getElementById(id);
if (el) el.innerHTML = html;
}
// ── 1. Load ONLY the active section before anything else ─────────────────
// Feature modules set up DOM event listeners — the active section's elements
// must exist when JS runs. Other sections are loaded in step 2.
try {
var _ar = await fetch('/static/sections/' + _activeSection + '.html?v=' + _ts);
if (!_ar.ok) throw new Error(_ar.status + ' ' + _ar.statusText);
_inject(_activeSection, await _ar.text());
} catch (e) {
var _ael = document.getElementById(_activeSection);
if (_ael) _ael.innerHTML =
'<p style="color:var(--red);padding:24px">Failed to load section <b>' +
_activeSection + '</b>: ' + e.message + '</p>';
}
// ── 2. Fetch remaining sections in background (parallel with JS loading) ──
// These run concurrently with utils + settings below. Because section HTML
// files are much smaller than JS, they finish before the feature-module batch
// needs them.
var _bgSections = Promise.all(
SECTIONS.filter(function (id) { return id !== _activeSection && LAZY_HTML_SECTIONS.indexOf(id) === -1; })
.map(async function (id) {
try {
var res = await fetch('/static/sections/' + id + '.html?v=' + _ts);
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
_inject(id, await res.text());
} catch (e) {
var el = document.getElementById(id);
if (el) el.innerHTML =
'<p style="color:var(--red);padding:24px">Failed to load section <b>' +
id + '</b>: ' + e.message + '</p>';
}
})
);
// ── 3. Load JS in parallel batches ────────────────────────────────────────
// Scripts with async=false are FETCHED in parallel but EXECUTED in DOM
// insertion order — safe for modules that share a global scope.
//
// Each script gets up to 3 attempts before this rejects. Without a retry,
// one dropped request on a flaky mobile connection left the rest of the
// batch executing anyway (a script's own failure never blocked its
// siblings) — a function or variable from the missing module stayed
// undefined while everything downstream of it kept running, surfacing as
// confusing symptoms nowhere near the real cause (confirmed live: a core
// module failing to load left a later one throwing "fmtDbfs is not
// defined", and a save button reachable at all reporting "No audio
// ready" instead of the page ever explaining a script hadn't loaded).
function _load(src, attemptsLeft) {
attemptsLeft = attemptsLeft == null ? 3 : attemptsLeft;
return new Promise(function (resolve, reject) {
var s = document.createElement('script');
s.src = src + '?v=' + _appVersion;
s.async = false; // fetch in parallel, execute in DOM order
s.onload = resolve;
s.onerror = function () {
if (s.parentNode) s.parentNode.removeChild(s);
if (attemptsLeft > 1) {
setTimeout(function () {
_load(src, attemptsLeft - 1).then(resolve, reject);
}, 350 * (4 - attemptsLeft)); // 350ms, then 700ms before the last try
} else {
reject(new Error('Failed to load ' + src));
}
};
document.body.appendChild(s);
});
}
function _loadBatch(srcs) {
return Promise.all(srcs.map(function (src) { return _load(src); }));
}
// Retries above absorb a transient drop; this is what the user sees when
// a module still would not load after 3 tries — reloading is the only
// real fix (nothing downstream of a missing module can be trusted), so
// say so plainly rather than leaving a partially-working page to explain
// itself through whatever error a missing function happens to throw.
function _notifyLoadFailure() {
console.error('One or more app modules failed to load after retries.');
try {
if (typeof window.toast === 'function') {
window.toast('Some parts of the app failed to load. Please reload the page.', 'error', 20000);
return;
}
} catch (_) {}
// utils.js itself failed to load, or toast() errored -- fall back to
// a plain banner rather than a silent console-only failure.
var banner = document.createElement('div');
banner.textContent = 'Some parts of the app failed to load. Please reload the page.';
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:6000;'
+ 'background:#b91c1c;color:#fff;padding:10px 16px;font:600 14px system-ui,sans-serif;text-align:center;';
document.body.appendChild(banner);
}
// A — foundation (sequential: each must finish before the next batch starts)
await _load('/static/js/utils.js');
await _load('/static/js/i18n.js'); // defines window.t / applyI18n before features run
await _load('/static/js/settings.js');
// Wait for background sections BEFORE feature modules run.
// Feature modules set up event listeners on DOM elements; all section HTML
// must already be injected. On a local server the sections finish well before
// settings.js completes, so this await is typically instant.
await _bgSections;
// C — feature modules: always try the bundle first (one request instead of 23).
// Falls back to individual files if the bundle is missing or fails to load.
var _featureFiles = [];
var _lazyFeatureGroups = {};
try {
var _manifestResponse = await fetch('/static/module-manifest.json?v=' + _appVersion, { cache: 'no-store' });
if (!_manifestResponse.ok) throw new Error('module manifest ' + _manifestResponse.status);
var _manifest = await _manifestResponse.json();
if (!Array.isArray(_manifest.core) || !_manifest.core.length) throw new Error('empty core module manifest');
_featureFiles = _manifest.core.map(function (name) { return '/static/js/' + name + '.js'; });
_lazyFeatureGroups = _manifest.lazy && typeof _manifest.lazy === 'object' ? _manifest.lazy : {};
} catch (_) {
console.error('Unable to load the feature module manifest');
}
var _bundleOk = false;
try { await _load('/static/dist/main.min.js'); _bundleOk = true; }
catch (_) { _bundleOk = false; }
var _loadedFeatureModules = new Set();
if (_bundleOk) _featureFiles.forEach(function (src) { _loadedFeatureModules.add(src); });
if (!_bundleOk) {
try {
await _loadBatch(_featureFiles);
_featureFiles.forEach(function (src) { _loadedFeatureModules.add(src); });
} catch (error) {
// A module that still failed after 3 retries each is not a blip --
// the app cannot be trusted to work correctly from here. Mark
// whatever DID load so ensureFeatureModules doesn't re-fetch it, and
// say so plainly rather than silently revealing a partial app.
_featureFiles.forEach(function (src) { _loadedFeatureModules.add(src); });
console.error('Core feature modules failed to load', error);
_notifyLoadFailure();
}
}
// Public section loader used by nav.js. Modules are loaded in manifest order,
// exactly once, and a failed group leaves the existing section visible rather
// than turning navigation into a blank screen.
function _ensureSectionHtml(id) {
var el = document.getElementById(id);
if (LAZY_HTML_SECTIONS.indexOf(id) === -1 || !el || el.firstElementChild) return Promise.resolve();
if (_sectionHtml[id]) return _sectionHtml[id];
_sectionHtml[id] = fetch('/static/sections/' + id + '.html?v=' + _ts)
.then(function (res) {
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
return res.text();
})
.then(function (html) {
_inject(id, html);
if (_uiReady && window.applyI18n) window.applyI18n(el);
})
.catch(function (e) {
delete _sectionHtml[id];
el.innerHTML = '<p style="color:var(--red);padding:24px">Failed to load section <b>' + id + '</b>: ' + e.message + '</p>';
});
return _sectionHtml[id];
}
window.ensureFeatureModules = function (sectionId) {
return _ensureSectionHtml(sectionId).then(function () { return _ensureModules(sectionId); });
};
function _ensureModules(sectionId) {
var names = Array.isArray(_lazyFeatureGroups[sectionId]) ? _lazyFeatureGroups[sectionId] : [];
var pending = names.map(function (name) { return '/static/js/' + name + '.js'; })
.filter(function (src) { return !_loadedFeatureModules.has(src); });
if (!pending.length) return Promise.resolve();
pending.forEach(function (src) { _loadedFeatureModules.add(src); });
return _loadBatch(pending).catch(function (error) {
pending.forEach(function (src) { _loadedFeatureModules.delete(src); });
console.error('Unable to load section modules for ' + sectionId, error);
_notifyLoadFailure();
throw error;
});
}
// D — navigation (must run before init/reveal so routing is interactive before
// async settings or voice-library refreshes can occupy the backend).
try { await _load('/static/nav.js'); } catch (_) {}
// A deep link or restored section must have its feature code before init and
// before the first visible section is activated.
try { if (window.ensureFeatureModules) await window.ensureFeatureModules(_activeSection); } catch (_) {}
// E — init (needs feature modules and nav globals to be defined)
try { await _load('/static/js/init.js'); } catch (_) {}
// ── Remove skeleton, reveal sections ────────────────────────────────────
// Always runs — even if a script above failed to load, showing a partial UI
// is far better than leaving the skeleton on screen indefinitely.
_revealApp();
function _revealApp() {
clearTimeout(_skTimeout);
var sk = document.getElementById('app-skeleton');
// Always remove the skeleton if it's still in the DOM — never gate on
// sk-hidden (the 8s fallback may have added that class without removing it).
if (sk) {
sk.classList.add('sk-hidden');
setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300);
}
document.querySelectorAll('.page-section').forEach(function (s) {
s.style.display = '';
});
}
// ── i18n: add the language picker and translate the (now fully-rendered) UI ──
if (window.initLangPicker) window.initLangPicker();
if (window.applyI18n) window.applyI18n(document);
if (window.initAccessibilityEnhancements) window.initAccessibilityEnhancements();
_uiReady = true;
// E — post-init modules load in background AFTER the UI is already visible.
// Engines, AI backends, and conversation load while the user browses other tabs.
_loadBatch([
'/static/js/engines.js',
'/static/js/ai-backends.js',
'/static/js/conversation.js',
]);
})();