non blocking status loading
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user