83f792ca39
Правки по ревью #499. Владелец выбрал вариант C по вопросу потери несохранённого ввода. F1 (reload по навигации): убрал ветку visibilitychange→reload И немедленный reload скрытой-при-получении вкладки — visibility больше не влияет на авто-reload вообще. На реальном mismatch: баннер (кнопка «Обновить» — немедленный reload под тем же one-shot гардом) + модульный флаг pendingNavReload. Хук useVersionReloadOnNavigation (useLocation + useEffect по location.key, пропуск первого рендера через useRef; react-router v7 BrowserRouter компонентный, объекта роутера нет — подписка изнутри дерева) на СЛЕДУЮЩЕЙ навигации зовёт consumeNavigationReload → performAutoReload (mark-перед-reload, при spent/непишущемся storage — только баннер). Скрытая вкладка идёт тем же путём (C единообразно). chunk-load-error-boundary reload не тронут — остаётся бэкапом для не-навигирующей вкладки. Так reload в безопасной точке (юзер и так уходит со страницы), а не когда свернул с недописанным комментарием. F2 (лог-след): общий recordReloadBreadcrumb/takeReloadBreadcrumb (sessionStorage, переживает reload). performAutoReload пишет breadcrumb {path:proactive,server, client} + console.warn перед reload; chunk-boundary пишет {path:chunk-boundary}; surfacePreviousReloadBreadcrumb на маунте UserProvider логирует прошлый след. F3 (CHANGELOG + AGENTS.md): user-facing запись про version-coherence + строка про артефакт client/dist/version.json. Тесты: vitest version-coherence+reload-guard+guarded-reload 23 (переписан на MemoryRouter+useNavigate: reload РОВНО раз на первой навигации после mismatch, НЕ на 2-й навигации, НЕ на 2-м mismatch one-shot, НЕ от ухода в фон; скрытая вкладка — только на навигации; Update — сразу; banner-only/write-fail — никогда). Шире: features/user 34. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
// Shared one-shot auto-reload guard.
|
|
//
|
|
// Both auto-reload paths — the reactive chunk-load-error-boundary (recovers
|
|
// AFTER a stale lazy chunk 404s) and the proactive version-coherence feature
|
|
// (reloads BEFORE the tab hits a stale chunk) — go through these functions so
|
|
// they share ONE session-scoped flag. Net guarantee: at most a single
|
|
// automatic reload per browser session across both paths. Once the flag is set
|
|
// (or sessionStorage is unavailable), every further mismatch degrades to a
|
|
// manual banner/UI — no reload loop under permanent skew, node oscillation, or
|
|
// a disabled storage.
|
|
const RELOAD_FLAG = "chunk-reload-attempted";
|
|
|
|
/**
|
|
* Has an automatic reload already been performed (or attempted) this session?
|
|
*
|
|
* A storage read error (private mode / disabled) is reported as `true` so the
|
|
* caller fails toward NOT reloading — an unguarded loop is worse than a stale
|
|
* tab the user can reload manually.
|
|
*/
|
|
export function hasAutoReloaded(): boolean {
|
|
try {
|
|
return sessionStorage.getItem(RELOAD_FLAG) !== null;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record that an automatic reload is being performed this session.
|
|
*
|
|
* Returns whether the write succeeded. A `false` return (storage unavailable)
|
|
* means the caller MUST NOT reload — otherwise the flag would never stick and
|
|
* the reload could loop.
|
|
*/
|
|
export function markAutoReloaded(): boolean {
|
|
try {
|
|
sessionStorage.setItem(RELOAD_FLAG, "1");
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Diagnostic breadcrumb for an automatic reload. Written right before
|
|
// window.location.reload() (which clears the console) and read back on the next
|
|
// page load, so a "the tab reloaded itself / it's looping" field report is
|
|
// diagnosable: which path fired (proactive version-coherence vs the reactive
|
|
// chunk-load boundary) and which version pair triggered it. sessionStorage
|
|
// survives a same-tab reload, unlike the console.
|
|
const RELOAD_BREADCRUMB_KEY = "reload-breadcrumb";
|
|
|
|
export type ReloadBreadcrumb = {
|
|
path: "proactive" | "chunk-boundary";
|
|
serverVersion?: string;
|
|
clientVersion?: string;
|
|
at: number;
|
|
};
|
|
|
|
/**
|
|
* Persist a best-effort breadcrumb just before an automatic reload. Failures
|
|
* (storage unavailable) are swallowed — this is diagnostics only and must never
|
|
* block or alter the reload decision.
|
|
*/
|
|
export function recordReloadBreadcrumb(
|
|
entry: Omit<ReloadBreadcrumb, "at">,
|
|
): void {
|
|
try {
|
|
sessionStorage.setItem(
|
|
RELOAD_BREADCRUMB_KEY,
|
|
JSON.stringify({ ...entry, at: Date.now() }),
|
|
);
|
|
} catch {
|
|
// best-effort diagnostics only
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read and clear the breadcrumb left by an auto-reload in the previous page
|
|
* load. Cleared on read so it surfaces exactly once per reload.
|
|
*/
|
|
export function takeReloadBreadcrumb(): ReloadBreadcrumb | null {
|
|
try {
|
|
const raw = sessionStorage.getItem(RELOAD_BREADCRUMB_KEY);
|
|
if (!raw) return null;
|
|
sessionStorage.removeItem(RELOAD_BREADCRUMB_KEY);
|
|
return JSON.parse(raw) as ReloadBreadcrumb;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|