fix(app): make web update detection survive flaky networks (#3135)

This commit is contained in:
Hampus
2026-10-02 21:39:52 +02:00
committed by GitHub
parent 27fc634bc9
commit effeaaa435
3 changed files with 38 additions and 12 deletions
+31 -10
View File
@@ -6,6 +6,8 @@ import {
shouldShowNativeDesktopUpdateDownloadProgress,
shouldShowNativeDesktopUpdateInApp,
} from '@app/features/app/utils/UpdaterPlatformUtils';
import {WORKER_NAVIGATION_CACHE_PREFIX} from '@app/features/platform/service_worker/WorkerCacheCleanup';
import {getProtectedCacheStorage} from '@app/features/platform/state/ProtectedWebStorage';
import type {UpdaterContext, UpdaterDownloadOption, UpdaterEvent} from '@app/features/platform/types/Electron';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {getClientInfo} from '@app/features/platform/utils/ClientInfo';
@@ -49,9 +51,10 @@ export const DOWNLOADING_UPDATE_DESCRIPTOR = msg({
});
const logger = new Logger('Updater');
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
const MIN_CHECK_INTERVAL_MS = 60 * 1000;
const MANUAL_DOWNLOAD_REFRESH_TIMEOUT_MS = 5 * 1000;
const WEB_CHECK_TIMEOUT_MS = 15 * 1000;
const VERSION_ENDPOINT = '/version.json';
const CURRENT_BUILD_VERSION = Config.PUBLIC_BUILD_VERSION ?? null;
const ALLOWED_WEB_UPDATE_HOSTS = new Set([
@@ -61,6 +64,21 @@ const ALLOWED_WEB_UPDATE_HOSTS = new Set([
'canary.fluxer.com',
]);
async function dropCachedAppShell(): Promise<void> {
const browserCaches = getProtectedCacheStorage();
if (!browserCaches) return;
try {
const cacheNames = await browserCaches.keys();
await Promise.all(
cacheNames
.filter((cacheName) => cacheName.startsWith(WORKER_NAVIGATION_CACHE_PREFIX))
.map((cacheName) => browserCaches.delete(cacheName)),
);
} catch (error) {
logger.warn('Failed to drop the cached app shell before reloading', error);
}
}
function normalizeUpdaterContext(context: NativeUpdaterEvent['context']): UpdaterContext {
switch (context) {
case 'user':
@@ -487,11 +505,9 @@ class Updater {
shouldCheckNative ? this.checkNativeUpdate(checkContext) : Promise.resolve(null),
this.checkWebUpdate(),
]);
this.transition({
type: 'web.checked',
available: webResult?.available ?? false,
version: webResult?.version ?? null,
});
if (webResult) {
this.transition({type: 'web.checked', available: webResult.available, version: webResult.version});
}
if (userInitiated && (!shouldCheckNative || (!this.isChecking && !this.nativeCheckFailed))) {
this.showCurrentUpdateState();
}
@@ -521,7 +537,7 @@ class Updater {
private async checkWebUpdate(): Promise<{
available: boolean;
version: string | null;
}> {
} | null> {
if (!ALLOWED_WEB_UPDATE_HOSTS.has(window.location.host)) {
return {available: false, version: null};
}
@@ -529,24 +545,28 @@ class Updater {
const response = await fetch(VERSION_ENDPOINT, {
cache: 'no-store',
headers: {'Cache-Control': 'no-cache'},
signal: AbortSignal.timeout(WEB_CHECK_TIMEOUT_MS),
});
if (!response.ok) {
logger.debug('Version endpoint not available');
return {available: false, version: null};
return null;
}
const payload = (await response.json()) as {
version?: string;
buildVersion?: string;
};
const version = payload.version ?? payload.buildVersion ?? null;
const updateAvailable = Boolean(version && CURRENT_BUILD_VERSION && version !== CURRENT_BUILD_VERSION);
if (!version) {
return null;
}
const updateAvailable = Boolean(CURRENT_BUILD_VERSION && version !== CURRENT_BUILD_VERSION);
return {
available: updateAvailable,
version,
};
} catch (error) {
logger.debug('Failed to fetch version info silently:', error);
return {available: false, version: null};
return null;
}
}
@@ -586,6 +606,7 @@ class Updater {
}
if (this.updateInfo.web.available) {
logger.info('Applying web update, reloading...');
await dropCachedAppShell();
window.location.reload();
return;
}
@@ -13,7 +13,11 @@ import {
precacheAssets,
seedAppShell,
} from '@app/features/platform/service_worker/WorkerAppShell';
import {shouldDeleteWorkerCache, WORKER_CACHE_PREFIX} from '@app/features/platform/service_worker/WorkerCacheCleanup';
import {
shouldDeleteWorkerCache,
WORKER_CACHE_PREFIX,
WORKER_NAVIGATION_CACHE_PREFIX,
} from '@app/features/platform/service_worker/WorkerCacheCleanup';
import {getWorkerFetchRoute} from '@app/features/platform/service_worker/WorkerFetchRouting';
import {
getBadgeCount,
@@ -45,7 +49,7 @@ const ensureServiceWorkerReady: Promise<void> = Promise.resolve();
const SERVICE_WORKER_VERSION = typeof __FLUXER_SW_VERSION__ === 'string' ? __FLUXER_SW_VERSION__ : 'dev';
const PRECACHE_MANIFEST = typeof __FLUXER_PRECACHE_MANIFEST__ === 'undefined' ? [] : __FLUXER_PRECACHE_MANIFEST__;
const PRECACHE_CACHE = `${WORKER_CACHE_PREFIX}-precache-${SERVICE_WORKER_VERSION}`;
const NAVIGATION_CACHE = `${WORKER_CACHE_PREFIX}-navigation-${SERVICE_WORKER_VERSION}`;
const NAVIGATION_CACHE = `${WORKER_NAVIGATION_CACHE_PREFIX}${SERVICE_WORKER_VERSION}`;
const EXPECTED_CACHES = new Set([PRECACHE_CACHE, NAVIGATION_CACHE]);
const NAVIGATION_NETWORK_TIMEOUT_MS = 650;
const serviceWorkerCaches = self.caches;
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const WORKER_CACHE_PREFIX = 'fluxer';
export const WORKER_NAVIGATION_CACHE_PREFIX = `${WORKER_CACHE_PREFIX}-navigation-`;
export function shouldDeleteWorkerCache(cacheName: string, expectedCaches: ReadonlySet<string>): boolean {
return cacheName.startsWith(`${WORKER_CACHE_PREFIX}-`) && !expectedCaches.has(cacheName);