1+ // service-worker.js - Enhanced for cross-origin caching
2+ const CACHE_NAME = 'ccported-cache-v1' ;
3+ const CACHE_METADATA_KEY = 'ccported-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+ './index.html' ,
9+ ] ;
10+
11+ // Add the domains you want to cache from
12+ const ALLOWED_DOMAINS = [
13+ 'ccgstatic.com' ,
14+ // Add other domains here as needed
15+ ] ;
16+ const BLACKLIST = [
17+ "pagead2.googlesyndication.com" ,
18+ "storage.ko-fi.com" ,
19+ "www.google-analytics.com" ,
20+ "amazonaws.com"
21+ ]
22+
23+ // Install event - precache critical resources
24+ self . addEventListener ( 'install' , event => {
25+ fetch ( "/servers.txt" ) . then ( response => response . text ( ) ) . then ( text => {
26+ const servers = text . split ( '\n' ) . map ( line => line . split ( "," ) [ 0 ] . trim ( ) ) . filter ( line => line . length > 0 ) ;
27+ ALLOWED_DOMAINS . push ( ...servers ) ;
28+ } ) ;
29+ event . waitUntil (
30+ caches . open ( CACHE_NAME )
31+ . then ( cache => {
32+ console . log ( 'Precaching game assets' ) ;
33+ return cache . addAll ( PRECACHE_ASSETS ) ;
34+ } )
35+ . then ( ( ) => self . skipWaiting ( ) )
36+ ) ;
37+ } ) ;
38+
39+ // Activate event - clean up old caches
40+ self . addEventListener ( 'activate' , event => {
41+ event . waitUntil (
42+ caches . keys ( ) . then ( cacheNames => {
43+ return Promise . all (
44+ cacheNames . filter ( cacheName => {
45+ return cacheName . startsWith ( 'ccported-cache-' ) &&
46+ cacheName !== CACHE_NAME ;
47+ } ) . map ( cacheName => {
48+ return caches . delete ( cacheName ) ;
49+ } )
50+ ) ;
51+ } ) . then ( ( ) => self . clients . claim ( ) )
52+ ) ;
53+ } ) ;
54+
55+ // Helper function to determine if a request should be cached
56+ function isCacheableRequest ( request ) {
57+ const url = new URL ( request . url ) ;
58+
59+ // Never cache txt files, change often
60+ if ( url . pathname . endsWith ( '.txt' ) ) {
61+ return false ;
62+ }
63+
64+ // Only cache GET requests
65+ if ( request . method !== 'GET' ) {
66+ return false ;
67+ }
68+
69+ // Check if domain is allowed for cross-origin caching
70+ const isAllowedDomain = ALLOWED_DOMAINS . some ( domain => url . hostname . includes ( domain ) ) ;
71+ const isBlacklisted = BLACKLIST . some ( domain => url . hostname . includes ( domain ) ) ;
72+ if ( isBlacklisted ) {
73+ return false ;
74+ }
75+ // Allow caching for origin domain or explicitly allowed domains
76+ const isSameOrigin = url . origin === self . location . origin ;
77+ if ( ! isSameOrigin && ! isAllowedDomain ) {
78+ return false ;
79+ }
80+
81+ // Cache based on file extensions (website - non game- assets)
82+ const extensions = [
83+ '.html' , '.js' , '.css' , '.json' ,
84+ '.png' , '.jpg' , '.jpeg' , '.gif' , '.webp' , '.svg' ,
85+ '.woff' , '.woff2' , '.ttf' , '.otf' ,
86+ '.mp3' , '.ogg' , '.wav' ,
87+ '.mp4' , '.webm'
88+ ] ;
89+
90+ if ( extensions . some ( ext => url . pathname . endsWith ( ext ) ) ) {
91+ return true ;
92+ }
93+
94+ return false ;
95+ }
96+
97+ // Network-first strategy with fallback to cache
98+ async function networkFirstStrategy ( request ) {
99+ try {
100+ // For cross-origin requests that need credentials, use appropriate fetch options
101+ const fetchOptions = { } ;
102+ if ( isCrossOrigin ( request ) ) {
103+ fetchOptions . mode = 'cors' ;
104+ // Don't send credentials by default for cross-origin requests
105+ // unless you explicitly need them
106+ fetchOptions . credentials = 'same-origin' ;
107+ }
108+
109+ // Try network first
110+ const networkResponse = await fetch ( request , fetchOptions ) ;
111+
112+ // If successful, clone and cache
113+ if ( networkResponse . ok ) {
114+ const cache = await caches . open ( CACHE_NAME ) ;
115+ cache . put ( request , networkResponse . clone ( ) ) ;
116+ return networkResponse ;
117+ }
118+
119+ // If network fails with an error status, try cache
120+ throw new Error ( 'Network response was not ok' ) ;
121+ } catch ( error ) {
122+ // Fall back to cache
123+ const cachedResponse = await caches . match ( request ) ;
124+ if ( cachedResponse ) {
125+ return cachedResponse ;
126+ }
127+
128+ // Nothing in cache, return the error response
129+ throw error ;
130+ }
131+ }
132+
133+ // Helper function to check if request is cross-origin
134+ function isCrossOrigin ( request ) {
135+ const url = new URL ( request . url ) ;
136+ return url . origin !== self . location . origin ;
137+ }
138+
139+ // Cache-first strategy with network fallback
140+ async function cacheFirstStrategy ( request ) {
141+ // Try the cache first
142+ const cachedResponse = await caches . match ( request ) ;
143+ if ( cachedResponse ) {
144+ return cachedResponse ;
145+ }
146+
147+ // If not in cache, get from network with appropriate options
148+ try {
149+ const fetchOptions = { } ;
150+ if ( isCrossOrigin ( request ) ) {
151+ fetchOptions . mode = 'cors' ;
152+ fetchOptions . credentials = 'same-origin' ;
153+ }
154+
155+ const networkResponse = await fetch ( request , fetchOptions ) ;
156+
157+ // Cache the network response for future
158+ if ( networkResponse . ok ) {
159+ const cache = await caches . open ( CACHE_NAME ) ;
160+ cache . put ( request , networkResponse . clone ( ) ) ;
161+ }
162+
163+ return networkResponse ;
164+ } catch ( error ) {
165+ // Handle fetch failure
166+ console . error ( 'Fetch failed:' , error ) ;
167+ throw error ;
168+ }
169+ }
170+
171+ // Stale-while-revalidate strategy
172+ async function staleWhileRevalidateStrategy ( request ) {
173+ // Try to get from cache immediately
174+ const cachedResponse = await caches . match ( request ) ;
175+
176+ // Fetch from network and update cache in the background
177+ const fetchOptions = { } ;
178+ if ( isCrossOrigin ( request ) ) {
179+ fetchOptions . mode = 'cors' ;
180+ fetchOptions . credentials = 'same-origin' ;
181+ }
182+
183+ const fetchPromise = fetch ( request , fetchOptions ) . then ( networkResponse => {
184+ if ( networkResponse . ok ) {
185+ const cache = caches . open ( CACHE_NAME ) . then ( cache => {
186+ cache . put ( request , networkResponse . clone ( ) ) ;
187+ return networkResponse ;
188+ } ) ;
189+ }
190+ return networkResponse ;
191+ } ) . catch ( error => {
192+ console . error ( 'Background fetch failed:' , error ) ;
193+ } ) ;
194+
195+ // Return the cached response immediately if we have it
196+ return cachedResponse || fetchPromise ;
197+ }
198+
199+ // Time-aware cache-first strategy with network fallback
200+ async function timeAwareCacheFirstStrategy ( request ) {
201+ // Check if we have a cached version first
202+ const cachedResponse = await caches . match ( request . url ) ;
203+ // Determine if the cached file is stale
204+ const isStale = await isFileStale ( request . url ) ;
205+
206+ // If we have a non-stale cached response, return it
207+ if ( cachedResponse && ! isStale ) {
208+ return cachedResponse ;
209+ }
210+ console . log ( request . url , isStale )
211+
212+ // Otherwise, get from network (either no cache or stale cache)
213+ try {
214+ const fetchOptions = { } ;
215+ if ( isCrossOrigin ( request ) ) {
216+ fetchOptions . mode = 'cors' ;
217+ fetchOptions . credentials = 'same-origin' ;
218+ }
219+
220+ const networkResponse = await fetch ( request , fetchOptions ) ;
221+
222+ // Cache the network response for future
223+ if ( networkResponse . ok ) {
224+ const cache = await caches . open ( CACHE_NAME ) ;
225+ await cache . put ( request , networkResponse . clone ( ) ) ;
226+ await updateCacheTimestamp ( request . url ) ;
227+ }
228+
229+ return networkResponse ;
230+ } catch ( error ) {
231+ // If network fails and we have a cached version (even if stale), return it
232+ if ( cachedResponse ) {
233+ return cachedResponse ;
234+ }
235+
236+ // No cached fallback available
237+ console . error ( 'Fetch failed:' , error ) ;
238+ throw error ;
239+ }
240+ }
241+
242+ // Helper function to save cache metadata
243+ async function saveMetadataStore ( metadata ) {
244+ const cache = await caches . open ( CACHE_NAME ) ;
245+ const metadataBlob = new Blob ( [ JSON . stringify ( metadata ) ] , { type : 'application/json' } ) ;
246+ const metadataResponse = new Response ( metadataBlob ) ;
247+ await cache . put ( CACHE_METADATA_KEY , metadataResponse ) ;
248+ }
249+
250+ // Helper function to update timestamp for a cached file
251+ async function updateCacheTimestamp ( url ) {
252+ const metadata = await getMetadataStore ( ) ;
253+ metadata [ url ] = Date . now ( ) ;
254+ await saveMetadataStore ( metadata ) ;
255+ }
256+
257+ async function isFileStale ( url ) {
258+ const metadata = await getMetadataStore ( ) ;
259+ const timestamp = metadata [ url ] ;
260+
261+ if ( ! timestamp ) {
262+ console . log ( "No timestamp for" , url ) ;
263+ return true ; // No timestamp means we should revalidate
264+ }
265+
266+ const now = Date . now ( ) ;
267+ const age = now - timestamp ;
268+ const maxAgeMs = MAX_AGE_DAYS * 24 * 60 * 60 * 1000 ; // Convert days to milliseconds
269+ return age > maxAgeMs ;
270+ }
271+
272+ // Helper function to get the cache metadata store
273+ async function getMetadataStore ( ) {
274+ const cache = await caches . open ( CACHE_NAME ) ;
275+ const metadataResponse = await cache . match ( CACHE_METADATA_KEY ) ;
276+
277+ if ( metadataResponse ) {
278+ return metadataResponse . json ( ) ;
279+ } else {
280+ // Initialize with empty metadata if none exists
281+ return { } ;
282+ }
283+ }
284+
285+ // Fetch event - handle all requests
286+ self . addEventListener ( 'fetch' , event => {
287+ // Ignore non-GET requests
288+ if ( event . request . method !== 'GET' ) return ;
289+
290+ const request = event . request ;
291+
292+ // Choose caching strategy based on request type
293+ if ( isCacheableRequest ( request ) ) {
294+ // For image assets from external domains, use cache-first
295+ const url = new URL ( request . url ) ;
296+ const isImage = / \. ( p n g | j p g | j p e g | g i f | w e b p | s v g ) $ / i. test ( url . pathname ) ;
297+ const isExternalDomain = ALLOWED_DOMAINS . some ( domain => url . hostname . includes ( domain ) ) ;
298+
299+ if ( isImage && isExternalDomain ) {
300+ event . respondWith ( timeAwareCacheFirstStrategy ( request ) ) ;
301+ }
302+ // For game assets, use cache-first
303+ else if ( request . url . includes ( 'game_' ) ) {
304+ event . respondWith ( cacheFirstStrategy ( request ) ) ;
305+ }
306+ // For HTML and JSON files, use network-first to get latest versions
307+ else if ( request . url . endsWith ( '.html' ) || request . url . endsWith ( '.json' ) || request . url . endsWith ( '.txt' ) ) {
308+ event . respondWith ( networkFirstStrategy ( request ) ) ;
309+ }
310+ // For everything else cacheable, use time-aware cache
311+ else {
312+ event . respondWith ( timeAwareCacheFirstStrategy ( request ) ) ;
313+ }
314+ }
315+ // Let non-cacheable requests go through without service worker intervention
316+ } ) ;
317+
318+ // Listen for messages from the main thread
319+ self . addEventListener ( 'message' , event => {
320+ // Handle custom cache invalidation
321+ if ( event . data && event . data . action === 'CLEAR_CACHE' ) {
322+ caches . delete ( CACHE_NAME ) . then ( ( ) => {
323+ event . ports [ 0 ] . postMessage ( { status : 'Cache cleared' } ) ;
324+ } ) ;
325+ }
326+ } ) ;
0 commit comments