1// Workbox 7.4.1, self-hosted: Google's workbox-cdn was discontinued at the 6.x line, and self-hosting 2// keeps worker installs/updates independent of any third-party CDN. 3importScripts('/Scripts/workbox/workbox-sw.js'); 4workbox.setConfig({ modulePathPrefix: '/Scripts/workbox/' }); 5 6self.addEventListener('message', (event) => { 7 if (event.data && event.data.type === 'SKIP_WAITING') { 8 self.skipWaiting(); 9 } 10}); 11 12self.addEventListener('message', (event) => { 13 console.log('[SW] Received message:', event.data); 14 15 if (event.data && event.data.type === 'CLEAR_PROGRAMME_CACHE') { 16 const cacheNamesToDelete = ['programme', 'programme-partials']; 17 18 caches.keys().then((names) => { 19 console.log('[SW] Existing caches:', names); 20 }); 21 22 Promise.all( 23 cacheNamesToDelete.map(name => caches.delete(name)) 24 ).then(results => { 25 console.log('[SW] Deleted caches:', cacheNamesToDelete.filter((_, i) => results[i])); 26 }).catch(err => { 27 console.error('[SW] Error clearing caches:', err); 28 }); 29 } 30}); 31 32 33self.__precacheManifest = [ 34 { 35 "url": "/pwaOffline.html" 36 }, 37 { 38 "url": "/Scripts/CoreJSBundle" 39 }, 40 { 41 "url": "/Styles/CustomBootstrapCSSBundle" 42 }, 43 { 44 "url": "/Styles/CoreCSSBundle" 45 }, 46 { 47 "url": "/Styles/AdditionalCSSBundle" 48 }, 49 { 50 "url": "/Styles/DevExpress" 51 }, 52 { 53 "url": "/Content/DevExtremeBundle" 54 }, 55 { 56 "url": "/Scripts/DevExtremeBundle" 57 }, 58 { 59 "url": "/Script/AcademicProgrammeBundle" 60 }, 61 { 62 "url": "/favicon.ico" 63 } 64].concat(self.__precacheManifest || []); 65workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 66 67workbox.routing.registerRoute( 68 // Cache Image File 69 /.*\.(?:png|jpg|jpeg|svg|gif|webp)/, 70 new workbox.strategies.CacheFirst({ 71 cacheName: 'images', 72 plugins: [ 73 new workbox.expiration.ExpirationPlugin({ 74 maxEntries: 60, 75 maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days 76 }) 77 ] 78 }) 79); 80 81workbox.routing.registerRoute( 82 new RegExp('https://platform\\.twitter\\.com.*'), 83 new workbox.strategies.NetworkOnly() 84); 85 86workbox.routing.registerRoute( 87 new RegExp('https://syndication\\.twitter\\.com.*'), 88 new workbox.strategies.NetworkOnly() 89); 90 91workbox.routing.registerRoute( 92 new RegExp('https://www\\.google-analytics\\.com.*'), 93 new workbox.strategies.NetworkOnly() 94); 95 96workbox.routing.registerRoute( 97 new RegExp('http://www\\.google-analytics\\.com.*'), 98 new workbox.strategies.NetworkOnly() 99); 100 101// The only cross-origin assets worth an offline copy (icon kit / consent CSS the shell needs). 102// Tightly bounded: cached no-cors responses are opaque and Chrome pads each one by several MB of 103// reported quota. All other cross-origin requests are passthrough - see setDefaultHandler. 104workbox.routing.registerRoute( 105 ({url}) => url.hostname === 'kit.fontawesome.com' 106 || url.hostname === 'ka-f.fontawesome.com' 107 || url.hostname === 'cdn.jsdelivr.net', 108 new workbox.strategies.StaleWhileRevalidate({ 109 cacheName: 'ext-static', 110 plugins: [ 111 new workbox.expiration.ExpirationPlugin({ 112 maxEntries: 40, 113 maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days 114 purgeOnQuotaError: true 115 }) 116 ] 117 }) 118); 119 120// SignalR URLs carry a unique token/timestamp per request (a cached copy can never be replayed) and 121// Admin pages are per-user and useless offline. The static /signalr/hubs proxy script is the 122// exception: it falls through to the bounded default so EP pages can still load it offline. 123workbox.routing.registerRoute( 124 ({url}) => { 125 const path = url.pathname.toLowerCase(); 126 return (path.startsWith('/signalr') && path !== '/signalr/hubs') || path.startsWith('/admin'); 127 }, 128 new workbox.strategies.NetworkOnly() 129); 130 131workbox.routing.registerRoute( 132 new RegExp('\/MyEcpr/Account/Create*'), 133 new workbox.strategies.NetworkOnly() 134); 135 136workbox.routing.registerRoute( 137 // Case-insensitive: the live route is /Login (capital L), and a lowercase-only match would let the 138 // login page fall through to the default NetworkFirst handler and be cached. 139 new RegExp('\/login.*', 'i'), 140 new workbox.strategies.NetworkOnly() 141); 142 143workbox.routing.registerRoute( 144 new RegExp('\/Events\/AcademicProgramme\/Schedule.*'), 145 new workbox.strategies.StaleWhileRevalidate({ 146 cacheName: "schedule", 147 plugins: [ 148 // DevExtreme grids append a unique query string per sort/filter/page, so without an 149 // expiry every variant lives forever. 150 new workbox.expiration.ExpirationPlugin({ 151 maxEntries: 100, 152 maxAgeSeconds: 300, // 5 Minutes 153 purgeOnQuotaError: true 154 }) 155 ] 156 }) 157); 158 159workbox.routing.registerRoute( 160 new RegExp('\/Events\/AcademicProgramme\/ScheduleGridView.*'), 161 new workbox.strategies.StaleWhileRevalidate({ 162 cacheName: "schedule", 163 plugins: [ 164 new workbox.expiration.ExpirationPlugin({ 165 maxEntries: 100, 166 maxAgeSeconds: 300, // 5 Minutes 167 purgeOnQuotaError: true 168 }) 169 ] 170 }) 171); 172 173workbox.routing.registerRoute( 174 ({url}) => url.pathname.startsWith('/Events/AcademicProgramme/Programme'), 175 new workbox.strategies.StaleWhileRevalidate({ 176 cacheName: 'programme', 177 plugins: [ 178 new workbox.expiration.ExpirationPlugin({ 179 maxAgeSeconds: 300, // 5 Minutes 180 purgeOnQuotaError: true 181 }) 182 ] 183 }) 184); 185 186workbox.routing.registerRoute( 187 ({url}) => url.pathname.includes('SectionGridView') 188 || url.pathname.includes('PanelGridView') 189 || url.pathname.includes('WorkshopGridView') 190 || url.pathname.includes('PaperGridView') 191 || url.pathname.includes('PeopleGridView'), 192 new workbox.strategies.StaleWhileRevalidate({ 193 cacheName: 'programme-partials', 194 plugins: [ 195 new workbox.expiration.ExpirationPlugin({ 196 maxAgeSeconds: 300, // 5 Minutes 197 purgeOnQuotaError: true 198 }) 199 ] 200 }) 201); 202 203workbox.routing.registerRoute( 204 new RegExp('\/Events\/AcademicProgramme\/KeywordGridView.*'), 205 new workbox.strategies.StaleWhileRevalidate({ 206 cacheName: "programme", 207 plugins: [ 208 // Must match the Programme route above: entries written by a plugin-less strategy get no 209 // expiration timestamp, so the other route's plugin can never prune them. 210 new workbox.expiration.ExpirationPlugin({ 211 maxAgeSeconds: 300, // 5 Minutes 212 purgeOnQuotaError: true 213 }) 214 ] 215 }) 216); 217 218// Meeting pages must never be served from cache - correctness, not performance. Remove when the 219// legacy VirtualEvent area is deleted. 220workbox.routing.registerRoute( 221 new RegExp('\/VirtualEvent\/VirtualMeeting.*'), 222 new workbox.strategies.NetworkOnly() 223); 224 225// Bounded LRU for same-origin GETs not matched above. The entry cap is load-bearing: '_='-busted and 226// grid-query URLs are unique per request, so an uncapped cache here grows without limit. 227const siteFallback = new workbox.strategies.NetworkFirst({ 228 cacheName: 'site-fallback', 229 plugins: [ 230 new workbox.expiration.ExpirationPlugin({ 231 maxEntries: 150, 232 maxAgeSeconds: 7 * 24 * 60 * 60, // 7 Days 233 purgeOnQuotaError: true 234 }) 235 ] 236}); 237 238workbox.routing.setDefaultHandler((args) => { 239 const request = args.event.request; 240 if (request.method !== 'GET') return fetch(request); 241 // Cross-origin is passthrough: no-cors responses are opaque (can't be val
241idated) and Chrome pads 242 // each cached one by several MB of reported quota - caching them is all cost, no benefit. 243 if (new URL(request.url).origin !== self.location.origin) return fetch(request); 244 return siteFallback.handle(args); 245}); 246 247workbox.routing.setCatchHandler(({ event }) => { 248 return caches.match('/pwaOffline.html'); 249}); 250 251// --- EventPlatform caching --------------------------------------------------------------------- 252// NetworkFirst throughout: online always shows the server's current version; the SW copy is only the 253// offline fallback. Per-user pages live in 'ep-personal', purged on sign-in/out (_Layout.cshtml, 254// Account/LoggedOut.cshtml) so accounts can't leak on a shared device; shared content persists. 255// Matchers are lowercase (MVC routes are case-insensitive); NetworkOnly rules must precede the catch-all. 256const epNetworkFirst = (cacheName) => new workbox.strategies.NetworkFirst({ 257 cacheName, 258 networkTimeoutSeconds: 3, // fall back to the cached copy quickly on a flaky connection 259 plugins: [ 260 new workbox.expiration.ExpirationPlugin({ 261 maxEntries: 200, // bounds a heavy day of unique URLs (searches, ?focus= variants) per bucket 262 maxAgeSeconds: 24 * 60 * 60, // offline-copy lifespan (a full event day) - NOT the freshness window 263 purgeOnQuotaError: true 264 }) 265 ] 266}); 267 268// Must run live (NetworkOnly): sign-in/out (a cached auth response can't start or end a real server 269// session) and Zoom joins (the meeting page carries a time-sensitive SDK signature, so a cached copy is 270// expired and the join fails - the legacy VirtualMeeting rule above does the same). 271workbox.routing.registerRoute( 272 ({url}) => url.pathname.toLowerCase().startsWith('/eventplatform/account/'), 273 new workbox.strategies.NetworkOnly() 274); 275workbox.routing.registerRoute( 276 ({url}) => url.pathname.toLowerCase().startsWith('/eventplatform/meeting/'), 277 new workbox.strategies.NetworkOnly() 278); 279 280// Shared, user-agnostic content - cached for offline, never purged on auth. 281workbox.routing.registerRoute( 282 ({url}) => url.pathname.toLowerCase().startsWith('/eventplatform/programme/loadlistpartialview'), 283 epNetworkFirst('ep-programme-partials') 284); 285workbox.routing.registerRoute( 286 ({url}) => { 287 const path = url.pathname.toLowerCase(); 288 return path.startsWith('/eventplatform/exhibitors') 289 || path.startsWith('/eventplatform/map') 290 || path.startsWith('/eventplatform/guide'); 291 }, 292 epNetworkFirst('ep-content') 293); 294 295// Everything else under /EventPlatform/ is per-user - cached for offline in the purgeable bucket. 296workbox.routing.registerRoute( 297 ({url}) => url.pathname.toLowerCase().startsWith('/eventplatform/'), 298 epNetworkFirst('ep-personal') 299); 300 301// On activate, delete buckets no strategy writes to (VirtualEvent-era names + the unnamed runtime 302// cache) and caches that can hold timestamp-less entries ExpirationPlugin can never prune 303// ('schedule'/'programme' - both repopulate fresh). Remove the VE names when that area is deleted. 304// workbox-sw lazy-loads modules via importScripts on first property access, which browsers disallow 305// inside event handlers after install - so resolve the runtime cache name at evaluation time. 306const workboxRuntimeCacheName = workbox.core.cacheNames.runtime; 307self.addEventListener('activate', (event) => { 308 const retiredCaches = ['splashScreen', 'exhibitorList', 'exhibitorsDetails', 'credits', 'helpDesk', 'map', 'highlights', 309 workboxRuntimeCacheName, 'schedule', 'programme']; 310 event.waitUntil(Promise.all(retiredCaches.map((name) => caches.delete(name)))); 311});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.