import * as plugins from './plugins.js'; import * as interfaces from '../dist_ts_interfaces/index.js'; import { ServiceWorker } from './classes.serviceworker.js'; import { logger } from './logging.js'; import { CacheManager } from './classes.cachemanager.js'; export class UpdateManager { public lastUpdateCheck: number = 0; public lastVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']; public serviceworkerRef: ServiceWorker; constructor(serviceWorkerRefArg: ServiceWorker) { this.serviceworkerRef = serviceWorkerRefArg; } /** * checks wether an update is needed */ private readonly MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds private readonly MIN_CHECK_INTERVAL = 100000; // 100 seconds in milliseconds private readonly OFFLINE_GRACE_PERIOD = 7 * 24 * 60 * 60 * 1000; // 7 days grace period when offline private lastCacheTimestamp: number = 0; public async checkUpdate(cacheManager: CacheManager): Promise { const lswVersionInfoKey = 'versionInfo'; const cacheTimestampKey = 'cacheTimestamp'; // Initialize or load version info if (!this.lastVersionInfo && !(await this.serviceworkerRef.store.check(lswVersionInfoKey))) { this.lastVersionInfo = { appHash: '', appSemVer: 'v0.0.0', }; } else if (!this.lastVersionInfo && (await this.serviceworkerRef.store.check(lswVersionInfoKey))) { this.lastVersionInfo = await this.serviceworkerRef.store.get(lswVersionInfoKey); } // Load or initialize cache timestamp if (await this.serviceworkerRef.store.check(cacheTimestampKey)) { this.lastCacheTimestamp = await this.serviceworkerRef.store.get(cacheTimestampKey); } const now = Date.now(); const millisSinceLastCheck = now - this.lastUpdateCheck; const cacheAge = now - this.lastCacheTimestamp; // Check if we need to handle stale cache if (cacheAge > this.MAX_CACHE_AGE) { const isOnline = await this.serviceworkerRef.networkManager.checkOnlineStatus(); if (isOnline) { logger.log('info', `Cache is older than ${this.MAX_CACHE_AGE}ms, forcing update...`); await this.forceUpdate(cacheManager); return true; } else if (cacheAge > this.OFFLINE_GRACE_PERIOD) { // If we're offline and beyond grace period, warn but continue serving cached content logger.log('warn', `Cache is stale and device is offline. Cache age: ${cacheAge}ms. Using cached content with warning.`); // We could potentially show a warning to the user here return false; } else { logger.log('info', `Cache is stale but device is offline. Within grace period. Using cached content.`); return false; } } // Regular update check interval if (millisSinceLastCheck < this.MIN_CHECK_INTERVAL && cacheAge < this.MAX_CACHE_AGE) { return false; } logger.log('info', 'checking for update of the app by comparing app hashes...'); this.lastUpdateCheck = now; const currentVersionInfo = await this.getVersionInfoFromServer(); logger.log('info', `old versionInfo: ${JSON.stringify(this.lastVersionInfo)}`); logger.log('info', `current versionInfo: ${JSON.stringify(currentVersionInfo)}`); const needsUpdate = this.lastVersionInfo.appHash !== currentVersionInfo.appHash ? true : false; if (needsUpdate) { logger.log('info', 'Caches need to be updated'); logger.log('info', 'starting a debounced update task'); this.performAsyncUpdateDebouncedTask.trigger(); this.lastVersionInfo = currentVersionInfo; await this.serviceworkerRef.store.set(lswVersionInfoKey, this.lastVersionInfo); // Update cache timestamp this.lastCacheTimestamp = now; await this.serviceworkerRef.store.set('cacheTimestamp', now); } else { logger.log('ok', 'caches are still valid, performing revalidation in a bit...'); this.performAsyncCacheRevalidationDebouncedTask.trigger(); // Update cache timestamp after successful revalidation this.lastCacheTimestamp = now; await this.serviceworkerRef.store.set('cacheTimestamp', now); } } /** * gets the apphash from the server */ public async getVersionInfoFromServer() { try { const getAppHashRequest = new plugins.typedrequest.TypedRequest< interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo >('/sw-typedrequest', 'serviceworker_versionInfo'); // Use networkManager for the request with retries and timeout const response = await getAppHashRequest.fire({}); return response; } catch (error) { logger.log('warn', `Failed to get version info from server: ${error.message}`); throw error; } } // tasks /** * this task is executed once we know that there is a new version available */ private async forceUpdate(cacheManager: CacheManager) { try { logger.log('info', 'Forcing cache update due to staleness'); const currentVersionInfo = await this.getVersionInfoFromServer(); // Only proceed with cache cleaning if we successfully got new version info await this.serviceworkerRef.cacheManager.cleanCaches('Cache is stale, forcing update.'); this.lastVersionInfo = currentVersionInfo; await this.serviceworkerRef.store.set('versionInfo', this.lastVersionInfo); this.lastCacheTimestamp = Date.now(); await this.serviceworkerRef.store.set('cacheTimestamp', this.lastCacheTimestamp); await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll(); } catch (error) { logger.log('error', `Failed to force update: ${error.message}. Keeping existing cache.`); // If update fails, we'll keep using the existing cache throw error; } } public performAsyncUpdateDebouncedTask = new plugins.taskbuffer.TaskDebounced({ name: 'performAsyncUpdate', taskFunction: async () => { logger.log('info', 'trying to update PWA with serviceworker'); await this.serviceworkerRef.cacheManager.cleanCaches('a new app version has been communicated by the server.'); // lets notify all current clients about the update await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll(); }, debounceTimeInMillis: 2000, }); public performAsyncCacheRevalidationDebouncedTask = new plugins.taskbuffer.TaskDebounced({ name: 'performAsyncCacheRevalidation', taskFunction: async () => { await this.serviceworkerRef.cacheManager.revalidateCache(); }, debounceTimeInMillis: 6000 }); }