mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(pwa): pause the version check while the tab is hidden
The version poller asked `/api/version` every 60 s from a plain timer, also in a background tab or a minimised window, for as long as the browser let timers run. It was the largest standing poll and the only one that kept going while hidden, although the poll notes describe it as the same shape as the others. A tick now asks only while the tab is in view, and a tab that comes back after a full interval away checks once at once, so a release still surfaces on return.
This commit is contained in:
1 parent
9b2c68fbe5
commit
a6fdd48058
2 files changed
+60
-8
No files matched your search
@@ -15,7 +15,11 @@
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { cachesToEvict, resolveVersionPollDecision } from "../version-poller";
|
||||
import {
|
||||
cachesToEvict,
|
||||
resolveVersionPollDecision,
|
||||
versionCheckDueOnReturn,
|
||||
} from "../version-poller";
|
||||
|
||||
describe("resolveVersionPollDecision", () => {
|
||||
it("is up-to-date when live matches the shell", () => {
|
||||
@@ -172,3 +176,20 @@ describe("cachesToEvict", () => {
|
||||
expect(cachesToEvict(keys, "1.43.0")).not.toContain("another-app-cache");
|
||||
});
|
||||
});
|
||||
|
||||
describe("versionCheckDueOnReturn", () => {
|
||||
const NOW = 1_000_000_000;
|
||||
|
||||
it("asks at once when the tab comes back after a full interval away", () => {
|
||||
expect(versionCheckDueOnReturn("visible", NOW - 60_000, NOW)).toBe(true);
|
||||
expect(versionCheckDueOnReturn("visible", 0, NOW)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not ask again on a quick switch back", () => {
|
||||
expect(versionCheckDueOnReturn("visible", NOW - 5_000, NOW)).toBe(false);
|
||||
});
|
||||
|
||||
it("never asks while the tab is hidden", () => {
|
||||
expect(versionCheckDueOnReturn("hidden", 0, NOW)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,8 @@ import { HEALTHLOG_CACHE_NAME_RE } from "@/lib/pwa/sw-cache-policy";
|
||||
* the user trips the error — typically in the middle of a
|
||||
* navigation flow.
|
||||
*
|
||||
* This component runs the check pro-actively. Every 60 s it fetches
|
||||
* This component runs the check pro-actively. Every 60 s while the tab
|
||||
* is in view (and once on coming back to it) it fetches
|
||||
* `/api/version` and compares the live version string against
|
||||
* `NEXT_PUBLIC_APP_VERSION` (injected from `package.json` at build
|
||||
* time by `next.config.ts`). When the live version moves ahead of
|
||||
@@ -93,6 +94,19 @@ async function fetchLiveVersion(signal: AbortSignal): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a tab that comes back into view checks the version now. A hidden
|
||||
* tab skips its ticks, so the first look after a full interval away is due at
|
||||
* once; a quick switch back and forth is not a reason to ask again.
|
||||
*/
|
||||
export function versionCheckDueOnReturn(
|
||||
visibility: DocumentVisibilityState,
|
||||
lastCheckedAt: number,
|
||||
now: number,
|
||||
): boolean {
|
||||
return visibility === "visible" && now - lastCheckedAt >= POLL_INTERVAL_MS;
|
||||
}
|
||||
|
||||
// In-memory mirror of the session guard: under strict-privacy modes
|
||||
// sessionStorage throws, and without a fallback a persistent
|
||||
// shell/live mismatch (misconfigured build) would reload every poll.
|
||||
@@ -165,8 +179,10 @@ export function VersionPoller(): null {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const controller = new AbortController();
|
||||
let lastCheckedAt = 0;
|
||||
|
||||
async function checkOnce(): Promise<void> {
|
||||
lastCheckedAt = Date.now();
|
||||
const live = await fetchLiveVersion(controller.signal);
|
||||
let lastReloadedFor: string | null = null;
|
||||
try {
|
||||
@@ -201,16 +217,31 @@ export function VersionPoller(): null {
|
||||
}
|
||||
|
||||
// First check 5 s after mount — gives the app a chance to settle
|
||||
// before we surface anything. Then on the recurring interval.
|
||||
const initial = window.setTimeout(() => void checkOnce(), 5_000);
|
||||
const interval = window.setInterval(
|
||||
() => void checkOnce(),
|
||||
POLL_INTERVAL_MS,
|
||||
);
|
||||
// before we surface anything. Then on the recurring interval, but only
|
||||
// while the tab is in view: a background tab asks nothing and checks
|
||||
// once when it comes back.
|
||||
const tick = () => {
|
||||
if (document.visibilityState === "visible") void checkOnce();
|
||||
};
|
||||
const onVisibilityChange = () => {
|
||||
if (
|
||||
versionCheckDueOnReturn(
|
||||
document.visibilityState,
|
||||
lastCheckedAt,
|
||||
Date.now(),
|
||||
)
|
||||
) {
|
||||
void checkOnce();
|
||||
}
|
||||
};
|
||||
const initial = window.setTimeout(tick, 5_000);
|
||||
const interval = window.setInterval(tick, POLL_INTERVAL_MS);
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(initial);
|
||||
window.clearInterval(interval);
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
controller.abort();
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
Reference in new issue
Block a user