PageSourceSearch

https://www.destination.com/sw.js

js destination.com collected 2026-10-03 21:39:19 UTC 5,146 bytes, 141 lines download raw bytes

1/**
2 * destination.com — minimal service worker for in-trip offline use.
3 *
4 * Scope (deliberately narrow)
5 * ---------------------------
6 * - Caches PUBLIC share URLs: /trips/<id>/share/<token>
7 *   These are explicitly designed for any-user access — no Clerk session
8 *   required — so caching them in a shared device-level SW doesn't leak
9 *   user data. The HMAC token is itself the credential.
10 *
11 * - Does NOT cache /trips/<id> (the owner view). That route is auth-gated
12 *   and shows user-specific content; caching it in a device-level SW
13 *   could leak across browser profiles. A future PR can scope per-user
14 *   caches with the userId in the cache key.
15 *
16 * - Pre-caches a minimal app shell on install (/, /plan, /trips) so the
17 *   nav surfaces don't blank out on flaky in-trip connections.
18 *
19 * Strategy
20 * --------
21 * - Share URLs: stale-while-revalidate. Show cached immediately, fetch
22 *   the fresh version in background and update the cache.
23 * - Everything else: pass through (no SW involvement) so the existing
24 *   network/proxy/etc. behavior stays unchanged.
25 *
26 * Versioning
27 * ----------
28 * Bump CACHE_NAME (v1 → v2 → ...) to invalidate all caches at once on a
29 * release that needs it. Old caches are pruned on activate.
30 */
31
32const CACHE_NAME = 'destination-v1';
33const SHELL_URLS = ['/', '/plan', '/trips'];
34
35self.addEventListener('install', (event) => {
36  // Pre-cache the shell. Failures (e.g. 404 on /trips before sign-in)
37  // shouldn't block install — best-effort.
38  event.waitUntil(
39    caches
40      .open(CACHE_NAME)
41      .then((cache) => Promise.allSettled(SHELL_URLS.map((u) => cache.add(u))))
42      .then(() => self.skipWaiting()),
43  );
44});
45
46self.addEventListener('activate', (event) => {
47  // Prune old caches so a bumped CACHE_NAME actually frees disk.
48  event.waitUntil(
49    caches
50      .keys()
51      .then((keys) =>
52        Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))),
53      )
54      .then(() => self.clients.claim()),
55  );
56});
57
58/**
59 * Match anything under /trips/<id>/share/<token>. We're permissive on the
60 * id/token format (just non-empty segments) because the server-side
61 * verifyShareToken() does the real check; SW just decides whether to
62 * cache the response.
63 */
64function isShareUrl(url) {
65  return /^\/trips\/[^/]+\/share\/[^/]+\/?$/.test(url.pathname);
66}
67
68self.addEventListener('fetch', (event) => {
69  // Only navigations + same-origin GET. POST / API calls / cross-origin
70  // (Mapbox tiles, Clerk, etc.) flow through untouched.
71  const req = event.request;
72  if (req.method !== 'GET') return;
73  const url = new URL(req.url);
74  if (url.origin !== self.location.origin) return;
75
76  if (isShareUrl(url)) {
77    // Stale-while-revalidate for public share URLs.
78    event.respondWith(
79      caches.open(CACHE_NAME).then((cache) =>
80        cache.match(req).then((cached) => {
81          const network = fetch(req)
82            .then((resp) => {
83              // Only cache successful, non-opaque responses. 503 / 4xx
84              // stay in the network branch and never overwrite a good cache.
85              if (resp && resp.status === 200) {
86                cache.put(req, resp.clone()).catch(() => {});
87              }
88              return resp;
89            })
90            .catch(() => null);
91          // Prefer cached if present (instant load); otherwise wait for net.
92          return cached || network.then((r) => r || new Response('Offline', { status: 503 }));
93        }),
94      ),
95    );
96  }
97});
98
99/* ── Web Push ─────────────────────────────────────────────────────────
100 * Round 8: handle push events fired by /api/push/send via our /api/cron
101 * jobs. Payload shape ({ title, body, url, icon, tag, data }) matches
102 * the PushPayload interface in src/lib/web-push.ts.
103 *
104 * Click handler opens the URL in the existing tab if it's already open,
105 * otherwise opens a fresh window.
106 */
107self.addEventListener('push', (event) => {
108  if (!event.data) return;
109  let payload;
110  try {
111    payload = event.data.json();
112  } catch (err) {
113    payload = { title: 'destination.com', body: String(event.data.text() || '') };
114  }
115  const title = payload.title || 'destination.com';
116  const opts = {
117    body: payload.body || '',
118    icon: payload.icon || '/icon.svg',
119    badge: '/icon.svg',
120    tag: payload.tag,
121    data: { url: payload.url || '/', ...(payload.data || {}) },
122  };
123  event.waitUntil(self.registration.showNotification(title, opts));
124});
125
126self.addEventListener('notificationclick', (event) => {
127  event.notification.close();
128  const target = (event.notification.data && event.notification.data.url) || '/';
129  event.waitUntil(
130    self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((wins) => {
131      for (const w of wins) {
132        if ('focus' in w && new URL(w.url).origin === self.location.origin) {
133          w.focus();
134          if ('navigate' in w) w.navigate(target);
135          return;
136        }
137      }
138      if (self.clients.openWindow) return self.clients.openWindow(target);
139    }),
140  );
141});

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.