fix(offline): poll navigator.onLine to return online (the online event is unreliable)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m24s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m24s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
The auto-offline -> online return still did not fire: the retry hung on the 'online' event, which an installed PWA often does not deliver. Replace it with a lightweight poll while in auto-offline that reads navigator.onLine (a reliable live flag, unlike the event) and only hits the network for a reachability check when the interface is actually up - so flight mode ON costs no radio, and flight mode OFF is detected within ~4s and returns online. The 'online' event, when it does fire, just kicks an immediate check. Runs only in auto-offline (deliberate offline is the player's choice); wired for both the mid-session and cold-start auto-offline paths.
This commit is contained in:
+26
-11
@@ -587,28 +587,42 @@ function promptOfflineChoice(): Promise<boolean> {
|
|||||||
* cold-start dialog) is left as the player's choice. These are passive OS events — no polling, no
|
* cold-start dialog) is left as the player's choice. These are passive OS events — no polling, no
|
||||||
* battery cost. Web-only and skipped in the mock (the e2e drives connectivity directly).
|
* battery cost. Web-only and skipped in the mock (the e2e drives connectivity directly).
|
||||||
*/
|
*/
|
||||||
// tryReturnOnline is fired from the online event while in auto-offline: the interface being back does
|
// While in auto-offline, poll for the network to return so the app comes back online — robust to the
|
||||||
// not mean the gateway is reachable yet (DNS/routing settle for a moment after flight mode), so it
|
// unreliable `online` event (which often does not fire in an installed PWA). Each tick is cheap: it
|
||||||
// re-checks a few times with backoff before returning online, stopping if the player takes over the
|
// reads navigator.onLine (no radio) and only hits the network (a reachability check) when the
|
||||||
// state meanwhile. This bounded retry is what actually gets the app back online after flight mode off.
|
// interface is actually up, so while flight mode is on it costs nothing. The poll runs ONLY in
|
||||||
async function tryReturnOnline(): Promise<void> {
|
// auto-offline (a deliberate offline is the player's choice) and stops on returning online.
|
||||||
for (let attempt = 0; attempt < 4; attempt++) {
|
let recoveryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
export function scheduleRecovery(delayMs: number): void {
|
||||||
|
if (recoveryTimer) {
|
||||||
|
clearTimeout(recoveryTimer);
|
||||||
|
recoveryTimer = null;
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined' || !offlineMode.active || !offlineMode.auto) return;
|
||||||
|
recoveryTimer = setTimeout(async () => {
|
||||||
|
recoveryTimer = null;
|
||||||
if (!offlineMode.active || !offlineMode.auto) return;
|
if (!offlineMode.active || !offlineMode.auto) return;
|
||||||
if (await checkReachable(BOOT_REACHABILITY_TIMEOUT_MS)) {
|
const interfaceUp = typeof navigator === 'undefined' || navigator.onLine;
|
||||||
|
if (interfaceUp && (await checkReachable(BOOT_REACHABILITY_TIMEOUT_MS))) {
|
||||||
if (offlineMode.active && offlineMode.auto) setOfflineMode(false);
|
if (offlineMode.active && offlineMode.auto) setOfflineMode(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 600 * (attempt + 1)));
|
scheduleRecovery(4000);
|
||||||
}
|
}, delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initNetworkReactivity(): void {
|
export function initNetworkReactivity(): void {
|
||||||
if (typeof window === 'undefined' || import.meta.env.MODE === 'mock') return;
|
if (typeof window === 'undefined' || import.meta.env.MODE === 'mock') return;
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
if (!offlineMode.active) setOfflineMode(true, false); // auto (session) — self-heals below
|
if (!offlineMode.active) {
|
||||||
|
setOfflineMode(true, false); // auto (session)
|
||||||
|
scheduleRecovery(4000); // poll for the network to return
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// The online event, when it fires, just kicks an immediate reachability check; the poll above is
|
||||||
|
// the reliable fallback for platforms where it does not fire.
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
if (offlineMode.active && offlineMode.auto) void tryReturnOnline();
|
if (offlineMode.active && offlineMode.auto) scheduleRecovery(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,6 +919,7 @@ export async function bootstrap(): Promise<void> {
|
|||||||
if (goOffline) {
|
if (goOffline) {
|
||||||
// A deliberate dialog choice is sticky; an auto (no-interface) offline is session-only.
|
// A deliberate dialog choice is sticky; an auto (no-interface) offline is session-only.
|
||||||
setOfflineMode(true, !interfaceOffline);
|
setOfflineMode(true, !interfaceOffline);
|
||||||
|
if (interfaceOffline) scheduleRecovery(4000); // auto-offline: poll for the network to return
|
||||||
app.session = saved;
|
app.session = saved;
|
||||||
app.profile = cachedProfile;
|
app.profile = cachedProfile;
|
||||||
if (router.route.name === 'login' || router.route.name === 'confirm') navigate('/');
|
if (router.route.name === 'login' || router.route.name === 'confirm') navigate('/');
|
||||||
|
|||||||
Reference in New Issue
Block a user