1/* eslint-disable no-restricted-globals */ 2/* eslint-disable consistent-return */ 3/* global CryptoJS idbKeyval, importScripts */ 4 5importScripts('/assets/scripts/cryptoMD5.js'); 6importScripts('/assets/scripts/idb-keyval-indexedDb.js'); 7 8const CACHE_NAME = 'cached_items'; 9// TODO: add and CHANNEL_NAME on client side and use it from there 10// TODO: so if we change it in one place, changes will be applied everywhere in the app 11const CHANNEL_NAME = 'endnote-m-channel'; 12// add to list internal endpoints/assets that we don't want to cache 13const paths = ['/download']; 14let wasAppInOnlineMode = false; 15 16self.addEventListener('install', () => { 17 // Skip over the "waiting" lifecycle state, to ensure that our 18 // new service worker is activated immediately, even if there's 19 // another tab open controlled by our older service worker code. 20 self.skipWaiting(); 21}); 22 23self.addEventListener('activate', (event) => { 24 event.waitUntil(self.clients.claim()); 25}); 26 27/** If request it's not a coming from the external server and it's not in the list of paths that should be ignored, we should cache it */ 28function shouldAssetBeCached(request) { 29 return request.url.match(self.location.origin) && !paths.some((path) => request.url.match(path)); 30} 31 32/** 33 * Returns the cached response for the given request. 34 * @param request 35 * @return Promise<Response | null> 36 */ 37async function getResponseFromIndexedDB(request) { 38 let data; 39 try { 40 const body = await request.json(); 41 const id = CryptoJS.MD5(body.query).toString(); 42 data = await idbKeyval.get(id); 43 if (!data) { 44 return null; 45 } 46 47 // TODO: Check cache max age 48 49 return new Response(JSON.stringify(data.response.body), data.response); 50 } catch (err) { 51 return null; 52 } 53} 54 55/** 56 * Serializes a Response into a plain JS object 57 * @param response 58 * @returns Promise<{ 59 headers: {}; 60 status: any; 61 statusText: any; 62}> 63 */ 64async function serializeResponse(response) { 65 const serializedHeaders = {}; 66 // eslint-disable-next-line no-restricted-syntax 67 for (const entry of response.headers.entries()) { 68 serializedHeaders[entry[0]] = entry[1]; 69 } 70 const serialized = { 71 headers: serializedHeaders, 72 status: response.status, 73 statusText: response.statusText 74 }; 75 serialized.body = await response.json(); 76 return serialized; 77} 78 79/** 80 * Saves some key/value to indexedDB. 81 * @param key 82 * @param value 83 */ 84async function saveToIndexedDB(key, value) { 85 const id = CryptoJS.MD5(key).toString(); 86 const entry = { 87 value, 88 timestamp: Date.now() 89 }; 90 await idbKeyval.set(id, entry); 91} 92 93/** 94 * Returns the cached response for the given request. 95 * @param key 96 * @return Promise<{timestap: string, value:string}> 97 */ 98async function getFromIndexedDB(key) { 99 try { 100 const id = CryptoJS.MD5(key).toString(); 101 const data = idbKeyval.get(id); 102 return data; 103 // TODO: Check cache max age 104 } catch (err) { 105 // console.error('getFromIndexedDB error:', err); 106 return null; 107 } 108} 109 110/** 111 * Saves the response for the given request. 112 * if request is already stored in indexedDB, and response is changed, under the same id we will have updated data 113 * that means it will not create separate object in indexedDB for the same request 114 * @param request 115 * @param response 116 */ 117async function saveResponseToIndexedDB(request, response) { 118 if (!request) { 119 return; 120 } 121 122 const body = await request.json(); 123 124 // Don't store graphql mutations in indexedDB, for now we don't need it. 125 if (body.query.startsWith('mutation')) { 126 return; 127 } 128 129 const id = CryptoJS.MD5(body.query).toString(); 130 const entry = { 131 query: body.query, 132 response: await serializeResponse(response), 133 timestamp: Date.now() 134 }; 135 await idbKeyval.set(id, entry); 136} 137 138/** isOnline status will be send to the client side */ 139async function sendIsOnlineStatus(event, isOnlineMode) { 140 if (!event.clientId) { 141 return; 142 } 143 144 const client = await self.clients.get(event.clientId); 145 if (!client) { 146 return; 147 } 148 149 client.postMessage({ 150 isOnline: isOnlineMode 151 }); 152 wasAppInOnlineMode = isOnlineMode; 153} 154 155/** 156 * Apply a network first caching strategy 157 * @param event 158 */ 159async function networkFirstStrategy(event) { 160 event.respondWith( 161 // First try to fetch the request from the server 162 fetch(event.request.clone()) 163 .then(async (response) => { 164 // If it works, put the response into IndexedDB 165 if (response?.clone().ok) { 166 if (!wasAppInOnlineMode) { 167 await sendIsOnlineStatus(event, true); 168 } 169 await saveResponseToIndexedDB(event.request.clone(), response.clone()); 170 } 171 return response; 172 }) 173 .catch(async () => { 174 if (wasAppInOnlineMode) { 175 await sendIsOnlineStatus(event, false); 176 } 177 // If it does not work, return the cached response. 178 const dataFromIndexedDB = await getResponseFromIndexedDB(event.request.clone()); 179 return dataFromIndexedDB; 180 }) 181 ); 182} 183 184/** 185 * caching asset using network first strategy 186 * @param event 187 */ 188async function cacheAsset(event) { 189 event.respondWith( 190 (async () => { 191 const requestUrl = event.request.url;
192 try { 193 const response = await fetch(requestUrl); 194 if (response.ok) { 195 const cache = await caches.open(CACHE_NAME); 196 // In production most of files will be hashed, that can cause issue where we can have same files with different hash added at the end of file 197 // Remove hash 198 const requestUrlWithoutHashing = requestUrl.split('.').slice(-2)[0]; 199 200 // Delete old entries that are coming from previous deployments 201 cache.keys().then((keys) => { 202 keys.forEach((request) => { 203 if (request.url.includes(requestUrlWithoutHashing)) { 204 cache.delete(request); 205 } 206 }); 207 }); 208 209 // Put in cache if succeeds 210 // response needs to be cloned 211 await cache.put(requestUrl, response.clone()); 212 return response; 213 } 214 } catch (e) { 215 const cacheResponse = await caches.match(requestUrl); 216 if (cacheResponse) { 217 return cacheResponse; 218 } 219 } 220 })() 221 ); 222} 223 224/** 225 * Apply a network first caching strategy for index.html 226 * @param event 227 */ 228async function networkFirstStrategyForHTML(event, htmlPath) { 229 event.respondWith( 230 (async () => { 231 // console.info(`[Service Worker] Fetching resource from server: ${htmlPath}`); 232 const cacheKey = htmlPath; 233 try { 234 const serverResponse = await fetch(cacheKey); 235 if (serverResponse.ok) { 236 const cache = await caches.open(CACHE_NAME); 237 await cache.put(cacheKey, serverResponse.clone()); 238 await saveToIndexedDB('hostname', self.location.origin); 239 return serverResponse; 240 } 241 } catch (e) { 242 // console.info(`[Service Worker] Fetching resource failed for: ${htmlPath}`, e); 243 const cacheResponse = await caches.match(cacheKey); 244 if (cacheResponse) { 245 return cacheResponse; 246 } 247 // console.warn('networkFirstStrategyForHTML no cached response that matches key', cacheKey); 248 } 249 })() 250 ); 251} 252 253/** check if request's url contains extension */ 254function hasExtension(url) { 255 const urlLastPart = url.split('/').pop(); 256 return urlLastPart.indexOf('.') !== -1; 257} 258 259// Listen to fetch requests 260self.addEventListener('fetch', async (event) => { 261 // When page is reloaded or navigate to, respond with base page 262 // Base page in our Single Page Application is index.html 263 if ((event.isReload || event.request.mode === 'navigate') && !hasExtension(event.request.url)) { 264 const milliseconds = new Date().getTime(); 265 // Append milliseconds to the path to force the browser to fetch the resource from the server 266 const htmlPath = `/index.html?v=${milliseconds.toString()}`; 267 await networkFirstStrategyForHTML(event, htmlPath); 268 return; 269 } 270 if (event.request.method === 'GET' && shouldAssetBeCached(event.request)) { 271 await cacheAsset(event); 272 return; 273 } 274 if (event.request.method === 'POST' && event.request.url.match('/graphql')) { 275 // TODO: exclude usn graphql request 276 await networkFirstStrategy(event); 277 } 278}); 279 280const serviceWorkerComs = new BroadcastChannel(CHANNEL_NAME); 281/** 282 * This receives a messages from the client via BroadcastChannel. 283 * 284 * @param {*} event 285 */ 286serviceWorkerComs.onmessage = async (event) => { 287 const hostname = await getFromIndexedDB('hostname'); 288 289 if (!hostname || event.origin !== hostname?.value) { 290 return; 291 } 292 const { action } = event.data; 293 if (action === 'logout') { 294 await idbKeyval.clear(); 295 await caches.delete(CACHE_NAME); 296 } 297};
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.