Serialize refresh scheduling and remove overlap polling
This commit is contained in:
27
REVIEW.md
27
REVIEW.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user