Add event-driven state sync with ETag optimization

This commit is contained in:
2026-02-18 19:58:57 +01:00
parent 5b921063ec
commit 3c7f3d2114
17 changed files with 493 additions and 30 deletions

View File

@@ -18,24 +18,53 @@ async function request(path, { method = "GET", body } = {}) {
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
let msg = `${res.status}`;
try {
const data = await res.json();
msg =
data.error || data.detail || data.title || JSON.stringify(data);
} catch {
/* ignore */
}
const err = new Error(msg);
err.status = res.status;
throw err;
}
if (!res.ok) throw await toApiError(res);
return res.status === 204 ? null : res.json();
}
async function requestState(ifNoneMatch) {
const headers = { ...defaultHeaders };
if (ifNoneMatch) headers["If-None-Match"] = ifNoneMatch;
const res = await fetch(withBase("/api/state"), {
method: "GET",
credentials: "same-origin",
headers,
});
if (res.status === 304) {
return {
notModified: true,
etag: res.headers.get("ETag"),
data: null,
};
}
if (!res.ok) throw await toApiError(res);
return {
notModified: false,
etag: res.headers.get("ETag"),
data: await res.json(),
};
}
async function toApiError(res) {
let msg = `${res.status}`;
try {
const data = await res.json();
msg = data.error || data.detail || data.title || JSON.stringify(data);
} catch {
/* ignore */
}
const err = new Error(msg);
err.status = res.status;
return err;
}
export const api = {
state: () => request("/api/state"),
state: (ifNoneMatch) => requestState(ifNoneMatch),
stateEventsUrl: () => withBase("/api/events/state"),
me: () => request("/api/me"),
authOptions: () => request("/api/auth/options"),
register: (payload) =>

View File

@@ -18,14 +18,27 @@ import {
import { state, clearUserState } from "./state.js";
export async function loadState() {
const [me, stateData] = await Promise.all([api.me(), api.state()]);
const stateResponse = await api.state(state.stateEtag);
if (stateResponse?.etag) state.stateEtag = stateResponse.etag;
if (stateResponse?.notModified) return false;
const stateData = stateResponse.data;
state.isAuthenticated = true;
state.me = me;
state.hasJoker = me.hasJoker ?? false;
state.me = {
id: stateData.id,
username: stateData.username,
displayName: stateData.displayName,
isAdmin: stateData.isAdmin,
isOwner: stateData.isOwner,
currentPhase: stateData.currentPhase,
votesFinal: stateData.votesFinal,
hasJoker: stateData.hasJoker,
};
state.hasJoker = stateData.hasJoker ?? false;
state.prevPhase = state.phase;
state.phase = stateData.currentPhase;
state.resultsOpen = stateData.resultsOpen;
state.votesFinal = stateData.votesFinal ?? me?.votesFinal ?? false;
state.votesFinal = stateData.votesFinal ?? false;
state.counts = stateData;
if (state.prevPhase !== state.phase && state.phase === "Vote") {
state.votesRendered = false;
@@ -34,6 +47,7 @@ export async function loadState() {
renderWelcome();
renderPhasePill();
renderCounts();
return true;
}
export async function loadSuggestData() {
@@ -105,7 +119,19 @@ export async function refreshPhaseData() {
try {
const prevPhase = state.phase;
const prevResultsOpen = state.resultsOpen;
await loadState();
const stateChanged = await loadState();
const adminCard = document.getElementById("admin-card");
const adminPanelVisible =
!!adminCard && !adminCard.classList.contains("hidden");
if (!stateChanged) {
if (state.me?.isAdmin && adminPanelVisible) {
state.adminVoteStatus = await adminApi.voteStatus();
}
updatePhaseNav();
return false;
}
await Promise.all([
loadSuggestData(),
loadSuggestionsData(),
@@ -117,9 +143,6 @@ export async function refreshPhaseData() {
state.votesRendered = false;
await loadVoteData();
}
const adminCard = document.getElementById("admin-card");
const adminPanelVisible =
!!adminCard && !adminCard.classList.contains("hidden");
if (state.me?.isAdmin && adminPanelVisible) {
state.adminVoteStatus = await adminApi.voteStatus();
}

View File

@@ -17,6 +17,7 @@ export const state = {
votesRendered: false,
adminVoteStatus: null,
adminStatusSelectActive: false,
stateEtag: null,
};
export function clearUserState() {
@@ -34,7 +35,9 @@ export function clearUserState() {
state.myVotes = [];
state.results = [];
state.votesRendered = false;
state.adminVoteStatus = null;
state.adminStatusSelectActive = false;
state.stateEtag = null;
const adminCard = document.getElementById("admin-card");
if (adminCard) adminCard.classList.add("hidden");
}