"use strict"; (function registerStoragePlugin() { const storageKey = "tdkpin.save.v1"; 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 { return window.localStorage; } catch (_error) { return null; } } function sendSavedDataToRust() { wasm_exports.tdkpin_browser_storage_clear(); const storage = browserStorage(); let saved = null; try { saved = storage?.getItem(storageKey); } catch (_error) { saved = null; } if (saved !== null && saved !== undefined) { for (const byte of encoder.encode(saved)) { wasm_exports.tdkpin_browser_storage_push(byte); } } wasm_exports.tdkpin_browser_storage_finish(); } function flushRustSave() { const revision = wasm_exports.tdkpin_browser_storage_save_revision(); if (revision === lastRevision) { return; } const bytes = new Uint8Array( wasm_exports.tdkpin_browser_storage_save_length(), ); for (let index = 0; index < bytes.length; index += 1) { bytes[index] = wasm_exports.tdkpin_browser_storage_save_byte(index); } const storage = browserStorage(); try { storage?.setItem(storageKey, decoder.decode(bytes)); } catch (_error) { // Private browsing or a full quota should not stop the game loop. } wasm_exports.tdkpin_browser_storage_save_ack(); lastRevision = revision; } function deliverHighScores(json) { wasm_exports.tdkpin_browser_high_scores_clear(); for (const byte of encoder.encode(json)) { wasm_exports.tdkpin_browser_high_scores_push(byte); } wasm_exports.tdkpin_browser_high_scores_finish(); } async function fetchHighScores() { const generation = ++highScoreGeneration; try { const response = await fetch(highScoreApi, { cache: "no-store", headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(`high-score request failed (${response.status})`); } const json = await response.text(); if (generation === highScoreGeneration) { deliverHighScores(json); } } catch (error) { console.warn("shared high scores unavailable; using local scores", error); } } 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; } const bytes = new Uint8Array( wasm_exports.tdkpin_browser_high_score_length(), ); for (let index = 0; index < bytes.length; index += 1) { bytes[index] = wasm_exports.tdkpin_browser_high_score_byte(index); } highScoreRequestInFlight = true; let retryAfterMs = null; try { const response = await fetch(highScoreApi, { method: "POST", headers: { "Content-Type": "application/json" }, body: decoder.decode(bytes), keepalive: true, }); if (!response.ok) { retryAfterMs = retryAfterDelay(response); throw new Error(`high-score submission failed (${response.status})`); } const json = await response.text(); highScoreGeneration += 1; deliverHighScores(json); 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; } } finally { highScoreRequestInFlight = false; } } function onInit() { sendSavedDataToRust(); lastRevision = wasm_exports.tdkpin_browser_storage_save_revision(); void fetchHighScores(); window.setInterval(flushRustSave, 50); window.setInterval(() => void flushHighScoreSubmission(), 50); window.addEventListener("beforeunload", flushRustSave); } miniquad_add_plugin({ name: "tdkpin_storage", on_init: onInit, version: 1, }); })();