diff --git a/changelog.md b/changelog.md index 9119a94..4223266 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-12-04 - 6.5.0 - feat(serviceworker) +Add server-driven service worker cache invalidation and TypedSocket integration + +- TypedServer: push cache invalidation messages to service worker clients (tagged 'serviceworker') before notifying frontend clients on reload +- Service Worker: connect to TypedServer via TypedSocket; handle 'serviceworker_cacheInvalidate' typed request to clean caches and trigger client reloads +- Web inject: add fallback to clear caches via the Cache API when global service worker helper is not available +- Interfaces: add IRequest_Serviceworker_CacheInvalidate typedrequest interface +- Plugins: export typedsocket in web_serviceworker plugin surface +- Service worker connection: retry logic and improved logging for TypedSocket connection attempts + ## 2025-12-04 - 6.4.0 - feat(serviceworker) Add speedtest support to service worker and dashboard diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cca325c..3f6491c 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.4.0', + version: '6.5.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/classes.typedserver.ts b/ts/classes.typedserver.ts index b3ac69e..9bee2a5 100644 --- a/ts/classes.typedserver.ts +++ b/ts/classes.typedserver.ts @@ -500,6 +500,30 @@ export class TypedServer { return; } + // Push cache invalidation to service workers first + try { + const swConnections = await this.typedsocket.findAllTargetConnectionsByTag('serviceworker'); + for (const connection of swConnections) { + const pushCacheInvalidate = + this.typedsocket.createTypedRequest( + 'serviceworker_cacheInvalidate', + connection + ); + pushCacheInvalidate.fire({ + reason: 'File change detected', + timestamp: this.lastReload, + }).catch(err => { + console.warn('Failed to push cache invalidation to service worker:', err); + }); + } + if (swConnections.length > 0) { + console.log(`Pushed cache invalidation to ${swConnections.length} service worker(s)`); + } + } catch (error) { + console.warn('Failed to notify service workers:', error); + } + + // Notify frontend clients try { const connections = await this.typedsocket.findAllTargetConnectionsByTag( 'typedserver_frontend' diff --git a/ts_interfaces/serviceworker.ts b/ts_interfaces/serviceworker.ts index 43cf41a..45670f0 100644 --- a/ts_interfaces/serviceworker.ts +++ b/ts_interfaces/serviceworker.ts @@ -195,6 +195,24 @@ export interface IConnectionResult { // Speedtest interfaces // =============== +/** + * Cache invalidation request from server to service worker + */ +export interface IRequest_Serviceworker_CacheInvalidate + extends plugins.typedrequestInterfaces.implementsTR< + plugins.typedrequestInterfaces.ITypedRequest, + IRequest_Serviceworker_CacheInvalidate + > { + method: 'serviceworker_cacheInvalidate'; + request: { + reason: string; + timestamp: number; + }; + response: { + success: boolean; + }; +} + /** * Speedtest request between service worker and backend */ diff --git a/ts_web_inject/index.ts b/ts_web_inject/index.ts index f547dcf..8245185 100644 --- a/ts_web_inject/index.ts +++ b/ts_web_inject/index.ts @@ -75,8 +75,17 @@ export class ReloadChecker { this.infoscreen.setText(reloadText); if (globalThis.globalSw?.purgeCache) { await globalThis.globalSw.purgeCache(); + } else if ('caches' in window) { + // Fallback: clear caches via Cache API when service worker client isn't initialized + try { + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys.map(key => caches.delete(key))); + logger.log('ok', 'Cleared caches via Cache API fallback'); + } catch (err) { + logger.log('warn', `Failed to clear caches via Cache API: ${err}`); + } } else { - console.log('globalThis.globalSw not found...'); + console.log('globalThis.globalSw not found and Cache API not available...'); } this.infoscreen.setText(`cleaned caches`); await plugins.smartdelay.delayFor(200); diff --git a/ts_web_serviceworker/classes.serviceworker.ts b/ts_web_serviceworker/classes.serviceworker.ts index b680498..26bd63e 100644 --- a/ts_web_serviceworker/classes.serviceworker.ts +++ b/ts_web_serviceworker/classes.serviceworker.ts @@ -27,6 +27,10 @@ export class ServiceWorker { public taskManager: TaskManager; public store: plugins.webstore.WebStore; + // TypedSocket connection for server communication + private typedsocket: plugins.typedsocket.TypedSocket; + private typedrouter = new plugins.typedrequest.TypedRouter(); + constructor(selfArg: interfaces.ServiceWindow) { logger.log('info', `Service worker instantiating at ${Date.now()}`); this.serviceWindowRef = selfArg; @@ -76,16 +80,52 @@ export class ServiceWorker { try { await selfArg.clients.claim(); logger.log('ok', 'Clients claimed successfully'); - + await this.cacheManager.cleanCaches('new service worker loaded! :)'); logger.log('ok', 'Caches cleaned successfully'); - + done.resolve(); logger.log('success', `Service worker activated at ${new Date().toISOString()}`); + + // Connect to TypedServer for cache invalidation after activation + this.connectToServer(); } catch (error) { logger.log('error', `Service worker activation error: ${error}`); done.reject(error); } }); } + + /** + * Connect to TypedServer via TypedSocket for cache invalidation + */ + private async connectToServer(): Promise { + try { + // Register handler for cache invalidation from server + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler('serviceworker_cacheInvalidate', async (reqArg) => { + logger.log('info', `Cache invalidation requested from server: ${reqArg.reason}`); + await this.cacheManager.cleanCaches(reqArg.reason); + // Notify all clients to reload + await this.leleServiceWorkerBackend.triggerReloadAll(); + return { success: true }; + }) + ); + + // Connect to server via TypedSocket + this.typedsocket = await plugins.typedsocket.TypedSocket.createClient( + this.typedrouter, + this.serviceWindowRef.location.origin + ); + + // Tag this connection as a service worker for server-side filtering + await this.typedsocket.setTag('serviceworker', {}); + + logger.log('ok', 'Service worker connected to TypedServer via TypedSocket'); + } catch (error: any) { + logger.log('warn', `Service worker TypedSocket connection failed: ${error?.message || error}`); + // Retry connection after a delay + setTimeout(() => this.connectToServer(), 10000); + } + } } \ No newline at end of file diff --git a/ts_web_serviceworker/plugins.ts b/ts_web_serviceworker/plugins.ts index fd6a221..e24794a 100644 --- a/ts_web_serviceworker/plugins.ts +++ b/ts_web_serviceworker/plugins.ts @@ -5,8 +5,9 @@ export { interfaces }; // @apiglobal scope import * as typedrequest from '@api.global/typedrequest'; +import * as typedsocket from '@api.global/typedsocket'; -export { typedrequest }; +export { typedrequest, typedsocket }; // @pushrocks scope import * as smartdelay from '@push.rocks/smartdelay';