PageSourceSearch

https://guestify.ai/sw.js

js guestify.ai collected 2026-10-04 23:36:41 UTC 5,109 bytes, 126 lines download raw bytes

1// Bump on any change to caching behavior so installed PWAs purge prior caches
2// (including poisoned redirect entries) on activate. v3 cached the legacy
3// `/dashboard` shell — a route that now only 307-redirects to
4// `/app/{org}/{client}` — which broke post-login navigation on installed
5// (mobile) clients; v4 stops caching redirects entirely; v5 stops caching
6// partial/range responses (which made `cache.put` throw "Unexpected internal
7// error") and purges any such poisoned entries written by v4.
8const CACHE_VERSION = 'v5';
9const STATIC_CACHE = `guestify-static-${CACHE_VERSION}`;
10const DYNAMIC_CACHE = `guestify-dynamic-${CACHE_VERSION}`;
11
12// Install: activate immediately. There is no app shell to pre-cache — the
13// authed app lives under `/app/{org}/{client}` and is session- and
14// redirect-gated, so it must never be baked into a static cache.
15self.addEventListener('install', () => {
16  self.skipWaiting();
17});
18
19// Activate: clean up old cache versions
20self.addEventListener('activate', (event) => {
21  event.waitUntil(
22    caches.keys().then((cacheNames) => {
23      return Promise.all(
24        cacheNames
25          .filter((name) => name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
26          .map((name) => caches.delete(name))
27      );
28    })
29  );
30  self.clients.claim();
31});
32
33// Determine if a request is an API route
34function isApiRoute(url) {
35  return url.pathname.startsWith('/api/');
36}
37
38// Determine if a request is a navigation request
39function isNavigationRequest(request) {
40  return request.mode === 'navigate';
41}
42
43// Network-first strategy for dynamic content
44async function networkFirst(request) {
45  try {
46    const response = await fetch(request);
47    // Only cache clean, final 200 GET responses. A redirected response
48    // (`response.redirected === true`, e.g. the 307 from `/dashboard` or the
49    // `/app` entry hops) must never be cached: replaying it for a navigation
50    // request is rejected by the browser / serves a stale shell, which lands
51    // the user on the wrong page after login. Skipping these lets the redirect
52    // chain resolve to the real `/app/{org}/{client}` page on every load.
53    //
54    // Partial/range responses are also excluded. The media-kit builder loads
55    // media (audio/video/large images) with `Range` headers; `cache.put`
56    // rejects with "Failed to execute 'put' on 'Cache': Unexpected internal
57    // error" for these, and because the write below is fire-and-forget the
58    // rejection used to surface as a noisy "Uncaught (in promise)" in the
59    // console. We skip range requests / non-`basic` (opaque, partial) responses
60    // up front and additionally swallow any residual `cache.put` rejection
61    // (e.g. QuotaExceeded under storage pressure) so a cache-write failure can
62    // never break the page or spam the console.
63    if (
64      request.method === 'GET' &&
65      response.status === 200 &&
66      response.type === 'basic' &&
67      !response.redirected &&
68      !request.headers.has('range')
69    ) {
70      const cache = await caches.open(DYNAMIC_CACHE);
71      cache.put(request, response.clone()).catch(() => {});
72    }
73    return response;
74  } catch {
75    const cached = await caches.match(request);
76    if (cached) return cached;
77
78    return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
79  }
80}
81
82// Fetch handler
83self.addEventListener('fetch', (event) => {
84  const url = new URL(event.request.url);
85
86  // Only handle same-origin requests
87  if (url.origin !== self.location.origin) return;
88
89  // Never intercept service worker assets. The browser must always fetch these
90  // from the network so updates are not blocked by stale dynamic cache entries.
91  if (url.pathname === '/sw.js' || url.pathname === '/sw-push.js') return;
92
93  // Never intercept auth routes — they rely on Set-Cookie headers and CSRF tokens
94  if (url.pathname.startsWith('/api/auth/')) return;
95
96  // Never intercept session/redirect-gated entry points. `/auth/*` (login),
97  // `/dashboard*` (legacy shim that 307s to the tenant path), and the bare
98  // `/app` resolver all return redirects that depend on the live session and
99  // must reach the network so the post-login chain resolves to the real
100  // `/app/{org}/{client}` page. Concrete tenant pages under `/app/{org}/...`
101  // are not matched here and still go through network-first below.
102  if (
103    url.pathname.startsWith('/auth/') ||
104    url.pathname === '/auth' ||
105    url.pathname.startsWith('/dashboard/') ||
106    url.pathname === '/dashboard' ||
107    url.pathname === '/app' ||
108    url.pathname === '/app/'
109  ) {
110    return;
111  }
112
113  // Never intercept Next.js static assets — they are content-hashed and
114  // already have optimal HTTP cache headers. Caching them in the SW causes
115  // stale bundles to be served after deployments.
116  if (url.pathname.startsWith('/_next/')) return;
117
118  // API routes and HTML pages: network-first
119  if (isApiRoute(url) || isNavigationRequest(event.request)) {
120    event.respondWith(networkFirst(event.request));
121    return;
122  }
123
124  // Default: network-first
125  event.respondWith(networkFirst(event.request));
126});

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.