1// Service worker for smart-mirror failover. 2// 3// Generated per-request by node/server.js (route GET /service-worker). 4// The backup domain is picked from the same auto-increment group of 10 5// in the `offlinedomains` collection and injected via EJS below. 6// 7// Behavior on top-level HTML navigations: 8// 1. Fire the original request immediately. 9// 2. In parallel, after BACKUP_TIMEOUT_MS, probe the backup mirror's 10// sentinel file. If it returns 2xx, build a 302 redirect to the same 11// path on the backup with ?od=<original host> appended. 12// 3. Original wins as soon as it returns a 2xx response. 13// 4. Backup wins if the probe succeeds before the original returns ok. 14// 5. If both fail, fall back to a synthetic 503 (or the non-ok original 15// response, which is more informative). 16// 17// Non-navigation requests (assets, XHR, iframes, POST forms, â¦) are NOT 18// intercepted â the browser handles them normally. 19 20const BACKUP_DOMAIN = 'dental-prof-implantation.ru'; 21const BACKUP_TIMEOUT_MS = 5000; 22const PROBE_PATH = '/test74382647832.txt'; 23 24const LOG = '[sw-mirror]'; 25const tnow = () => Date.now(); 26 27self.addEventListener('install', (event) => { 28 console.log(LOG, 'install; backupDomain=' + (BACKUP_DOMAIN || '(none)')); 29 event.waitUntil(self.skipWaiting()); 30}); 31 32self.addEventListener('activate', (event) => { 33 console.log(LOG, 'activate'); 34 event.waitUntil(self.clients.claim()); 35}); 36 37function shouldHandle(request) { 38 if (request.destination === 'iframe') return false; 39 if (request.mode === 'navigate') return true; 40 if (request.method !== 'GET') return false; 41 const accept = request.headers.get('accept') || ''; 42 return accept.includes('text/html'); 43} 44 45self.addEventListener('fetch', (event) => { 46 if (!shouldHandle(event.request)) return; 47 event.respondWith(handleNavigation(event.request)); 48}); 49 50let requestSeq = 0; 51 52async function handleNavigation(request) { 53 const id = (++requestSeq).toString(36); 54 const start = tnow(); 55 const ms = () => (tnow() - start) + 'ms'; 56 const tag = (msg) => `${LOG} [req#${id}] ${msg}`; 57 58 console.log(tag('navigation'), { 59 url: request.url, 60 mode: request.mode, 61 destination: request.destination, 62 method: request.method, 63 backupDomain: BACKUP_DOMAIN || '(none)', 64 }); 65 66 // Guard against degenerate backups (no partner found, or partner == self). 67 const sameAsSelf = BACKUP_DOMAIN && 68 BACKUP_DOMAIN.toLowerCase() === new URL(request.url).hostname.toLowerCase(); 69 if (!BACKUP_DOMAIN || sameAsSelf) { 70 console.warn(tag(`no usable backup (${sameAsSelf ? 'equals self' : 'empty'}); passthrough`)); 71 try { 72 const r = await fetch(request); 73 console.log(tag(`passthrough status=${r.status} (${ms()})`)); 74 return r; 75 } catch (e) { 76 console.error(tag(`passthrough failed: ${(e && e.message) || e} (${ms()})`)); 77 return new Response('Service Unavailable', { status: 503 }); 78 } 79 } 80 81 let originalOk = false; 82 83 // Both promises ALWAYS resolve (never reject) â see .catch() below. 84 const originalPromise = fetch(request) 85 .then((response) => { 86 originalOk = response.ok; 87 console.log(tag(`original resolved status=${response.status} ok=${response.ok} (${ms()})`)); 88 return response; 89 }) 90 .catch((e) => { 91 console.warn(tag(`original rejected: ${(e && e.message) || e} (${ms()})`)); 92 return null; 93 }); 94 95 const backupPromise = tryBackup(request, tag, ms, () => originalOk); 96 97 // Decision logic: 98 // - As soon as original resolves with ok=true: use it. 99 // - As soon as backup produces a redirect: use it. 100 // - Otherwise wait for both, then pick best available (backup > non-ok original > 503). 101 return await new Promise((resolve) => { 102 let done = false; 103 const finish = (response, why) => { 104 if (done) return; 105 done = true; 106 console.log(tag(`final: ${why} (${ms()})`)); 107 resolve(response); 108 }; 109 110 originalPromise.then((r) => { 111 if (r && r.ok) finish(r, `original ok status=${r.status}`); 112 }); 113 114 backupPromise.then((r) => { 115 if (r) finish(r, 'backup redirect'); 116 }); 117 118 Promise.all([originalPromise, backupPromise]).then(([orig, bkp]) => { 119 if (done) return; 120 if (bkp) { 121 finish(bkp, 'backup (late)'); 122 } else if (orig) { 123 finish(orig, `original non-ok status=${orig.status}`); 124 } else { 125 finish( 126 new Response('Service Unavailable (no mirror available)', { 127 status: 503, 128 headers: { 'Content-Type': 'text/plain' }, 129 }), 130 'both failed' 131 ); 132 } 133 }); 134 }); 135} 136 137async function tryBackup(request, tag, ms, isOriginalOk) { 138 console.log(tag(`backup timer armed: ${BACKUP_TIMEOUT_MS}ms`)); 139 await new Promise((r) => setTimeout(r, BACKUP_TIMEOUT_MS)); 140 141 if (isOriginalOk()) { 142 console.log(tag(`backup canceled: original already ok (${ms()})`)); 143 return null; 144 } 145 146 const probeUrl = `https://${BACKUP_DOMAIN}${PROBE_PATH}`; 147 console.log(tag(`probing backup: ${probeUrl} (${ms()})`)); 148 149 let probe; 150 try { 151 probe = await fetch(probeUrl, { cache: 'no-store' }); 152 } catch (e) { 153 console.error(tag(`backup probe rejected: ${(e && e.message) || e} (${ms()})`)); 154 return null; 155 } 156 console.log(tag(`backup probe status=${probe.status} ok=${probe.ok} type=${probe.type} (${ms()})`)); 157 if (!probe.ok) return null; 158 159 if (isOriginalOk()) { 160 console.log(tag(`original succeeded during probe; not redirecting (${ms()})`)); 161 return null; 162 } 163 164 const target = new URL(request.url); 165 const originalHost = target.host; 166 target.hostname = BACKUP_DOMAIN; 167 target.port = ''; 168 target.searchParams.set('od', originalHost); 169 170 console.log(tag(`redirect -> ${target.toString()} (${ms()})`)); 171 return Response.redirect(target.toString(), 302); 172}
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.