1+ /**
2+ * JSONIC Service Worker
3+ * Provides offline-first caching with progressive loading
4+ */
5+
6+ const CACHE_VERSION = 'jsonic-v1.0.0' ;
7+ const CORE_CACHE = `${ CACHE_VERSION } -core` ;
8+ const FEATURE_CACHE = `${ CACHE_VERSION } -features` ;
9+ const DATA_CACHE = `${ CACHE_VERSION } -data` ;
10+ const STATIC_CACHE = `${ CACHE_VERSION } -static` ;
11+
12+ // Core files that should always be cached
13+ const CORE_FILES = [
14+ '/' ,
15+ '/index.html' ,
16+ '/manifest.json' ,
17+ '/favicon.ico' ,
18+ '/static/js/bundle.js' ,
19+ '/static/css/main.css'
20+ ] ;
21+
22+ // Feature chunks that can be cached on-demand
23+ const FEATURE_PATTERNS = [
24+ / \/ s t a t i c \/ j s \/ j s o n i c - .* \. c h u n k \. j s $ / ,
25+ / \/ s t a t i c \/ j s \/ \d + \. \w + \. c h u n k \. j s $ / ,
26+ ] ;
27+
28+ // API endpoints to cache
29+ const API_PATTERNS = [
30+ / \/ a p i \/ b e n c h m a r k s \/ / ,
31+ / \/ a p i \/ c o n f i g \/ / ,
32+ ] ;
33+
34+ // Install event - pre-cache core files
35+ self . addEventListener ( 'install' , ( event ) => {
36+ console . log ( '[Service Worker] Installing...' ) ;
37+
38+ event . waitUntil (
39+ caches . open ( CORE_CACHE ) . then ( ( cache ) => {
40+ console . log ( '[Service Worker] Pre-caching core files' ) ;
41+ return cache . addAll ( CORE_FILES . filter ( file => {
42+ // Only cache files that exist
43+ return fetch ( file , { method : 'HEAD' } )
44+ . then ( ( ) => true )
45+ . catch ( ( ) => false ) ;
46+ } ) ) ;
47+ } ) . then ( ( ) => {
48+ // Skip waiting to activate immediately
49+ return self . skipWaiting ( ) ;
50+ } )
51+ ) ;
52+ } ) ;
53+
54+ // Activate event - clean up old caches
55+ self . addEventListener ( 'activate' , ( event ) => {
56+ console . log ( '[Service Worker] Activating...' ) ;
57+
58+ event . waitUntil (
59+ caches . keys ( ) . then ( ( cacheNames ) => {
60+ return Promise . all (
61+ cacheNames . map ( ( cacheName ) => {
62+ // Delete old version caches
63+ if ( cacheName . startsWith ( 'jsonic-' ) &&
64+ ! cacheName . startsWith ( CACHE_VERSION ) ) {
65+ console . log ( '[Service Worker] Deleting old cache:' , cacheName ) ;
66+ return caches . delete ( cacheName ) ;
67+ }
68+ } )
69+ ) ;
70+ } ) . then ( ( ) => {
71+ // Take control of all clients immediately
72+ return self . clients . claim ( ) ;
73+ } )
74+ ) ;
75+ } ) ;
76+
77+ // Fetch event - serve from cache or network
78+ self . addEventListener ( 'fetch' , ( event ) => {
79+ const { request } = event ;
80+ const url = new URL ( request . url ) ;
81+
82+ // Skip non-GET requests
83+ if ( request . method !== 'GET' ) {
84+ return ;
85+ }
86+
87+ // Skip chrome-extension and other non-http(s) protocols
88+ if ( ! request . url . startsWith ( 'http' ) ) {
89+ return ;
90+ }
91+
92+ event . respondWith ( handleFetch ( request , url ) ) ;
93+ } ) ;
94+
95+ /**
96+ * Handle fetch requests with appropriate caching strategy
97+ */
98+ async function handleFetch ( request , url ) {
99+ // Check if it's a core file
100+ if ( CORE_FILES . includes ( url . pathname ) ) {
101+ return cacheFirst ( request , CORE_CACHE ) ;
102+ }
103+
104+ // Check if it's a feature chunk
105+ if ( FEATURE_PATTERNS . some ( pattern => pattern . test ( url . pathname ) ) ) {
106+ return cacheFirst ( request , FEATURE_CACHE ) ;
107+ }
108+
109+ // Check if it's an API endpoint
110+ if ( API_PATTERNS . some ( pattern => pattern . test ( url . pathname ) ) ) {
111+ return networkFirst ( request , DATA_CACHE ) ;
112+ }
113+
114+ // Check if it's a static asset
115+ if ( url . pathname . startsWith ( '/static/' ) ||
116+ url . pathname . match ( / \. ( j s | c s s | p n g | j p g | j p e g | g i f | s v g | w o f f | w o f f 2 ) $ / ) ) {
117+ return cacheFirst ( request , STATIC_CACHE ) ;
118+ }
119+
120+ // Default: network first for HTML and other content
121+ return networkFirst ( request , STATIC_CACHE ) ;
122+ }
123+
124+ /**
125+ * Cache-first strategy
126+ * Try cache first, fallback to network
127+ */
128+ async function cacheFirst ( request , cacheName ) {
129+ const cache = await caches . open ( cacheName ) ;
130+
131+ // Try to get from cache
132+ const cachedResponse = await cache . match ( request ) ;
133+ if ( cachedResponse ) {
134+ // Update cache in background
135+ fetchAndCache ( request , cache ) ;
136+ return cachedResponse ;
137+ }
138+
139+ // Not in cache, fetch from network
140+ try {
141+ const networkResponse = await fetch ( request ) ;
142+
143+ // Cache successful responses
144+ if ( networkResponse . ok ) {
145+ cache . put ( request , networkResponse . clone ( ) ) ;
146+ }
147+
148+ return networkResponse ;
149+ } catch ( error ) {
150+ console . error ( '[Service Worker] Fetch failed:' , error ) ;
151+
152+ // Return offline page if available
153+ const offlineResponse = await cache . match ( '/offline.html' ) ;
154+ if ( offlineResponse ) {
155+ return offlineResponse ;
156+ }
157+
158+ throw error ;
159+ }
160+ }
161+
162+ /**
163+ * Network-first strategy
164+ * Try network first, fallback to cache
165+ */
166+ async function networkFirst ( request , cacheName ) {
167+ const cache = await caches . open ( cacheName ) ;
168+
169+ try {
170+ const networkResponse = await fetch ( request ) ;
171+
172+ // Cache successful responses
173+ if ( networkResponse . ok ) {
174+ cache . put ( request , networkResponse . clone ( ) ) ;
175+ }
176+
177+ return networkResponse ;
178+ } catch ( error ) {
179+ // Network failed, try cache
180+ const cachedResponse = await cache . match ( request ) ;
181+ if ( cachedResponse ) {
182+ console . log ( '[Service Worker] Serving from cache:' , request . url ) ;
183+ return cachedResponse ;
184+ }
185+
186+ // No cache available
187+ console . error ( '[Service Worker] Network and cache failed:' , error ) ;
188+ throw error ;
189+ }
190+ }
191+
192+ /**
193+ * Fetch and update cache in background
194+ */
195+ async function fetchAndCache ( request , cache ) {
196+ try {
197+ const response = await fetch ( request ) ;
198+ if ( response . ok ) {
199+ cache . put ( request , response ) ;
200+ }
201+ } catch ( error ) {
202+ // Silent fail - we already returned from cache
203+ }
204+ }
205+
206+ // Message handling for feature pre-caching
207+ self . addEventListener ( 'message' , ( event ) => {
208+ const { type, data } = event . data ;
209+
210+ switch ( type ) {
211+ case 'PRE_CACHE_FEATURE' :
212+ preCacheFeature ( data . feature ) ;
213+ break ;
214+
215+ case 'GET_CACHE_STATUS' :
216+ getCacheStatus ( ) . then ( status => {
217+ event . ports [ 0 ] . postMessage ( status ) ;
218+ } ) ;
219+ break ;
220+
221+ case 'CLEAR_CACHE' :
222+ clearAllCaches ( ) ;
223+ break ;
224+
225+ case 'SKIP_WAITING' :
226+ self . skipWaiting ( ) ;
227+ break ;
228+ }
229+ } ) ;
230+
231+ /**
232+ * Pre-cache a feature module
233+ */
234+ async function preCacheFeature ( feature ) {
235+ const cache = await caches . open ( FEATURE_CACHE ) ;
236+ const featureUrl = `/static/js/jsonic-${ feature } .chunk.js` ;
237+
238+ try {
239+ const response = await fetch ( featureUrl ) ;
240+ if ( response . ok ) {
241+ await cache . put ( featureUrl , response ) ;
242+ console . log ( `[Service Worker] Pre-cached feature: ${ feature } ` ) ;
243+ }
244+ } catch ( error ) {
245+ console . error ( `[Service Worker] Failed to pre-cache feature ${ feature } :` , error ) ;
246+ }
247+ }
248+
249+ /**
250+ * Get cache statistics
251+ */
252+ async function getCacheStatus ( ) {
253+ const cacheNames = await caches . keys ( ) ;
254+ const status = { } ;
255+
256+ for ( const name of cacheNames ) {
257+ const cache = await caches . open ( name ) ;
258+ const requests = await cache . keys ( ) ;
259+
260+ status [ name ] = {
261+ count : requests . length ,
262+ size : 0 , // Size calculation would require iteration
263+ urls : requests . map ( r => r . url )
264+ } ;
265+ }
266+
267+ return {
268+ version : CACHE_VERSION ,
269+ caches : status ,
270+ totalCaches : cacheNames . length
271+ } ;
272+ }
273+
274+ /**
275+ * Clear all caches
276+ */
277+ async function clearAllCaches ( ) {
278+ const cacheNames = await caches . keys ( ) ;
279+
280+ await Promise . all (
281+ cacheNames . map ( name => caches . delete ( name ) )
282+ ) ;
283+
284+ console . log ( '[Service Worker] All caches cleared' ) ;
285+ }
286+
287+ // Periodic cache cleanup (every hour)
288+ setInterval ( ( ) => {
289+ cleanupOldCaches ( ) ;
290+ } , 60 * 60 * 1000 ) ;
291+
292+ /**
293+ * Clean up old cache entries
294+ */
295+ async function cleanupOldCaches ( ) {
296+ const maxAge = 7 * 24 * 60 * 60 * 1000 ; // 7 days
297+ const now = Date . now ( ) ;
298+
299+ const cacheNames = await caches . keys ( ) ;
300+
301+ for ( const cacheName of cacheNames ) {
302+ if ( cacheName === CORE_CACHE ) {
303+ continue ; // Don't clean core cache
304+ }
305+
306+ const cache = await caches . open ( cacheName ) ;
307+ const requests = await cache . keys ( ) ;
308+
309+ for ( const request of requests ) {
310+ const response = await cache . match ( request ) ;
311+ if ( response ) {
312+ const dateHeader = response . headers . get ( 'date' ) ;
313+ if ( dateHeader ) {
314+ const responseTime = new Date ( dateHeader ) . getTime ( ) ;
315+ if ( now - responseTime > maxAge ) {
316+ await cache . delete ( request ) ;
317+ console . log ( '[Service Worker] Deleted old cache entry:' , request . url ) ;
318+ }
319+ }
320+ }
321+ }
322+ }
323+ }
324+
325+ console . log ( '[Service Worker] Loaded successfully' ) ;
0 commit comments