mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-19 06:52:38 +00:00
Puts every external `<script>` in the v3 shell into the deferred queue, and
keeps each script's boot() firing at DOMContentLoaded exactly as it does today.
Behaviourally a no-op; it is what makes the ES-module flips safe.
WHY. `type="module"` defers execution to after HTML parse. Classic-`defer` and
module scripts share ONE "execute after parsing" list and run in DOCUMENT ORDER,
but a plain classic script runs DURING parse — ahead of all of them. So the
moment capabilities.js becomes a module while app.js is still plain, app.js runs
FIRST, and its 11 top-level `window.feedBack.on(...)` calls (app.js:6245-6722)
hit a bare `{}` — `_ensureFeedBackEventBus()` (capabilities.js:33), which
attaches .on/.emit/.off, would not have run yet. TypeError, app.js dies
mid-parse. Deferring everything now keeps document order == execution order
through the rest of the migration.
THE CATCH (Codex preflight caught this — a real ordering change). 22 scripts
guard their boot with `if (document.readyState === 'loading')`. A deferred
script runs at readyState 'interactive', so that test is FALSE and the else-branch
fires boot() immediately, at the script's position in document order — instead of
at DOMContentLoaded, after every script has evaluated.
That matters far more than one call site: a scan of the shell's scripts found
**43 forward references** where a script's boot() reads a global that a LATER
script defines (shell.js -> profile.js's window.v3Onboarding, songs.js ->
settings.js's window._confirmDialog, badges.js -> songs.js's
window.displayTuningName, ...). Every one of them resolves today only because
all boots happen at DOMContentLoaded. So the guards now treat 'interactive' as
not-ready (`!== 'complete'`), restoring that exactly.
Codex's specific finding (first-run onboarding silently skipped) did NOT
reproduce — shell.js's boot() awaits /api/profile, and that yield lets the
remaining deferred scripts run first. But the race it described is real, the
guard is silent when it fails (`&& window.v3Onboarding`), and the other 42
forward refs have no such await protecting them. Fixed at the root rather than
at the one site.
VERIFIED. A/B against origin/main on a fresh profile, 13 probes (onboarding
overlay, v3Onboarding/v3Songs/v3Profile/fbNotify/v3Badges/uiPrompt/showScreen,
bus, capabilities.version, createHighway, plugin scripts, mounted screens):
IDENTICAL, zero console/page errors on both. pytest 2396, node 1028/1028,
ESLint 0 errors, Codex 0.
New guard: test_every_external_script_defers_so_document_order_is_execution_order
fails if any external tag is plain classic — verified to fail on a single
reverted tag, so it actually bites.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
254 lines
8.1 KiB
JavaScript
254 lines
8.1 KiB
JavaScript
/*
|
|
* fee[dB]ack v0.3.0 — Venue 3D scene bridge.
|
|
*
|
|
* Activates the highway_3d `venue` background style when Visualization = Venue.
|
|
* Reacts to v3:live-performance-state for lighting mood (read-only).
|
|
*/
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
const THEME_ID = 'small-club';
|
|
const ASSET_BASE = '/static/assets/venue/themes/small-club/';
|
|
const BG_PLATE = 'bg-plate.png';
|
|
const BG_PLATE_WEBP = 'bg-plate.webp';
|
|
|
|
let _active = false;
|
|
let _assetsLoaded = false;
|
|
let _loadFailed = false;
|
|
let _lastMood = 'idle';
|
|
let _bound = false;
|
|
|
|
function isVenueViz() {
|
|
if (root && root.v3VenueViz && typeof root.v3VenueViz.isVenueVisualization === 'function') {
|
|
const sel = root.v3VenueViz.getSelectedVizId
|
|
? root.v3VenueViz.getSelectedVizId()
|
|
: root.v3VenueViz.readVizSelection();
|
|
return root.v3VenueViz.isVenueVisualization(sel);
|
|
}
|
|
try {
|
|
const sel = document.getElementById('viz-picker');
|
|
if (sel && sel.value) return String(sel.value) === 'venue';
|
|
return localStorage.getItem('vizSelection') === 'venue';
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function h3dApi(name) {
|
|
return root && typeof root[name] === 'function' ? root[name] : null;
|
|
}
|
|
|
|
function setH3dActive(on) {
|
|
const fn = h3dApi('h3dVenueSceneSetActive');
|
|
if (fn) fn(!!on);
|
|
}
|
|
|
|
function setH3dMood(state) {
|
|
const fn = h3dApi('h3dVenueSceneSetMood');
|
|
if (fn) fn(state);
|
|
}
|
|
|
|
function readH3dState() {
|
|
const fn = h3dApi('h3dVenueSceneGetState');
|
|
return fn ? fn() : null;
|
|
}
|
|
|
|
function syncPlaceholderVisibility() {
|
|
try {
|
|
if (root && root.v3VenueViz && typeof root.v3VenueViz.syncPlayerVizClass === 'function') {
|
|
const id = root.v3VenueViz.getSelectedVizId
|
|
? root.v3VenueViz.getSelectedVizId()
|
|
: root.v3VenueViz.readVizSelection();
|
|
root.v3VenueViz.syncPlayerVizClass(id);
|
|
}
|
|
} catch (_) { /* visual-only */ }
|
|
try {
|
|
if (root && root.v3VenueMoodFx && typeof root.v3VenueMoodFx.onVenueVisualizationSelected === 'function' &&
|
|
isVenueViz()) {
|
|
root.v3VenueMoodFx.onVenueVisualizationSelected();
|
|
}
|
|
} catch (_) { /* visual-only */ }
|
|
}
|
|
|
|
function readArrangementSignal() {
|
|
// Intentional karaoke/vocals signal: active arrangement name from the
|
|
// highway WS (user selected Vocals in #arr-select). Do NOT use
|
|
// highway.getLyricsVisible() — lyrics overlay stays on during normal
|
|
// guitar practice and must not force vocals POV.
|
|
try {
|
|
const si = root.highway && typeof root.highway.getSongInfo === 'function'
|
|
? root.highway.getSongInfo()
|
|
: null;
|
|
if (si && si.arrangement) return si.arrangement;
|
|
const cs = root.feedBack && root.feedBack.currentSong;
|
|
if (cs && cs.arrangement) return cs.arrangement;
|
|
if (cs && cs.arrangementSmartName) return cs.arrangementSmartName;
|
|
} catch (_) { /* visual-only */ }
|
|
return '';
|
|
}
|
|
|
|
function syncInstrumentPov() {
|
|
const fn = h3dApi('h3dVenueSceneSetInstrumentPov');
|
|
if (fn) fn(readArrangementSignal());
|
|
}
|
|
|
|
function activate() {
|
|
if (_active) {
|
|
syncInstrumentPov();
|
|
syncVenueMotion();
|
|
return;
|
|
}
|
|
_active = true;
|
|
_assetsLoaded = false;
|
|
_loadFailed = false;
|
|
setH3dActive(true);
|
|
setH3dMood(_lastMood);
|
|
syncInstrumentPov();
|
|
syncVenueMotion();
|
|
}
|
|
|
|
function syncVenueMotion() {
|
|
const motionApi = root && root.v3VenueMoodFx;
|
|
const mode = motionApi && typeof motionApi.getMotion === 'function'
|
|
? motionApi.getMotion()
|
|
: 'subtle';
|
|
const fn = h3dApi('h3dVenueSceneSetMotionMode');
|
|
if (fn) fn(mode);
|
|
else if (motionApi && typeof motionApi.syncMotionToRenderer === 'function') {
|
|
motionApi.syncMotionToRenderer(mode);
|
|
}
|
|
}
|
|
|
|
function deactivate() {
|
|
if (!_active) {
|
|
setH3dActive(false);
|
|
return;
|
|
}
|
|
_active = false;
|
|
_assetsLoaded = false;
|
|
_loadFailed = false;
|
|
setH3dActive(false);
|
|
syncPlaceholderVisibility();
|
|
}
|
|
|
|
function syncViz(vizId) {
|
|
const id = String(vizId || '');
|
|
if (id === 'venue') {
|
|
activate();
|
|
} else {
|
|
deactivate();
|
|
}
|
|
}
|
|
|
|
function onPerformanceState(e) {
|
|
if (!_active) return;
|
|
const d = (e && e.detail) || {};
|
|
const state = String(d.state || 'idle').toLowerCase();
|
|
// v3:live-performance-state fires per note hit/miss; skip the renderer
|
|
// push when the mood is unchanged (e.g. a run of hits all in 'fire').
|
|
if (state === _lastMood) return;
|
|
_lastMood = state;
|
|
setH3dMood(state);
|
|
}
|
|
|
|
function onAssetsLoaded() {
|
|
_assetsLoaded = true;
|
|
_loadFailed = false;
|
|
syncPlaceholderVisibility();
|
|
}
|
|
|
|
function onAssetsFailed() {
|
|
_loadFailed = true;
|
|
_assetsLoaded = false;
|
|
syncPlaceholderVisibility();
|
|
}
|
|
|
|
function bindRuntime() {
|
|
if (_bound) return;
|
|
_bound = true;
|
|
const sm = root && root.feedBack;
|
|
if (sm && typeof sm.on === 'function') {
|
|
sm.on('v3:live-performance-state', onPerformanceState);
|
|
sm.on('song:loaded', () => {
|
|
if (_active) syncInstrumentPov();
|
|
});
|
|
sm.on('arrangement:changed', () => {
|
|
if (_active) syncInstrumentPov();
|
|
});
|
|
sm.on('song:arrangement-changed', () => {
|
|
if (_active) syncInstrumentPov();
|
|
});
|
|
sm.on('viz:renderer:ready', () => {
|
|
if (isVenueViz()) activate();
|
|
else deactivate();
|
|
});
|
|
sm.on('viz:reverted', () => deactivate());
|
|
}
|
|
if (isVenueViz()) activate();
|
|
}
|
|
|
|
function getState() {
|
|
const h3d = readH3dState();
|
|
const povApi = root && root.v3VenueInstrumentPov;
|
|
const arrangement = readArrangementSignal();
|
|
const instrumentPov = povApi && typeof povApi.resolveVenueInstrumentPov === 'function'
|
|
? povApi.resolveVenueInstrumentPov(arrangement)
|
|
: 'guitar';
|
|
return {
|
|
active: _active,
|
|
themeId: THEME_ID,
|
|
assetBase: ASSET_BASE,
|
|
arrangement,
|
|
instrumentPov,
|
|
assetsLoaded: _assetsLoaded || !!(h3d && h3d.assetsLoaded),
|
|
loadFailed: _loadFailed || !!(h3d && h3d.loadFailed),
|
|
mood: _lastMood,
|
|
h3dVenueState: h3d,
|
|
isVenueViz: isVenueViz(),
|
|
};
|
|
}
|
|
|
|
function shouldShowDomPlaceholder() {
|
|
// V2: no on-screen construction badge during Venue playback.
|
|
return false;
|
|
}
|
|
|
|
const api = {
|
|
THEME_ID,
|
|
ASSET_BASE,
|
|
BG_PLATE,
|
|
BG_PLATE_WEBP,
|
|
activate,
|
|
deactivate,
|
|
syncViz,
|
|
onAssetsLoaded,
|
|
onAssetsFailed,
|
|
onPerformanceState,
|
|
bindRuntime,
|
|
getState,
|
|
syncInstrumentPov,
|
|
syncVenueMotion,
|
|
readArrangementSignal,
|
|
shouldShowDomPlaceholder,
|
|
isSceneLoaded: () => {
|
|
if (_assetsLoaded) return true;
|
|
const h3d = readH3dState();
|
|
return !!(h3d && h3d.assetsLoaded);
|
|
},
|
|
};
|
|
|
|
if (root) root.v3VenueScene3d = api;
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
|
|
if (typeof document !== 'undefined') {
|
|
const boot = () => bindRuntime();
|
|
// `defer` runs this at readyState 'interactive' — later scripts have not
|
|
// evaluated yet, so wait for DOMContentLoaded (see static/v3/index.html).
|
|
if (document.readyState !== 'complete') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
}
|
|
}(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null)));
|