PageSourceSearch

https://tatyanovskiy.ru/service-worker.js

js tatyanovskiy.ru collected 2026-10-04 08:28:42 UTC 7,338 bytes, 197 lines download raw bytes

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 response that proves this domain
13//      is reachable (see isReachable).
14//   4. Backup wins if the probe succeeds before the original proves reachable.
15//   5. If both fail, fall back to a synthetic 503 (or the non-ok original
16//      response, which is more informative).
17//
18// A navigation that already carries ?od= is the product of a hop and is never
19// hopped again. Without that, any condition that is non-ok on both siblings
20// (a persistent upstream 5xx, a per-domain cookie missing on the sibling)
21// bounces the visitor between the two domains indefinitely.
22//
23// Non-navigation requests (assets, XHR, iframes, POST forms, …) are NOT
24// intercepted — the browser handles them normally.
25
26const BACKUP_DOMAIN     = 'technology-new.ru';
27const BACKUP_TIMEOUT_MS = 5000;
28const PROBE_PATH        = '/test74382647832.txt';
29
30const LOG = '[sw-mirror]';
31const tnow = () => Date.now();
32
33self.addEventListener('install', (event) => {
34  console.log(LOG, 'install; backupDomain=' + (BACKUP_DOMAIN || '(none)'));
35  event.waitUntil(self.skipWaiting());
36});
37
38self.addEventListener('activate', (event) => {
39  console.log(LOG, 'activate');
40  event.waitUntil(self.clients.claim());
41});
42
43// A blocked domain fails at the network layer (DNS poisoning, RST, timeout),
44// so any answer from our own server proves reachability — except 5xx, which
45// is what an origin outage behind Cloudflare looks like. Two non-ok answers
46// still count:
47//   - opaqueredirect: navigations are fetched with redirect:'manual', so every
48//     3xx (e.g. /p/https/bbc.com/ -> www.bbc.com) arrives as status 0, ok=false.
49//   - the proof-of-work interstitial (503 + X-Mirror-Pow), which must be shown
50//     here: the pass cookie is per-domain, so the sibling would gate too.
51function isReachable(response) {
52  if (!response) return false;
53  if (response.ok || response.type === 'opaqueredirect') return true;
54  if (response.headers.get('X-Mirror-Pow') === '1') return true;
55  return response.status > 0 && response.status < 500;
56}
57
58function shouldHandle(request) {
59  if (request.destination === 'iframe') return false;
60  if (request.mode === 'navigate') return true;
61  if (request.method !== 'GET') return false;
62  const accept = request.headers.get('accept') || '';
63  return accept.includes('text/html');
64}
65
66self.addEventListener('fetch', (event) => {
67  if (!shouldHandle(event.request)) return;
68  event.respondWith(handleNavigation(event.request));
69});
70
71let requestSeq = 0;
72
73async function handleNavigation(request) {
74  const id = (++requestSeq).toString(36);
75  const start = tnow();
76  const ms = () => (tnow() - start) + 'ms';
77  const tag = (msg) => `${LOG} [req#${id}] ${msg}`;
78
79  console.log(tag('navigation'), {
80    url: request.url,
81    mode: request.mode,
82    destination: request.destination,
83    method: request.method,
84    backupDomain: BACKUP_DOMAIN || '(none)',
85  });
86
87  // Guard against degenerate backups (no partner found, or partner == self),
88  // and against hopping a request that is itself the result of a hop.
89  const requestUrl = new URL(request.url);
90  const sameAsSelf = BACKUP_DOMAIN &&
91    BACKUP_DOMAIN.toLowerCase() === requestUrl.hostname.toLowerCase();
92  const alreadyHopped = requestUrl.searchParams.has('od');
93  if (!BACKUP_DOMAIN || sameAsSelf || alreadyHopped) {
94    const why = alreadyHopped ? 'already hopped (od=)' : sameAsSelf ? 'equals self' : 'empty';
95    console.warn(tag(`no usable backup (${why}); passthrough`));
96    try {
97      const r = await fetch(request);
98      console.log(tag(`passthrough status=${r.status} (${ms()})`));
99      return r;
100    } catch (e) {
101      console.error(tag(`passthrough failed: ${(e && e.message) || e} (${ms()})`));
102      return new Response('Service Unavailable', { status: 503 });
103    }
104  }
105
106  let originalReachable = false;
107
108  // Both promises ALWAYS resolve (never reject) — see .catch() below.
109  const originalPromise = fetch(request)
110    .then((response) => {
111      originalReachable = isReachable(response);
112      console.log(tag(`original resolved status=${response.status} type=${response.type} reachable=${originalReachable} (${ms()})`));
113      return response;
114    })
115    .catch((e) => {
116      console.warn(tag(`original rejected: ${(e && e.message) || e} (${ms()})`));
117      return null;
118    });
119
120  const backupPromise = tryBackup(request, tag, ms, () => originalReachable);
121
122  // Decision logic:
123  //   - As soon as original resolves reachable: use it.
124  //   - As soon as backup produces a redirect: use it.
125  //   - Otherwise wait for both, then pick best available (backup > non-ok original > 503).
126  return await new Promise((resolve) => {
127    let done = false;
128    const finish = (response, why) => {
129      if (done) return;
130      done = true;
131      console.log(tag(`final: ${why} (${ms()})`));
132      resolve(response);
133    };
134
135    originalPromise.then((r) => {
136      if (isReachable(r)) finish(r, `original reachable status=${r.status} type=${r.type}`);
137    });
138
139    backupPromise.then((r) => {
140      if (r) finish(r, 'backup redirect');
141    });
142
143    Promise.all([originalPromise, backupPromise]).then(([orig, bkp]) => {
144      if (done) return;
145      if (bkp) {
146        finish(bkp, 'backup (late)');
147      } else if (orig) {
148        finish(orig, `original non-ok status=${orig.status}`);
149      } else {
150        finish(
151          new Response('Service Unavailable (no mirror available)', {
152            status: 503,
153            headers: { 'Content-Type': 'text/plain' },
154          }),
155          'both failed'
156        );
157      }
158    });
159  });
160}
161
162async function tryBackup(request, tag, ms, isOriginalReachable) {
163  console.log(tag(`backup timer armed: ${BACKUP_TIMEOUT_MS}ms`));
164  await new Promise((r) => setTimeout(r, BACKUP_TIMEOUT_MS));
165
166  if (isOriginalReachable()) {
167    console.log(tag(`backup canceled: original already reachable (${ms()})`));
168    return null;
169  }
170
171  const probeUrl = `https://${BACKUP_DOMAIN}${PROBE_PATH}`;
172  console.log(tag(`probing backup: ${probeUrl} (${ms()})`));
173
174  let probe;
175  try {
176    probe = await fetch(probeUrl, { cache: 'no-store' });
177  } catch (e) {
178    console.error(tag(`backup probe rejected: ${(e && e.message) || e} (${ms()})`));
179    return null;
180  }
181  console.log(tag(`backup probe status=${probe.status} ok=${probe.ok} type=${probe.type} (${ms()})`));
182  if (!probe.ok) return null;
183
184  if (isOriginalReachable()) {
185    console.log(tag(`original reachable during probe; not redirecting (${ms()})`));
186    return null;
187  }
188
189  const target = new URL(request.url);
190  const originalHost = target.host;
191  target.hostname = BACKUP_DOMAIN;
192  target.port = '';
193  target.searchParams.set('od', originalHost);
194
195  console.log(tag(`redirect -> ${target.toString()} (${ms()})`));
196  return Response.redirect(target.toString(), 302);
197}

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.