1/**2 * Package-owned reload budget for framework-driven page reloads.3 *4 * The runtime reloads the document at two well-defined boundaries (sync5 * election and principal replacement on SharedWorker browsers). A device6 * whose persisted state contradicts itself can turn those boundaries into a7 * reload cycle. The budget counts framework-driven reloads in session8 * storage — per tab, surviving each reload — and a settled boot clears it,9 * so only an unbroken reload-without-ready sequence ever reaches the limit.10 *11 * @module12 */1314import { anchorAppId } from "./data-sink.ts";1516/** The storage shape the budget needs; tests inject a plain map-backed one. */17export type ReloadCounterStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;1819const reloadCountKey = `lofi:reload-count:${anchorAppId}`;2021// Two consecutive framework reloads without a settled boot are legitimate22// (election, then e.g. an immediate principal replacement); the third is a23// cycle.24const maxReloadAttempts = 3;2526function defaultStorage(): ReloadCounterStorage | null {27 try {28 return typeof sessionStorage === "undefined" ? null : sessionStorage;29 } catch {30 return null;31 }32}3334/**35 * Records one framework-driven reload attempt and answers whether it may36 * proceed. Without session storage the budget degrades open — every attempt37 * is allowed — because there is then also no cross-reload state to loop on.38 */39export function noteReloadAttempt(40 storage: ReloadCounterStorage | null = defaultStorage(),41): { allowed: boolean; count: number } {42 if (storage === null) return { allowed: true, count: 0 };43 try {44 const previous = Number(storage.getItem(reloadCountKey) ?? "0");45 const count = (Number.isFinite(previous) && previous >= 0 ? previous : 0) + 1;46 storage.setItem(reloadCountKey, String(count));47 return { allowed: count < maxReloadAttempts, count };48 } catch {49 return { allowed: true, count: 0 };50 }51}5253/** Clears the budget — called when a boot settles into a working runtime. */54export function resetReloadAttempts(55 storage: ReloadCounterStorage | null = defaultStorage(),56): void {57 if (storage === null) return;58 try {59 storage.removeItem(reloadCountKey);60 } catch {61 // A storage failure only delays the reset until the next settled boot.62 }63}