Skip to content

Commit 65099e6

Browse files
committed
add back game worker (improved thanks claude)
1 parent affcf59 commit 65099e6

1 file changed

Lines changed: 384 additions & 0 deletions

File tree

static/game_worker.js

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
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+
/game_/, // Files containing 'game_'
14+
/\.data$/, // Unity WebGL data files
15+
/\.wasm$/, // WebAssembly files
16+
/\.bundle$/, // Asset bundles
17+
/\.unity3d$/, // Unity asset files
18+
/\.pak$/, // Package files
19+
/\.bin$/, // Binary files
20+
/\.spritemap$/, // Sprite atlases
21+
/assets\//, // Assets directory
22+
/gamedata\//, // 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

Comments
 (0)