PageSourceSearch

https://web.endnote.com/pwa-service-worker.js

js endnote.com collected 2026-09-24 08:35:40 UTC 8,744 bytes, 297 lines download raw bytes

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.