non blocking status loading

This commit is contained in:
Simon
2026-06-24 20:48:49 +00:00
parent b931765c06
commit 455e5cf8d8
2 changed files with 114 additions and 33 deletions

View File

@@ -34,6 +34,12 @@ window.App = window.App || {};
await App.videos.loadVideos();
App.favorites.syncButtons();
// The UI above is rendered entirely from the last known status cached in
// localStorage, so startup never blocks on (or breaks because of) a slow
// or failing status endpoint. Now fetch fresh status in the background and
// reconcile the UI with whatever comes back.
App.storage.refreshServerStatusInBackground();
}
initApp();

View File

@@ -174,7 +174,11 @@ App.session = App.session || {};
return selected;
};
// Ensures defaults exist and refreshes server status.
// Ensures defaults exist and establishes a session from cached status.
// Intentionally does NOT touch the network: the last known status of every
// server is persisted in localStorage, so the UI can render instantly from
// it. Fresh status is fetched separately (and non-blockingly) via
// refreshServerStatusInBackground().
App.storage.ensureDefaults = async function() {
if (!localStorage.getItem('config')) {
localStorage.setItem('config', JSON.stringify({
@@ -200,13 +204,79 @@ App.session = App.session || {};
if (!localStorage.getItem(FEED_END_BEHAVIOR_KEY)) {
localStorage.setItem(FEED_END_BEHAVIOR_KEY, 'loop');
}
await App.storage.initializeServerStatus();
App.storage.ensureSessionFromCache();
};
// Fetches server status and keeps the session pointing to a valid channel/options.
// A stable fingerprint of which server/channel a session targets, used to
// decide whether a status refresh actually changed what's being shown (and
// thus whether videos need reloading).
function sessionSignature(session) {
if (!session) return '';
return `${session.server}::${session.channel ? session.channel.id : ''}`;
}
// Builds a session pointing at a valid channel/options using ONLY the status
// data already cached in `config` (no network). Returns the session object,
// or null if no server in the config currently exposes any channels.
App.session.buildSessionFromCache = function(config) {
if (!config || !Array.isArray(config.servers) || config.servers.length === 0) return null;
const serverKeys = config.servers.map((serverObj) => Object.keys(serverObj)[0]);
const existingSession = App.storage.getSession();
const selectedServerKey = existingSession && serverKeys.includes(existingSession.server)
? existingSession.server
: serverKeys[0];
const serverEntry = config.servers.find((serverObj) => Object.keys(serverObj)[0] === selectedServerKey);
const serverData = serverEntry ? serverEntry[selectedServerKey] : null;
if (!serverData || !Array.isArray(serverData.channels) || serverData.channels.length === 0) {
return null;
}
const prefs = App.storage.getPreferences();
const serverPrefs = prefs[selectedServerKey] || {};
const channel = App.session.resolveChannelById(serverData, serverPrefs.channelId) || serverData.channels[0];
const savedOptions = serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[channel.id] : null;
const options = savedOptions ? App.session.hydrateOptions(channel, savedOptions) : App.session.buildDefaultOptions(channel);
return {
server: selectedServerKey,
channel: channel,
options: options,
};
};
// Ensures the stored session points at a channel that still exists in the
// cached status, rebuilding it from cache if necessary. Never clears a valid
// selection. Returns true if a usable session exists afterwards.
App.storage.ensureSessionFromCache = function() {
const config = App.storage.getConfig();
const serverKeys = (config.servers || []).map((serverObj) => Object.keys(serverObj)[0]);
const existingSession = App.storage.getSession();
// Leave a still-valid session untouched so we don't disturb the user's
// current server/channel selection on refresh.
if (existingSession && existingSession.channel && serverKeys.includes(existingSession.server)) {
const serverEntry = config.servers.find((serverObj) => Object.keys(serverObj)[0] === existingSession.server);
const serverData = serverEntry ? serverEntry[existingSession.server] : null;
if (serverData && App.session.resolveChannelById(serverData, existingSession.channel.id)) {
return true;
}
}
const sessionData = App.session.buildSessionFromCache(config);
if (sessionData) {
App.storage.setSession(sessionData);
App.session.savePreference(sessionData);
return true;
}
return false;
};
// Fetches fresh server status and merges it into the cached config. Crucially,
// a failed status request preserves the server's LAST KNOWN status (channels,
// groups, etc.) instead of wiping it -- so a flaky/down status endpoint can no
// longer brick the app. Returns true if the active session's target changed
// (e.g. channels appeared for the first time), signalling a video reload.
App.storage.initializeServerStatus = async function() {
const config = JSON.parse(localStorage.getItem('config'));
if (!config || !config.servers) return;
if (!config || !config.servers) return false;
const fetchDirectStatus = async (server) => {
const directUrl = `${server.replace(/\/$/, '')}/api/status`;
@@ -231,6 +301,7 @@ App.session = App.session || {};
const statusPromises = config.servers.map(async (serverObj) => {
const server = Object.keys(serverObj)[0];
const prior = serverObj[server];
try {
// Try a direct request first, then fall back to the server-side proxy.
try {
@@ -239,41 +310,45 @@ App.session = App.session || {};
serverObj[server] = await fetchProxiedStatus(server);
}
} catch (err) {
serverObj[server] = {
online: false,
channels: []
};
// The request failed. Keep the last known good status so the user
// doesn't lose their channels when the status endpoint is down;
// just flag it offline. Only fall back to an empty stub when we've
// never successfully fetched this server.
if (prior && Array.isArray(prior.channels) && prior.channels.length > 0) {
serverObj[server] = Object.assign({}, prior, { online: false });
} else {
serverObj[server] = {
online: false,
channels: []
};
}
}
});
await Promise.all(statusPromises);
localStorage.setItem('config', JSON.stringify(config));
const existingSession = App.storage.getSession();
const serverKeys = config.servers.map((serverObj) => Object.keys(serverObj)[0]);
if (serverKeys.length === 0) return;
const selectedServerKey = existingSession && serverKeys.includes(existingSession.server)
? existingSession.server
: serverKeys[0];
const serverEntry = config.servers.find((serverObj) => Object.keys(serverObj)[0] === selectedServerKey);
const serverData = serverEntry ? serverEntry[selectedServerKey] : null;
const before = sessionSignature(App.storage.getSession());
App.storage.ensureSessionFromCache();
const after = sessionSignature(App.storage.getSession());
return before !== after;
};
if (serverData && serverData.channels && serverData.channels.length > 0) {
const prefs = App.storage.getPreferences();
const serverPrefs = prefs[selectedServerKey] || {};
const preferredChannelId = serverPrefs.channelId;
const channel = App.session.resolveChannelById(serverData, preferredChannelId) || serverData.channels[0];
const savedOptions = serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[channel.id] : null;
const options = savedOptions ? App.session.hydrateOptions(channel, savedOptions) : App.session.buildDefaultOptions(channel);
const sessionData = {
server: selectedServerKey,
channel: channel,
options: options,
};
App.storage.setSession(sessionData);
App.session.savePreference(sessionData);
}
// Refreshes server status without blocking; updates the menu and reloads
// videos only if the refresh actually changed the active selection. Safe to
// fire-and-forget during startup so the UI renders from cache immediately.
App.storage.refreshServerStatusInBackground = function() {
return App.storage.initializeServerStatus()
.then((changed) => {
if (App.ui && typeof App.ui.renderMenu === 'function') {
App.ui.renderMenu();
}
if (changed && App.videos && typeof App.videos.resetAndReload === 'function') {
App.videos.resetAndReload();
}
})
.catch((err) => {
console.error('Background status refresh failed:', err);
});
};
})();