Add event-driven state sync with ETag optimization
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
updatePhaseNav,
|
||||
configureUiRuntime,
|
||||
} from "./js/ui.js";
|
||||
import { api } from "./js/api.js";
|
||||
import { loadSuggestData, loadVoteData, refreshPhaseData } from "./js/data.js";
|
||||
import { setupAuthHandlers } from "./js/app-auth-handlers.js";
|
||||
import { setupAdminHandlers } from "./js/app-admin-handlers.js";
|
||||
@@ -29,11 +30,16 @@ import { setupVoteNavigationHandlers } from "./js/app-vote-nav-handlers.js";
|
||||
|
||||
const REFRESH_MIN_MS = 3000;
|
||||
const REFRESH_MAX_MS = 20000;
|
||||
const EVENTS_RECONNECT_MIN_MS = 1000;
|
||||
const EVENTS_RECONNECT_MAX_MS = 15000;
|
||||
let refreshInFlight = null;
|
||||
let refreshTimerId = null;
|
||||
let refreshSchedulerStarted = false;
|
||||
let unchangedRefreshCycles = 0;
|
||||
let nextRefreshDelayMs = REFRESH_MIN_MS;
|
||||
let stateEventSource = null;
|
||||
let eventsReconnectTimerId = null;
|
||||
let eventsReconnectDelayMs = EVENTS_RECONNECT_MIN_MS;
|
||||
|
||||
async function runSerializedRefresh() {
|
||||
if (refreshInFlight) return refreshInFlight;
|
||||
@@ -47,13 +53,81 @@ async function refreshWithUiErrorHandling() {
|
||||
try {
|
||||
const changed = await runSerializedRefresh();
|
||||
updateRefreshCadence(changed === true);
|
||||
if (state.isAuthenticated) {
|
||||
ensureStateEventStream();
|
||||
} else {
|
||||
closeStateEventStream();
|
||||
}
|
||||
} catch (err) {
|
||||
// Back off after transient failures to avoid hammering server/dependencies.
|
||||
nextRefreshDelayMs = Math.min(nextRefreshDelayMs * 2, REFRESH_MAX_MS);
|
||||
if (!handleAuthError(err, clearUserState)) toast(err.message, true);
|
||||
if (handleAuthError(err, clearUserState)) {
|
||||
closeStateEventStream();
|
||||
return;
|
||||
}
|
||||
|
||||
toast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function closeStateEventStream() {
|
||||
if (eventsReconnectTimerId !== null) {
|
||||
window.clearTimeout(eventsReconnectTimerId);
|
||||
eventsReconnectTimerId = null;
|
||||
}
|
||||
|
||||
if (stateEventSource) {
|
||||
stateEventSource.close();
|
||||
stateEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleStateEventReconnect() {
|
||||
if (eventsReconnectTimerId !== null || !state.isAuthenticated) return;
|
||||
|
||||
eventsReconnectTimerId = window.setTimeout(() => {
|
||||
eventsReconnectTimerId = null;
|
||||
ensureStateEventStream();
|
||||
}, eventsReconnectDelayMs);
|
||||
|
||||
eventsReconnectDelayMs = Math.min(
|
||||
Math.round(eventsReconnectDelayMs * 1.8),
|
||||
EVENTS_RECONNECT_MAX_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function ensureStateEventStream() {
|
||||
if (!state.isAuthenticated || typeof window.EventSource === "undefined") {
|
||||
closeStateEventStream();
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateEventSource) return;
|
||||
|
||||
stateEventSource = new EventSource(api.stateEventsUrl(), {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
stateEventSource.onopen = () => {
|
||||
eventsReconnectDelayMs = EVENTS_RECONNECT_MIN_MS;
|
||||
};
|
||||
|
||||
stateEventSource.onerror = () => {
|
||||
if (!stateEventSource) return;
|
||||
stateEventSource.close();
|
||||
stateEventSource = null;
|
||||
scheduleStateEventReconnect();
|
||||
};
|
||||
|
||||
stateEventSource.addEventListener("state", () => {
|
||||
unchangedRefreshCycles = 0;
|
||||
nextRefreshDelayMs = baseRefreshDelayForPhase();
|
||||
if (!document.hidden && !state.adminStatusSelectActive) {
|
||||
refreshWithUiErrorHandling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleNextRefresh() {
|
||||
refreshTimerId = window.setTimeout(async () => {
|
||||
if (!document.hidden && !state.adminStatusSelectActive) {
|
||||
@@ -119,6 +193,9 @@ function setupHandlers() {
|
||||
setupAdminHandlers({ runSerializedRefresh });
|
||||
setupVoteNavigationHandlers({ runSerializedRefresh });
|
||||
setupLanguageSwitchers();
|
||||
document.getElementById("logout")?.addEventListener("click", () => {
|
||||
closeStateEventStream();
|
||||
});
|
||||
|
||||
onLanguageChange(() => {
|
||||
updateLanguageButtons();
|
||||
|
||||
Reference in New Issue
Block a user