1const CACHE_PREFIX = "mtranslate-pwa"; 2const STATIC_CACHE = `${CACHE_PREFIX}-static-v1`; 3const PAGE_CACHE = `${CACHE_PREFIX}-pages-v1`; 4const RUNTIME_CACHE = `${CACHE_PREFIX}-runtime-v1`; 5const VERSION_CACHE = `${CACHE_PREFIX}-version`; 6const VERSION_KEY = "/__mtranslate_pwa_version__"; 7 8const APP_SHELL_URLS = [ 9 "/pwa_app", 10 "/manifest.json", 11 "/images/pwa_icons/icon_96.png", 12 "/images/pwa_icons/icon_128.png", 13 "/images/pwa_icons/icon_144.png", 14 "/images/pwa_icons/icon_152.png", 15 "/images/pwa_icons/icon_192.png", 16 "/images/pwa_icons/icon_256.png", 17 "/images/pwa_icons/icon_512.png", 18]; 19 20const BYPASS_PREFIXES = [ 21 "/api/2/", 22 "/api/trpc", 23 "/admin", 24 "/cpanel", 25 "/login", 26 "/logout", 27 "/review", 28 "/translate", 29 "/word-detail", 30 "/word-alternative", 31 "/alternative", 32 "/204check", 33 "/geo.js", 34 "/index.js", 35]; 36 37function isSameOrigin(url) { 38 return url.origin === self.location.origin; 39} 40 41function shouldBypass(request) { 42 const url = new URL(request.url); 43 44 if (request.method !== "GET" || !isSameOrigin(url)) { 45 return true; 46 } 47 48 return BYPASS_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)); 49} 50 51function isStaticAsset(url) { 52 return ( 53 url.pathname.startsWith("/_next/static/") || 54 url.pathname.startsWith("/images/pwa_icons/") || 55 /\.(?:css|js|mjs|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$/i.test(url.pathname) 56 ); 57} 58 59async function readStoredVersion() { 60 const cache = await caches.open(VERSION_CACHE); 61 const response = await cache.match(VERSION_KEY); 62 63 if (!response) { 64 return null; 65 } 66 67 try { 68 const payload = await response.json(); 69 return typeof payload.version === "string" ? payload.version : null; 70 } catch { 71 return null; 72 } 73} 74 75async function writeStoredVersion(version) { 76 const cache = await caches.open(VERSION_CACHE); 77 await cache.put( 78 VERSION_KEY, 79 new Response(JSON.stringify({ version }), { 80 headers: { "content-type": "application/json" }, 81 }), 82 ); 83} 84 85async function fetchServerVersion() { 86 const response = await fetch(`/api/pwa-version?t=${Date.now()}`, { 87 cache: "no-store", 88 credentials: "same-origin", 89 }); 90 91 if (!response.ok) { 92 return null; 93 } 94 95 const payload = await response.json(); 96 return typeof payload.version === "string" ? payload.version : null; 97} 98 99async function clearAppCaches() { 100 const keys = await caches.keys(); 101 await Promise.all( 102 keys 103 .filter((key) => key.startsWith(CACHE_PREFIX) && key !== VERSION_CACHE) 104 .map((key) => caches.delete(key)), 105 ); 106} 107 108async function notifyClients(message) { 109 const clients = await self.clients.matchAll({ includeUncontrolled: true }); 110 clients.forEach((client) => client.postMessage(message)); 111} 112 113async function syncServerVersion({ notify = false } = {}) { 114 const latestVersion = await fetchServerVersion(); 115 116 if (!latestVersion) { 117 return false; 118 } 119 120 const storedVersion = await readStoredVersion(); 121 122 if (!storedVersion) { 123 await writeStoredVersion(latestVersion); 124 return false; 125 } 126 127 if (storedVersion === latestVersion) { 128 return false; 129 } 130 131 await clearAppCaches(); 132 await writeStoredVersion(latestVersion); 133 134 if (notify) { 135 await notifyClients({ type: "PWA_VERSION_CHANGED", version: latestVersion }); 136 } 137 138 return true; 139} 140 141async function cacheAppShell() { 142 const cache = await caches.open(STATIC_CACHE); 143 144 await Promise.all( 145 APP_SHELL_URLS.map(async (url) => { 146 try { 147 await cache.add(new Request(url, { cache: "reload" })); 148 } catch { 149 // A failed optional asset must not abort service worker installation. 150 } 151 }), 152 ); 153} 154 155async function networkFirstNavigation(request) { 156 const cache = await caches.open(PAGE_CACHE); 157 158 try { 159 const response = await fetch(request); 160 161 if (response.ok && isSameOrigin(new URL(response.url))) { 162 const url = new URL(request.url); 163 if (APP_SHELL_URLS.includes(url.pathname)) { 164 await cache.put(request, response.clone()); 165 } 166 } 167 168 return response; 169 } catch { 170 const cachedResponse = await caches.match(request); 171 const shellResponse = await caches.match("/pwa_app"); 172 173 return ( 174 cachedResponse || 175 shellResponse || 176 new Response("M-Translate is offline. Reconnect and try again.", { 177 headers: { "content-type": "text/plain;charset=utf-8" }, 178 status: 503, 179 }) 180 ); 181 } 182} 183 184async function staleWhileRevalidate(request) { 185 const cache = await caches.open(RUNTIME_CACHE); 186 const cachedResponse = await cache.match(request); 187 const fetchPromise = fetch(request) 188 .then((response) => { 189 if (response.ok && isSameOrigin(new URL(response.url))) { 190 cache.put(request, response.clone()); 191 } 192 return response; 193 }) 194 .catch(() => cachedResponse); 195 196 return cachedResponse || fetchPromise; 197} 198 199self.addEventListener("install", (event) => {
200 event.waitUntil(cacheAppShell()); 201}); 202 203self.addEventListener("activate", (event) => { 204 event.waitUntil( 205 Promise.all([ 206 self.clients.claim(), 207 syncServerVersion({ notify: true }), 208 ]), 209 ); 210}); 211 212self.addEventListener("message", (event) => { 213 if (event.data?.type === "SKIP_WAITING") { 214 self.skipWaiting(); 215 return; 216 } 217 218 if (event.data?.type === "CHECK_VERSION") { 219 event.waitUntil(syncServerVersion({ notify: true })); 220 return; 221 } 222 223 if (event.data?.type === "CLEAR_CACHES") { 224 event.waitUntil(clearAppCaches()); 225 } 226}); 227 228self.addEventListener("fetch", (event) => { 229 const { request } = event; 230 231 if (shouldBypass(request)) { 232 return; 233 } 234 235 const url = new URL(request.url); 236 237 if (request.mode === "navigate") { 238 event.waitUntil(syncServerVersion({ notify: true })); 239 event.respondWith(networkFirstNavigation(request)); 240 return; 241 } 242 243 if (isStaticAsset(url)) { 244 event.respondWith(staleWhileRevalidate(request)); 245 } 246});
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.