Serialize refresh scheduling and remove overlap polling

This commit is contained in:
2026-02-07 01:47:36 +01:00
parent 78701cebf2
commit 5e84686678
2 changed files with 66 additions and 40 deletions

View File

@@ -11,19 +11,15 @@ Active maintainability risks (priority order):
- Cross-feature coupling still exists via shared mutable state usage across UI/data modules (`wwwroot/js/ui.js:180`, `wwwroot/js/ui.js:401`, `wwwroot/js/ui.js:622`, `wwwroot/js/data.js:82`). - Cross-feature coupling still exists via shared mutable state usage across UI/data modules (`wwwroot/js/ui.js:180`, `wwwroot/js/ui.js:401`, `wwwroot/js/ui.js:622`, `wwwroot/js/data.js:82`).
- Impact: high regression surface and expensive refactors even after removing global `window` bridges. - Impact: high regression surface and expensive refactors even after removing global `window` bridges.
2. Frontend refresh scheduling can overlap async work (High) 2. Legacy phase path remains in active code (Medium)
- Refresh loop is still interval-driven in `wwwroot/app.js:290` with async work rooted in `wwwroot/js/data.js:82`.
- Impact: overlapping requests can race state updates and produce noisy UI transitions.
3. Legacy phase path remains in active code (Medium)
- `Reveal` is still present in `Domain/Phase.cs:6` and compatibility branches remain in `Endpoints/StateEndpoints.cs:102` and `Endpoints/StateEndpoints.cs:111`. - `Reveal` is still present in `Domain/Phase.cs:6` and compatibility branches remain in `Endpoints/StateEndpoints.cs:102` and `Endpoints/StateEndpoints.cs:111`.
- Impact: extra cognitive load and more branches to reason about during phase-flow changes. - Impact: extra cognitive load and more branches to reason about during phase-flow changes.
4. Unauthenticated 401 response shape is still framework-driven (Medium) 3. Unauthenticated 401 response shape is still framework-driven (Medium)
- Endpoint and filter unauthorized responses are standardized when app logic executes (`Infrastructure/AdminOnlyFilter.cs:15`, `Infrastructure/PhaseRequirementFilter.cs:15`, `Endpoints/SuggestEndpoints.cs:18`), but anonymous challenge responses remain middleware-controlled (`GameList.Tests/StateTests.cs:214`). - Endpoint and filter unauthorized responses are standardized when app logic executes (`Infrastructure/AdminOnlyFilter.cs:15`, `Infrastructure/PhaseRequirementFilter.cs:15`, `Endpoints/SuggestEndpoints.cs:18`), but anonymous challenge responses remain middleware-controlled (`GameList.Tests/StateTests.cs:214`).
- Impact: clients must tolerate both app-produced problem payloads and framework challenge responses. - Impact: clients must tolerate both app-produced problem payloads and framework challenge responses.
5. Static analysis and frontend lint guardrails are still missing (Medium) 4. Static analysis and frontend lint guardrails are still missing (Medium)
- CI currently gates restore/build/test only (`.github/workflows/ci.yml:23`-`.github/workflows/ci.yml:29`). - CI currently gates restore/build/test only (`.github/workflows/ci.yml:23`-`.github/workflows/ci.yml:29`).
- Impact: style drift and low-signal warnings can enter the codebase undetected. - Impact: style drift and low-signal warnings can enter the codebase undetected.
@@ -37,14 +33,6 @@ Active maintainability risks (priority order):
- Effort / Risk: `L / Med`. - Effort / Risk: `L / Med`.
- Dependencies (if any): none. - Dependencies (if any): none.
[P1] Replace uncontrolled polling with serialized refresh scheduling
- Problem: Severity `Medium`, Category `Reliability/Complexity`. Fixed 4-second polling can overlap when requests take longer than interval.
- Evidence: `wwwroot/app.js:290`, `wwwroot/js/data.js:82`.
- Recommendation: introduce a single-flight scheduler with backpressure, visibility pause/resume, and explicit trigger support.
- Acceptance criteria (testable): at most one in-flight refresh at a time; no duplicate refresh overlap during induced latency.
- Effort / Risk: `M / Low`.
- Dependencies (if any): none.
[P1] Remove legacy `Reveal` phase compatibility branches [P1] Remove legacy `Reveal` phase compatibility branches
- Problem: Severity `Medium`, Category `Complexity`. Legacy phase compatibility logic is still present in runtime paths. - Problem: Severity `Medium`, Category `Complexity`. Legacy phase compatibility logic is still present in runtime paths.
- Evidence: `Domain/Phase.cs:6`, `Endpoints/StateEndpoints.cs:102`, `Endpoints/StateEndpoints.cs:111`, `wwwroot/js/data.js:30`. - Evidence: `Domain/Phase.cs:6`, `Endpoints/StateEndpoints.cs:102`, `Endpoints/StateEndpoints.cs:111`, `wwwroot/js/data.js:30`.
@@ -80,11 +68,10 @@ Active maintainability risks (priority order):
## C) Suggested execution order ## C) Suggested execution order
1. Decompose `ui.js` by feature and keep orchestration thin. 1. Decompose `ui.js` by feature and keep orchestration thin.
2. Introduce serialized refresh scheduler. 2. Remove `Reveal` phase compatibility branches.
3. Remove `Reveal` phase compatibility branches. 3. Normalize/declare unauthenticated 401 contract behavior.
4. Normalize/declare unauthenticated 401 contract behavior. 4. Add analyzers + JS lint gates in CI.
5. Add analyzers + JS lint gates in CI. 5. Externalize i18n/FAQ assets.
6. Externalize i18n/FAQ assets.
## D) Guardrails ## D) Guardrails

View File

@@ -30,8 +30,55 @@ import {
refreshPhaseData, refreshPhaseData,
} from "./js/data.js"; } from "./js/data.js";
initI18n(); initI18n();
const REFRESH_INTERVAL_MS = 4000;
let refreshInFlight = null;
let refreshTimerId = null;
let refreshSchedulerStarted = false;
async function runSerializedRefresh() {
if (refreshInFlight) return refreshInFlight;
refreshInFlight = refreshPhaseData().finally(() => {
refreshInFlight = null;
});
return refreshInFlight;
}
async function refreshWithUiErrorHandling() {
try {
await runSerializedRefresh();
} catch (err) {
if (!handleAuthError(err, clearUserState)) toast(err.message, true);
}
}
function scheduleNextRefresh() {
refreshTimerId = window.setTimeout(async () => {
if (!document.hidden) {
await refreshWithUiErrorHandling();
}
scheduleNextRefresh();
}, REFRESH_INTERVAL_MS);
}
function startRefreshScheduler() {
if (refreshSchedulerStarted) return;
refreshSchedulerStarted = true;
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
refreshWithUiErrorHandling();
}
});
if (refreshTimerId !== null) {
window.clearTimeout(refreshTimerId);
}
scheduleNextRefresh();
}
configureUiRuntime({ configureUiRuntime({
refreshPhaseData, refreshPhaseData: runSerializedRefresh,
loadSuggestData, loadSuggestData,
loadVoteData, loadVoteData,
handleAuthError: (err) => handleAuthError(err, clearUserState), handleAuthError: (err) => handleAuthError(err, clearUserState),
@@ -105,7 +152,7 @@ function setupHandlers() {
setSavedUsername(username); setSavedUsername(username);
state.isAuthenticated = true; state.isAuthenticated = true;
setAuthUI(true); setAuthUI(true);
await refreshPhaseData(); await runSerializedRefresh();
toast(t("toast.loggedIn")); toast(t("toast.loggedIn"));
} catch (err) { } catch (err) {
if (err?.status === 401) return toast(t("auth.invalidCredentials"), true); if (err?.status === 401) return toast(t("auth.invalidCredentials"), true);
@@ -132,7 +179,7 @@ function setupHandlers() {
setSavedUsername(username); setSavedUsername(username);
state.isAuthenticated = true; state.isAuthenticated = true;
setAuthUI(true); setAuthUI(true);
await refreshPhaseData(); await runSerializedRefresh();
toast(t("toast.registered")); toast(t("toast.registered"));
} catch (err) { } catch (err) {
if (handleAuthError(err, clearUserState)) return; if (handleAuthError(err, clearUserState)) return;
@@ -214,7 +261,7 @@ function setupHandlers() {
} }
renderPhasePill(); renderPhasePill();
toast(t("admin.resultsUpdated")); toast(t("admin.resultsUpdated"));
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
e.target.checked = !desired; e.target.checked = !desired;
toast(err.message, true); toast(err.message, true);
@@ -233,7 +280,7 @@ function setupHandlers() {
try { try {
await adminApi.linkSuggestions(source, target); await adminApi.linkSuggestions(source, target);
toast(t("admin.linkDone")); toast(t("admin.linkDone"));
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
@@ -250,7 +297,7 @@ function setupHandlers() {
try { try {
await adminApi.grantJoker(playerId); await adminApi.grantJoker(playerId);
toast(t("admin.jokerGranted")); toast(t("admin.jokerGranted"));
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
@@ -266,7 +313,7 @@ function setupHandlers() {
await adminApi.deletePlayer(playerId); await adminApi.deletePlayer(playerId);
toast(t("admin.deleteDone")); toast(t("admin.deleteDone"));
close(); close();
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
@@ -281,7 +328,7 @@ async function adminAction(fn, successMessage) {
try { try {
await fn(); await fn();
toast(successMessage); toast(successMessage);
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
@@ -289,16 +336,8 @@ async function adminAction(fn, successMessage) {
async function main() { async function main() {
setupHandlers(); setupHandlers();
try { await refreshWithUiErrorHandling();
await refreshPhaseData(); startRefreshScheduler();
} catch (err) {
toast(err.message, true);
}
setInterval(() => {
refreshPhaseData().catch((err) => {
if (!handleAuthError(err, clearUserState)) toast(err.message, true);
});
}, 4000);
} }
main(); main();
@@ -358,7 +397,7 @@ function bindNavButtons() {
state.resultsOpen = resp.resultsOpen ?? state.resultsOpen; state.resultsOpen = resp.resultsOpen ?? state.resultsOpen;
state.votesRendered = false; state.votesRendered = false;
renderPhasePill(); renderPhasePill();
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
@@ -376,7 +415,7 @@ function bindNavButtons() {
state.resultsOpen = resp.resultsOpen ?? state.resultsOpen; state.resultsOpen = resp.resultsOpen ?? state.resultsOpen;
state.votesRendered = false; state.votesRendered = false;
renderPhasePill(); renderPhasePill();
await refreshPhaseData(); await runSerializedRefresh();
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }