From c263b0608cb095df7dcdfdc52969d23e9699e0c6 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 4 Dec 2025 11:25:56 +0000 Subject: [PATCH] feat(web_serviceworker): Add advanced service worker subsystems: cache deduplication, metrics, update & network managers, event bus and dashboard --- changelog.md | 14 +++++++ ts/00_commitinfo_data.ts | 2 +- ts_web_serviceworker/classes.backend.ts | 56 +++++++++++++++++++++---- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index dd01df1..3a5f5eb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,19 @@ # Changelog +## 2025-12-04 - 6.3.0 - feat(web_serviceworker) +Add advanced service worker subsystems: cache deduplication, metrics, update & network managers, event bus and dashboard + +- CacheManager: request deduplication for concurrent fetches, safer caching (preserve CORS headers), periodic in-flight cleanup and full cache cleaning API +- Fetch handling: improved handling for same-origin vs cross-origin requests, more robust 500 debug responses when upstream fetch fails +- UpdateManager: rate-limited update checks, offline grace period, debounced update and cache revalidation tasks, forceUpdate logic and persisted version/cache timestamps +- NetworkManager: online/offline detection, retry/backoff, request timeouts and more resilient makeRequest implementation +- EventBus: singleton pub/sub with history, once/onMany/onAll helpers and convenience emitters for cache/network/update events +- MetricsCollector: comprehensive metrics for cache, network, updates and connections with helper methods and JSON/HTML dashboard endpoints (/sw-dash, /sw-dash/metrics) +- ErrorHandler & ServiceWorkerError: structured error types, severity, context, history and helper APIs for consistent error reporting +- ServiceWorker & backend: improved install/activate flows, clients.claim(), cache cleaning on activation, backend APIs to purge cache and trigger reloads/notifications +- TypedServer / servertools: addRoute path pattern parsing (named params & wildcards), safer HTML injection for reload script, TypedRequest controller and service worker route helpers +- Various safety and compatibility improvements (response cloning, header normalization, cache-control decisions, and fallback behaviors) + ## 2025-12-04 - 6.2.0 - feat(web_serviceworker) Add service-worker dashboard and request deduplication; improve caching, metrics and error handling diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index afffeb9..64616f6 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: '6.2.0', + version: '6.3.0', 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.backend.ts b/ts_web_serviceworker/classes.backend.ts index 164a88d..5e57ba8 100644 --- a/ts_web_serviceworker/classes.backend.ts +++ b/ts_web_serviceworker/classes.backend.ts @@ -1,6 +1,7 @@ import * as plugins from './plugins.js'; import * as interfaces from '../dist_ts_interfaces/index.js'; import { logger } from './logging.js'; +import { getMetricsCollector } from './classes.metrics.js'; // Add type definitions for ServiceWorker APIs declare global { @@ -41,11 +42,15 @@ declare global { */ export class ServiceworkerBackend { public deesComms = new plugins.deesComms.DeesComms(); + private swSelf: ServiceWorkerGlobalScope; + private clientUpdateInterval: ReturnType | null = null; constructor(optionsArg: { self: any; purgeCache: (reqArg: interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['request']) => Promise; }) { + this.swSelf = optionsArg.self as unknown as ServiceWorkerGlobalScope; + const metrics = getMetricsCollector(); // lets handle wakestuff optionsArg.self.addEventListener('message', (event) => { @@ -53,16 +58,51 @@ export class ServiceworkerBackend { console.log('sw-backend: got wake up call'); } }); + this.deesComms.createTypedHandler('broadcastConnectionPolling', async reqArg => { + // Record connection attempt + metrics.recordConnectionAttempt(); + metrics.recordConnectionSuccess(); + // Update connected clients count + await this.updateConnectedClientsCount(); return { serviceworkerId: '123' }; - }) + }); this.deesComms.createTypedHandler('purgeServiceWorkerCache', async reqArg => { console.log(`Executing purge cache in serviceworker backend.`) return await optionsArg.purgeCache?.(reqArg); }); + + // Periodically update connected clients count + this.startClientCountUpdates(); + } + + /** + * Start periodic updates of connected client count + */ + private startClientCountUpdates(): void { + // Update immediately + this.updateConnectedClientsCount(); + + // Then update every 5 seconds + this.clientUpdateInterval = setInterval(() => { + this.updateConnectedClientsCount(); + }, 5000); + } + + /** + * Update the connected clients count using the Clients API + */ + private async updateConnectedClientsCount(): Promise { + try { + const clients = await this.swSelf.clients.matchAll({ type: 'window' }); + const metrics = getMetricsCollector(); + metrics.setConnectedClients(clients.length); + } catch (error) { + logger.log('warn', `Failed to update connected clients count: ${error}`); + } } /** @@ -71,7 +111,7 @@ export class ServiceworkerBackend { public async triggerReloadAll() { try { logger.log('info', 'Triggering reload for all clients due to new version'); - + // Send update message via DeesComms // This will be picked up by clients that have registered a handler for 'serviceworker_newVersion' await this.deesComms.postMessage({ @@ -79,13 +119,15 @@ export class ServiceworkerBackend { request: {}, messageId: `sw_update_${Date.now()}` }); - + // As a fallback, also use the clients API to reload clients that might not catch the broadcast - // We need to type-cast self since TypeScript doesn't recognize ServiceWorker API - const swSelf = self as unknown as ServiceWorkerGlobalScope; - const clients = await swSelf.clients.matchAll({ type: 'window' }); + const clients = await this.swSelf.clients.matchAll({ type: 'window' }); logger.log('info', `Found ${clients.length} clients to reload`); - + + // Update metrics with current client count + const metrics = getMetricsCollector(); + metrics.setConnectedClients(clients.length); + for (const client of clients) { if ('navigate' in client) { // For modern browsers, navigate to the same URL to trigger reload