diff --git a/changelog.md b/changelog.md index 8530b89..28d267e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-02-03 - 3.0.58 - fix(network-manager) +Refined network management logic for better offline handling. + +- Improved logic to handle missing connections more gracefully. +- Added detailed online/offline connection status logging. +- Implemented a check for stale cache with a grace period for offline scenarios. +- Network requests now use optimized retries and timeouts. + ## 2025-02-03 - 3.0.57 - fix(updateManager) Refine cache management for service worker updates. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index ba58134..f33dfa8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedserver', - version: '3.0.57', + version: '3.0.58', description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.' } diff --git a/ts_web_serviceworker/classes.networkmanager.ts b/ts_web_serviceworker/classes.networkmanager.ts index 54e4d83..2fcdfd4 100644 --- a/ts_web_serviceworker/classes.networkmanager.ts +++ b/ts_web_serviceworker/classes.networkmanager.ts @@ -1,18 +1,37 @@ import * as plugins from './plugins.js'; import { ServiceWorker } from './classes.serviceworker.js'; +import { logger } from './logging.js'; export class NetworkManager { public serviceWorkerRef: ServiceWorker; public webRequest: plugins.webrequest.WebRequest; + private isOffline: boolean = false; + private lastOnlineCheck: number = 0; + private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds public previousState: string; constructor(serviceWorkerRefArg: ServiceWorker) { this.serviceWorkerRef = serviceWorkerRefArg; this.webRequest = new plugins.webrequest.WebRequest(); + + // Listen for connection changes this.getConnection()?.addEventListener('change', () => { this.updateConnectionStatus(); }); + + // Listen for online/offline events + self.addEventListener('online', () => { + this.isOffline = false; + logger.log('info', 'Device is now online'); + this.updateConnectionStatus(); + }); + + self.addEventListener('offline', () => { + this.isOffline = true; + logger.log('warn', 'Device is now offline'); + this.updateConnectionStatus(); + }); } /** @@ -28,6 +47,81 @@ export class NetworkManager { } public updateConnectionStatus() { - console.log(`Connection type changed from ${this.previousState} to ${this.getEffectiveType()}`); + const currentType = this.getEffectiveType(); + logger.log('info', `Connection type changed from ${this.previousState} to ${currentType}`); + this.previousState = currentType; + } + + /** + * Checks if the device is currently online by attempting to contact the server + * @returns Promise true if online, false if offline + */ + public async checkOnlineStatus(): Promise { + const now = Date.now(); + // Only check if enough time has passed since last check + if (now - this.lastOnlineCheck < this.ONLINE_CHECK_INTERVAL) { + return !this.isOffline; + } + + try { + const response = await fetch('/sw-typedrequest', { + method: 'HEAD', + cache: 'no-cache' + }); + this.isOffline = false; + this.lastOnlineCheck = now; + return true; + } catch (error) { + this.isOffline = true; + this.lastOnlineCheck = now; + logger.log('warn', 'Device appears to be offline'); + return false; + } + } + + /** + * Makes a network request with offline handling + * @param request The request to make + * @param options Additional options + * @returns Promise + */ + public async makeRequest(request: Request | string, options: { + timeoutMs?: number; + retries?: number; + backoffMs?: number; + } = {}): Promise { + const { + timeoutMs = 5000, + retries = 1, + backoffMs = 1000 + } = options; + + let lastError: Error; + for (let i = 0; i <= retries; i++) { + try { + const isOnline = await this.checkOnlineStatus(); + if (!isOnline) { + throw new Error('Device is offline'); + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + const response = await fetch(request, { + ...typeof request === 'string' ? {} : request, + signal: controller.signal + }); + + clearTimeout(timeoutId); + return response; + } catch (error) { + lastError = error; + if (i < retries) { + await new Promise(resolve => setTimeout(resolve, backoffMs * (i + 1))); + } + } + } + + throw lastError; } } diff --git a/ts_web_serviceworker/classes.updatemanager.ts b/ts_web_serviceworker/classes.updatemanager.ts index 39f6ed9..34195b1 100644 --- a/ts_web_serviceworker/classes.updatemanager.ts +++ b/ts_web_serviceworker/classes.updatemanager.ts @@ -19,6 +19,7 @@ export class UpdateManager { */ 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 { @@ -44,11 +45,23 @@ export class UpdateManager { const millisSinceLastCheck = now - this.lastUpdateCheck; const cacheAge = now - this.lastCacheTimestamp; - // Force update if cache is too old + // Check if we need to handle stale cache if (cacheAge > this.MAX_CACHE_AGE) { - logger.log('info', `Cache is older than ${this.MAX_CACHE_AGE}ms, forcing update...`); - await this.forceUpdate(cacheManager); - return true; + 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 @@ -85,11 +98,24 @@ export class UpdateManager { * gets the apphash from the server */ public async getVersionInfoFromServer() { - const getAppHashRequest = new plugins.typedrequest.TypedRequest< - interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo - >('/sw-typedrequest', 'serviceworker_versionInfo'); - const result = await getAppHashRequest.fire({}); - return result; + 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 this.serviceworkerRef.networkManager.makeRequest('/sw-typedrequest', { + timeoutMs: 5000, + retries: 2, + backoffMs: 1000 + }); + + const result = await response.json(); + return result; + } catch (error) { + logger.log('warn', `Failed to get version info from server: ${error.message}`); + throw error; + } } // tasks @@ -97,14 +123,22 @@ export class UpdateManager { * this task is executed once we know that there is a new version available */ private async forceUpdate(cacheManager: CacheManager) { - logger.log('info', 'Forcing cache update due to staleness'); - await this.serviceworkerRef.cacheManager.cleanCaches('Cache is stale, forcing update.'); - const currentVersionInfo = await this.getVersionInfoFromServer(); - 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(); + 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({