1+ // service-worker.js - This should be placed on the game server
2+ const CACHE_NAME = 'game-cache-v1' ;
3+ const CACHE_METADATA_KEY = 'game-cache-metadata' ;
4+ const MAX_AGE_DAYS = 7 ; // Revalidate files older than 7 days
5+
6+ // Assets to cache immediately on service worker installation
7+ const PRECACHE_ASSETS = [
8+ // Add critical game assets here if needed
9+ ] ;
10+
11+ // Game file patterns that should be aggressively cached
12+ const GAME_FILE_PATTERNS = [
13+ / g a m e _ / , // Files containing 'game_'
14+ / \. d a t a $ / , // Unity WebGL data files
15+ / \. w a s m $ / , // WebAssembly files
16+ / \. b u n d l e $ / , // Asset bundles
17+ / \. u n i t y 3 d $ / , // Unity asset files
18+ / \. p a k $ / , // Package files
19+ / \. b i n $ / , // Binary files
20+ / \. s p r i t e m a p $ / , // Sprite atlases
21+ / a s s e t s \/ / , // Assets directory
22+ / g a m e d a t a \/ / , // Game data directory
23+ ] ;
24+
25+ // Helper function to identify large/important game files
26+ function isGameFile ( url ) {
27+ const urlObj = new URL ( url ) ;
28+ return GAME_FILE_PATTERNS . some ( pattern => pattern . test ( urlObj . pathname ) ) ;
29+ }
30+
31+ // Helper function to get the cache metadata store
32+ async function getMetadataStore ( ) {
33+ try {
34+ const cache = await caches . open ( CACHE_NAME ) ;
35+ const metadataResponse = await cache . match ( CACHE_METADATA_KEY ) ;
36+
37+ if ( metadataResponse ) {
38+ return await metadataResponse . json ( ) ;
39+ } else {
40+ // Initialize with empty metadata if none exists
41+ return { } ;
42+ }
43+ } catch ( error ) {
44+ console . error ( 'Error getting metadata store:' , error ) ;
45+ return { } ;
46+ }
47+ }
48+
49+ // Helper function to save cache metadata
50+ async function saveMetadataStore ( metadata ) {
51+ try {
52+ const cache = await caches . open ( CACHE_NAME ) ;
53+ const metadataBlob = new Blob ( [ JSON . stringify ( metadata ) ] , { type : 'application/json' } ) ;
54+ const metadataResponse = new Response ( metadataBlob ) ;
55+ await cache . put ( CACHE_METADATA_KEY , metadataResponse ) ;
56+ } catch ( error ) {
57+ console . error ( 'Error saving metadata store:' , error ) ;
58+ }
59+ }
60+
61+ // Helper function to update timestamp for a cached file
62+ async function updateCacheTimestamp ( url ) {
63+ try {
64+ const metadata = await getMetadataStore ( ) ;
65+ metadata [ url ] = Date . now ( ) ;
66+ await saveMetadataStore ( metadata ) ;
67+ } catch ( error ) {
68+ console . error ( 'Error updating cache timestamp:' , error ) ;
69+ }
70+ }
71+
72+ // Helper function to check if a cached file is stale (older than MAX_AGE_DAYS)
73+ async function isFileStale ( url ) {
74+ try {
75+ const metadata = await getMetadataStore ( ) ;
76+ const timestamp = metadata [ url ] ;
77+
78+ if ( ! timestamp ) {
79+ return true ; // No timestamp means we should revalidate
80+ }
81+
82+ const now = Date . now ( ) ;
83+ const age = now - timestamp ;
84+ const maxAgeMs = MAX_AGE_DAYS * 24 * 60 * 60 * 1000 ; // Convert days to milliseconds
85+
86+ return age > maxAgeMs ;
87+ } catch ( error ) {
88+ console . error ( 'Error checking if file is stale:' , error ) ;
89+ return true ; // Assume stale on error
90+ }
91+ }
92+
93+ // Helper function to check if response is valid for caching
94+ function isValidResponse ( response ) {
95+ return response &&
96+ response . ok &&
97+ response . status >= 200 &&
98+ response . status < 300 &&
99+ response . status !== 206 && // Partial content
100+ response . status !== 209 ; // Contents of Related
101+ }
102+
103+ // Install event - precache critical resources
104+ self . addEventListener ( 'install' , event => {
105+ event . waitUntil (
106+ caches . open ( CACHE_NAME )
107+ . then ( async cache => {
108+ console . log ( 'Precaching game assets' ) ;
109+ if ( PRECACHE_ASSETS . length > 0 ) {
110+ await cache . addAll ( PRECACHE_ASSETS ) ;
111+
112+ // Initialize timestamps for precached assets
113+ const metadata = await getMetadataStore ( ) ;
114+ const now = Date . now ( ) ;
115+
116+ PRECACHE_ASSETS . forEach ( asset => {
117+ const url = new URL ( asset , self . location . origin ) . href ;
118+ metadata [ url ] = now ;
119+ } ) ;
120+
121+ await saveMetadataStore ( metadata ) ;
122+ }
123+ return self . skipWaiting ( ) ;
124+ } )
125+ . catch ( error => {
126+ console . error ( 'Failed to precache assets:' , error ) ;
127+ return self . skipWaiting ( ) ;
128+ } )
129+ ) ;
130+ } ) ;
131+
132+ // Activate event - clean up old caches
133+ self . addEventListener ( 'activate' , event => {
134+ event . waitUntil (
135+ caches . keys ( ) . then ( cacheNames => {
136+ return Promise . all (
137+ cacheNames . filter ( cacheName => {
138+ return cacheName . startsWith ( 'game-cache-' ) &&
139+ cacheName !== CACHE_NAME ;
140+ } ) . map ( cacheName => {
141+ console . log ( 'Deleting old cache:' , cacheName ) ;
142+ return caches . delete ( cacheName ) ;
143+ } )
144+ ) ;
145+ } ) . then ( ( ) => self . clients . claim ( ) )
146+ ) ;
147+ } ) ;
148+
149+ // Helper function to determine if a request should be cached
150+ function isCacheableRequest ( request ) {
151+ const url = new URL ( request . url ) ;
152+
153+ // Check for cache-busting query parameters
154+ const params = new URLSearchParams ( url . search ) ;
155+ if ( params . has ( 'cache' ) && params . get ( 'cache' ) === 'false' ||
156+ params . has ( 'cacheBust' ) ||
157+ params . has ( 'cachebust' ) ||
158+ params . has ( 'bust' ) ||
159+ params . has ( 'v' ) ||
160+ params . has ( 'version' ) ) {
161+ return false ; // Cache-busting query parameter
162+ }
163+
164+ // Never cache txt files, change often
165+ if ( url . pathname . endsWith ( '.txt' ) ) {
166+ return false ;
167+ }
168+
169+ // Only cache GET requests
170+ if ( request . method !== 'GET' ) {
171+ return false ;
172+ }
173+
174+ // Don't cache if it has no-cache header
175+ if ( request . headers . get ( 'cache-control' ) === 'no-cache' ) {
176+ return false ;
177+ }
178+
179+ return true ;
180+ }
181+
182+ // Network-first strategy with fallback to cache
183+ async function networkFirstStrategy ( request ) {
184+ try {
185+ // Try network first
186+ const networkResponse = await fetch ( request ) ;
187+
188+ // If successful and valid, clone and cache
189+ if ( isValidResponse ( networkResponse ) ) {
190+ const cache = await caches . open ( CACHE_NAME ) ;
191+ await cache . put ( request , networkResponse . clone ( ) ) ;
192+ await updateCacheTimestamp ( request . url ) ;
193+ return networkResponse ;
194+ }
195+
196+ // If network response is not valid for caching, try cache
197+ const cachedResponse = await caches . match ( request ) ;
198+ if ( cachedResponse ) {
199+ return cachedResponse ;
200+ }
201+
202+ // Return the network response even if not cacheable
203+ return networkResponse ;
204+ } catch ( error ) {
205+ // Fall back to cache
206+ const cachedResponse = await caches . match ( request ) ;
207+ if ( cachedResponse ) {
208+ return cachedResponse ;
209+ }
210+
211+ // Nothing in cache, rethrow the error
212+ throw error ;
213+ }
214+ }
215+
216+ // Time-aware cache-first strategy with network fallback
217+ async function timeAwareCacheFirstStrategy ( request ) {
218+ // Check if we have a cached version first
219+ const cachedResponse = await caches . match ( request ) ;
220+
221+ // Determine if the cached file is stale
222+ const isStale = await isFileStale ( request . url ) ;
223+
224+ // If we have a non-stale cached response, return it
225+ if ( cachedResponse && ! isStale ) {
226+ console . log ( '✅ Served fresh from cache:' , request . url ) ;
227+ return cachedResponse ;
228+ }
229+
230+ console . log ( '⚠️ Cache miss or stale, fetching from network:' , request . url , {
231+ hasCached : ! ! cachedResponse ,
232+ isStale
233+ } ) ;
234+
235+ // Otherwise, get from network (either no cache or stale cache)
236+ try {
237+ const networkResponse = await fetch ( request ) ;
238+
239+ // Cache the network response for future if it's valid
240+ if ( isValidResponse ( networkResponse ) ) {
241+ const cache = await caches . open ( CACHE_NAME ) ;
242+ await cache . put ( request , networkResponse . clone ( ) ) ;
243+ await updateCacheTimestamp ( request . url ) ;
244+ console . log ( '✅ Updated cache from network:' , request . url ) ;
245+ }
246+
247+ return networkResponse ;
248+ } catch ( error ) {
249+ // If network fails and we have a cached version (even if stale), return it
250+ if ( cachedResponse ) {
251+ console . log ( '✅ Served stale cache after network failure:' , request . url ) ;
252+ return cachedResponse ;
253+ }
254+
255+ // No cached fallback available
256+ console . error ( '❌ Network failed, no cache available for:' , request . url ) ;
257+ throw error ;
258+ }
259+ }
260+
261+ // Stale-while-revalidate strategy with timestamp awareness
262+ async function timeAwareStaleWhileRevalidateStrategy ( request ) {
263+ // Get from cache immediately
264+ const cachedResponse = await caches . match ( request ) ;
265+
266+ // Check if the cached file is stale
267+ const isStale = await isFileStale ( request . url ) ;
268+
269+ // If cachedResponse exists, fetch from network only if it's stale
270+ if ( cachedResponse ) {
271+ if ( isStale ) {
272+ console . log ( '🔄 Serving stale cache while revalidating:' , request . url ) ;
273+ // Fetch from network and update cache in the background
274+ fetch ( request ) . then ( async networkResponse => {
275+ if ( isValidResponse ( networkResponse ) ) {
276+ const cache = await caches . open ( CACHE_NAME ) ;
277+ await cache . put ( request , networkResponse . clone ( ) ) ;
278+ await updateCacheTimestamp ( request . url ) ;
279+ console . log ( '🔄 Background revalidation complete:' , request . url ) ;
280+ }
281+ } ) . catch ( error => {
282+ console . error ( '❌ Background revalidation failed:' , request . url , error ) ;
283+ } ) ;
284+ } else {
285+ console . log ( '✅ Served fresh from cache (SWR):' , request . url ) ;
286+ }
287+
288+ // Return the cached response immediately
289+ return cachedResponse ;
290+ } else {
291+ console . log ( '⚠️ No cache, fetching from network (SWR):' , request . url ) ;
292+ // No cached response, fetch from network
293+ const networkResponse = await fetch ( request ) ;
294+
295+ if ( isValidResponse ( networkResponse ) ) {
296+ // Cache the response for future
297+ const cache = await caches . open ( CACHE_NAME ) ;
298+ await cache . put ( request , networkResponse . clone ( ) ) ;
299+ await updateCacheTimestamp ( request . url ) ;
300+ console . log ( '✅ Cached new response (SWR):' , request . url ) ;
301+ }
302+
303+ return networkResponse ;
304+ }
305+ }
306+
307+ // Fetch event - handle all requests
308+ self . addEventListener ( 'fetch' , event => {
309+ // Ignore non-GET requests
310+ if ( event . request . method !== 'GET' ) return ;
311+
312+ const request = event . request ;
313+
314+ // Choose caching strategy based on request type
315+ if ( isCacheableRequest ( request ) ) {
316+ const url = new URL ( request . url ) ;
317+
318+ // For large game files, use aggressive cache-first strategy
319+ if ( isGameFile ( request . url ) ) {
320+ console . log ( '🎮 Game file detected:' , request . url ) ;
321+ event . respondWith ( timeAwareCacheFirstStrategy ( request ) ) ;
322+ }
323+ // For HTML and JSON files, use network-first to get latest versions
324+ else if ( url . pathname . endsWith ( '.html' ) || url . pathname . endsWith ( '.json' ) ) {
325+ console . log ( '📄 Dynamic content detected:' , request . url ) ;
326+ event . respondWith ( networkFirstStrategy ( request ) ) ;
327+ }
328+ // For JS files, use network-first to ensure updates
329+ else if ( url . pathname . endsWith ( '.js' ) ) {
330+ console . log ( '📜 JavaScript file detected:' , request . url ) ;
331+ event . respondWith ( networkFirstStrategy ( request ) ) ;
332+ }
333+ // For everything else cacheable, use time-aware stale-while-revalidate
334+ else {
335+ console . log ( '🗂️ Static asset detected:' , request . url ) ;
336+ event . respondWith ( timeAwareStaleWhileRevalidateStrategy ( request ) ) ;
337+ }
338+ }
339+ // Let non-cacheable requests go through without service worker intervention
340+ } ) ;
341+
342+ // Listen for messages from the main thread
343+ self . addEventListener ( 'message' , event => {
344+ // Handle custom cache invalidation
345+ if ( event . data && event . data . action === 'CLEAR_CACHE' ) {
346+ console . log ( '🗑️ Clearing all caches...' ) ;
347+ caches . delete ( CACHE_NAME ) . then ( ( ) => {
348+ event . ports [ 0 ] . postMessage ( { status : 'Cache cleared' } ) ;
349+ } ) ;
350+ }
351+ // Handle force revalidation of all assets
352+ else if ( event . data && event . data . action === 'REVALIDATE_ALL' ) {
353+ console . log ( '🔄 Marking all assets for revalidation...' ) ;
354+ getMetadataStore ( ) . then ( metadata => {
355+ // Set all timestamps to 0 to force revalidation
356+ Object . keys ( metadata ) . forEach ( url => {
357+ if ( url !== CACHE_METADATA_KEY ) {
358+ metadata [ url ] = 0 ;
359+ }
360+ } ) ;
361+ return saveMetadataStore ( metadata ) ;
362+ } ) . then ( ( ) => {
363+ event . ports [ 0 ] . postMessage ( { status : 'All assets marked for revalidation' } ) ;
364+ } ) ;
365+ }
366+ // Handle selective cache invalidation
367+ else if ( event . data && event . data . action === 'INVALIDATE_URL' ) {
368+ const url = event . data . url ;
369+ console . log ( '🗑️ Invalidating specific URL:' , url ) ;
370+ caches . open ( CACHE_NAME ) . then ( cache => {
371+ return cache . delete ( url ) ;
372+ } ) . then ( success => {
373+ if ( success ) {
374+ // Also remove from metadata
375+ return getMetadataStore ( ) . then ( metadata => {
376+ delete metadata [ url ] ;
377+ return saveMetadataStore ( metadata ) ;
378+ } ) ;
379+ }
380+ } ) . then ( ( ) => {
381+ event . ports [ 0 ] . postMessage ( { status : `Invalidated ${ url } ` } ) ;
382+ } ) ;
383+ }
384+ } ) ;
0 commit comments