PageSourceSearch

https://socialshot.ai/sw.js

js socialshot.ai collected 2026-10-05 12:39:50 UTC 8,160 bytes, 223 lines download raw bytes

1/*
2 * SocialShot AI service worker.
3 *
4 * Scope is deliberately narrow: this exists to make the site installable and
5 * to make repeat loads fast on slow mobile networks (India is the target
6 * market). It is NOT a general offline-first cache, and it is written to be
7 * boring on purpose — a service worker is the one script on the domain that
8 * can keep serving stale bytes after a bad deploy, so every caching decision
9 * below is opt-in rather than opt-out.
10 *
11 * ── What is NEVER touched (the request falls straight through to network) ──
12 *
13 *   1. Anything cross-origin. That covers api.socialshot.ai (live credit
14 *      balances, generation state), assets.socialshot.ai + *.r2.dev (signed
15 *      media URLs that EXPIRE — a cached one renders a permanently broken
16 *      image with no obvious cause), Clerk, Razorpay and PostHog. The origin
17 *      check at the top of the fetch handler is what enforces this, so no
18 *      third party can be cached by accident when one is added later.
19 *
20 *   2. Same-origin /api/* and the /ingest/* PostHog proxy.
21 *
22 *   3. RSC payload requests (`?_rsc=`, `RSC: 1`, `Accept: text/x-component`).
23 *      These are App Router navigation payloads keyed to a specific build and
24 *      to the signed-in user. Serving a stale one produces hydration errors
25 *      that are near-impossible to reproduce locally.
26 *
27 *   4. /_next/image. The optimizer proxies user-generated content, including
28 *      media behind signed R2 URLs, so its responses are both private and
29 *      short-lived. Same reasoning as (1).
30 *
31 *   5. Any URL carrying a credential-shaped query param (token / signature /
32 *      X-Amz-*). Belt and braces on top of (1) and (4).
33 *
34 *   6. HTML documents. Pages here are auth-dependent and are served
35 *      `Cache-Control: private, no-store`. Caching them risks handing a
36 *      signed-in shell to the next person on a shared device, which is a real
37 *      scenario for the reseller/kirana users this app targets. Navigations
38 *      are network-only with an offline fallback, nothing more.
39 *
40 * ── What IS cached ──
41 *
42 *   • /_next/static/*  — cache-first. Content-hashed and immutable by
43 *     contract, so this is safe and is where essentially all of the repeat-
44 *     visit speedup comes from.
45 *   • Static files we ship in /public (icons, fonts, marketing imagery) —
46 *     stale-while-revalidate.
47 *   • /offline.html — precached, shown only when a navigation fails.
48 */
49
50const VERSION = "v1";
51const STATIC_CACHE = `socialshot-static-${VERSION}`;
52const PRECACHE = `socialshot-precache-${VERSION}`;
53const EXPECTED_CACHES = new Set([STATIC_CACHE, PRECACHE]);
54
55const OFFLINE_URL = "/offline.html";
56const PRECACHE_URLS = [OFFLINE_URL, "/android-chrome-192x192.png"];
57
58// Cache-first entries are hashed and never invalidated within a VERSION, so
59// without a cap the bucket grows by roughly one build's worth of chunks per
60// deploy on a returning user's device. Trim on write rather than scheduling
61// anything: Safari in particular evicts whole origins under storage pressure,
62// and losing the precache to a quota eviction would take the offline page
63// with it.
64const STATIC_CACHE_MAX_ENTRIES = 160;
65
66const CACHEABLE_ASSET_RE =
67  /\.(?:png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|mp4|webm)$/i;
68
69const CREDENTIAL_PARAM_RE = /^(?:token|signature|sig|key|x-amz-)/i;
70
71function isNeverCacheable(url, request) {
72  // Same-origin API + analytics proxy.
73  if (url.pathname.startsWith("/api/")) return true;
74  if (url.pathname.startsWith("/ingest/")) return true;
75
76  // App Router RSC payloads.
77  if (url.searchParams.has("_rsc")) return true;
78  if (request.headers.get("RSC") === "1") return true;
79  const accept = request.headers.get("Accept") || "";
80  if (accept.includes("text/x-component")) return true;
81
82  // Next's image optimizer proxies private / signed user media.
83  if (url.pathname.startsWith("/_next/image")) return true;
84
85  // Anything that looks like it carries a credential.
86  for (const name of url.searchParams.keys()) {
87    if (CREDENTIAL_PARAM_RE.test(name)) return true;
88  }
89
90  return false;
91}
92
93async function trimCache(cacheName, maxEntries) {
94  const cache = await caches.open(cacheName);
95  const keys = await cache.keys();
96  if (keys.length <= maxEntries) return;
97  // Cache Storage preserves insertion order, so the head of the list is the
98  // oldest. Evict just enough to get back under the cap.
99  await Promise.all(
100    keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key)),
101  );
102}
103
104async function cacheFirst(request) {
105  const cache = await caches.open(STATIC_CACHE);
106  const hit = await cache.match(request);
107  if (hit) return hit;
108
109  const response = await fetch(request);
110  // Only store real, complete responses. `response.ok` excludes 4xx/5xx, and
111  // the type check excludes opaque cross-origin responses (which should be
112  // unreachable here given the origin guard, but a cached opaque response is
113  // both unreadable and counted against quota at its full padded size).
114  if (response.ok && response.type === "basic") {
115    await cache.put(request, response.clone());
116    trimCache(STATIC_CACHE, STATIC_CACHE_MAX_ENTRIES);
117  }
118  return response;
119}
120
121async function staleWhileRevalidate(request) {
122  const cache = await caches.open(STATIC_CACHE);
123  const hit = await cache.match(request);
124
125  const network = fetch(request)
126    .then((response) => {
127      if (response.ok && response.type === "basic") {
128        cache.put(request, response.clone());
129        trimCache(STATIC_CACHE, STATIC_CACHE_MAX_ENTRIES);
130      }
131      return response;
132    })
133    .catch(() => undefined);
134
135  if (hit) return hit;
136
137  const response = await network;
138  if (response) return response;
139  return Response.error();
140}
141
142async function handleNavigation(request) {
143  try {
144    return await fetch(request);
145  } catch {
146    // Offline (or the origin is unreachable). Never a cached page — see the
147    // header comment on why HTML is not cached at all.
148    const cache = await caches.open(PRECACHE);
149    const offline = await cache.match(OFFLINE_URL);
150    if (offline) return offline;
151    return new Response("You are offline.", {
152      status: 503,
153      headers: { "Content-Type": "text/plain; charset=utf-8" },
154    });
155  }
156}
157
158self.addEventListener("install", (event) => {
159  event.waitUntil(
160    caches.open(PRECACHE).then((cache) => cache.addAll(PRECACHE_URLS)),
161  );
162  // Deliberately NOT calling skipWaiting().
163  //
164  // A new worker activating under an already-open tab would start serving the
165  // new build's hashed chunks to a document rendered against the old build,
166  // which surfaces as ChunkLoadError. Letting the update wait for the last tab
167  // to close is the boring, correct default. The SKIP_WAITING message below is
168  // the escape hatch if an explicit "refresh to update" prompt is ever added.
169});
170
171self.addEventListener("activate", (event) => {
172  event.waitUntil(
173    (async () => {
174      const names = await caches.keys();
175      await Promise.all(
176        names
177          .filter((name) => name.startsWith("socialshot-") && !EXPECTED_CACHES.has(name))
178          .map((name) => caches.delete(name)),
179      );
180      await self.clients.claim();
181    })(),
182  );
183});
184
185self.addEventListener("message", (event) => {
186  if (event.data === "SKIP_WAITING") self.skipWaiting();
187});
188
189self.addEventListener("fetch", (event) => {
190  const request = event.request;
191
192  if (request.method !== "GET") return;
193
194  let url;
195  try {
196    url = new URL(request.url);
197  } catch {
198    return;
199  }
200
201  // The single most important line in this file: everything cross-origin is
202  // the network's problem, not ours.
203  if (url.origin !== self.location.origin) return;
204
205  if (isNeverCacheable(url, request)) return;
206
207  if (request.mode === "navigate") {
208    event.respondWith(handleNavigation(request));
209    return;
210  }
211
212  if (url.pathname.startsWith("/_next/static/")) {
213    event.respondWith(cacheFirst(request));
214    return;
215  }
216
217  if (CACHEABLE_ASSET_RE.test(url.pathname)) {
218    event.respondWith(staleWhileRevalidate(request));
219    return;
220  }
221
222  // Anything else (route handlers, /llms.txt, /openapi.json, …) is left alone.
223});

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.