diff --git a/tdkpin-rs/web/storage.js b/tdkpin-rs/web/storage.js index f832098..016dfc7 100644 --- a/tdkpin-rs/web/storage.js +++ b/tdkpin-rs/web/storage.js @@ -5,11 +5,15 @@ const encoder = new TextEncoder(); const decoder = new TextDecoder(); const highScoreApi = "/api/highscores"; + const highScoreRetryInitialDelayMs = 250; + const highScoreRetryMaxDelayMs = 30_000; let lastRevision = 0; let lastHighScoreRevision = 0; let highScoreRequestInFlight = false; let highScoreGeneration = 0; let highScoreSubmissionWarningShown = false; + let highScoreRetryDelayMs = highScoreRetryInitialDelayMs; + let highScoreRetryAt = 0; function browserStorage() { try { @@ -86,10 +90,50 @@ } } + function retryAfterDelay(response) { + if (response.status !== 429 && response.status !== 503) { + return null; + } + const value = response.headers.get("Retry-After")?.trim(); + if (!value) { + return null; + } + + const seconds = Number(value); + if (Number.isFinite(seconds) && seconds >= 0) { + const delay = seconds * 1000; + return Number.isFinite(delay) ? delay : null; + } + + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) + ? Math.max(0, timestamp - Date.now()) + : null; + } + + function scheduleHighScoreRetry(retryAfterMs) { + const jitteredDelay = highScoreRetryDelayMs * (0.5 + Math.random()); + const delay = + retryAfterMs === null ? jitteredDelay : retryAfterMs + jitteredDelay; + highScoreRetryAt = Date.now() + delay; + highScoreRetryDelayMs = Math.min( + highScoreRetryDelayMs * 2, + highScoreRetryMaxDelayMs, + ); + } + + function resetHighScoreRetry() { + highScoreRetryDelayMs = highScoreRetryInitialDelayMs; + highScoreRetryAt = 0; + } + async function flushHighScoreSubmission() { if (highScoreRequestInFlight) { return; } + if (Date.now() < highScoreRetryAt) { + return; + } const revision = wasm_exports.tdkpin_browser_high_score_revision(); if (revision === lastHighScoreRevision) { return; @@ -102,6 +146,7 @@ } highScoreRequestInFlight = true; + let retryAfterMs = null; try { const response = await fetch(highScoreApi, { method: "POST", @@ -110,6 +155,7 @@ keepalive: true, }); if (!response.ok) { + retryAfterMs = retryAfterDelay(response); throw new Error(`high-score submission failed (${response.status})`); } const json = await response.text(); @@ -118,7 +164,9 @@ wasm_exports.tdkpin_browser_high_score_ack(revision); lastHighScoreRevision = revision; highScoreSubmissionWarningShown = false; + resetHighScoreRetry(); } catch (error) { + scheduleHighScoreRetry(retryAfterMs); if (!highScoreSubmissionWarningShown) { console.warn("shared high-score submission failed; will retry", error); highScoreSubmissionWarningShown = true;