PageSourceSearch

https://infinitycolor.co/sw.js

js infinitycolor.co collected 2026-10-08 19:35:55 UTC 16,901 bytes, 566 lines download raw bytes

1/**
2 * Storefront service worker.
3 *
4 * Only public shell/static assets may be cached. Authenticated, account,
5 * commerce, and API traffic always stays on the browser/network path.
6 */
7
8importScripts("/sw-version.js");
9
10const BUILD_VERSION = self.__INFINITY_BUILD_VERSION__ || "v1-dev";
11
12// Generate cache names with build version
13function getCacheNames() {
14  return {
15    RUNTIME_CACHE: `runtime-cache-v${BUILD_VERSION}`,
16  };
17}
18
19// URLs to cache on install
20const URLS_TO_CACHE = ["/", "/offline.html", "/manifest.json"];
21
22const SENSITIVE_PATH_PREFIXES = [
23  "/api",
24  "/auth",
25  "/account",
26  "/addresses",
27  "/cart",
28  "/checkout",
29  "/favorites",
30  "/orders",
31  "/password",
32  "/payment",
33  "/privileges",
34  "/super-admin",
35  "/wallet",
36];
37
38function isSensitivePath(pathname) {
39  return SENSITIVE_PATH_PREFIXES.some(
40    (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),
41  );
42}
43
44/**
45 * Install event: cache essential files
46 */
47self.addEventListener("install", (event) => {
48  console.log("[SW] Installing service worker...");
49  const { RUNTIME_CACHE } = getCacheNames();
50
51  event.waitUntil(
52    Promise.all([
53      // Load PWA clients from IndexedDB on install
54      loadPWAClientsFromIDB(),
55      // Cache essential files
56      caches
57        .open(RUNTIME_CACHE)
58        .then((cache) => {
59          return cache.addAll(URLS_TO_CACHE).catch((err) => {
60            console.log("[SW] Some files failed to cache:", err);
61            // Don't fail the install if some files fail
62            return Promise.resolve();
63          });
64        }),
65    ]).then(() => self.skipWaiting()),
66  );
67});
68
69/**
70 * Delete old caches that don't match current version
71 */
72async function deleteOldCaches() {
73  try {
74    const { RUNTIME_CACHE } = getCacheNames();
75    const currentCaches = [RUNTIME_CACHE];
76    
77    const cacheNames = await caches.keys();
78    const deletePromises = cacheNames
79      .filter((cacheName) => !currentCaches.includes(cacheName))
80      .map((cacheName) => {
81        console.log("[SW] Deleting old cache:", cacheName);
82        return caches.delete(cacheName);
83      });
84    
85    await Promise.all(deletePromises);
86  } catch (error) {
87    console.error("[SW] Error deleting old caches:", error);
88  }
89}
90
91/**
92 * Activate event: clean up old caches
93 */
94self.addEventListener("activate", (event) => {
95  console.log("[SW] Activating service worker...");
96
97  // Compute the full Promise chain first
98  const activationPromise = deleteOldCaches().then(() => self.clients.claim());
99
100  // Then pass the complete promise to waitUntil
101  event.waitUntil(activationPromise);
102});
103
104/**
105 * Fetch event: handle requests with caching strategies
106 */
107self.addEventListener("fetch", (event) => {
108  const { request } = event;
109  const url = new URL(request.url);
110
111  // Skip non-GET requests and non-http(s) schemes
112  if (request.method !== "GET" || !url.protocol.startsWith("http")) {
113    return;
114  }
115
116  // Never intercept cross-origin requests (e.g. api.infinitycolor.co).
117  // Let the browser perform them so CORS is handled by the real API response.
118  // Otherwise the SW can return 503 on failure without CORS headers → CORS error in console.
119  try {
120    if (url.origin !== self.origin) {
121      return;
122    }
123  } catch (_) {
124    return;
125  }
126
127  // The release probe and the worker's own version assignment must always hit
128  // the network. Cache-first `.js` handling would otherwise pin `sw-version.js`,
129  // and a generic network-first intercept of `build-version.json` can still
130  // replay a cached copy when the origin is briefly unreachable during a roll.
131  if (
132    url.pathname === "/build-version.json" ||
133    url.pathname === "/sw-version.js" ||
134    url.pathname === "/sw.js"
135  ) {
136    return;
137  }
138
139  // Sensitive data and authorization-aware routes must never be intercepted,
140  // cached, replayed, or replaced with synthetic service-worker responses.
141  if (request.headers.has("Authorization") || isSensitivePath(url.pathname)) {
142    return;
143  }
144
145  // Next.js owns cache busting and image optimization for these assets.
146  if (
147    url.pathname.startsWith("/_next/static/") ||
148    url.pathname.startsWith("/_next/chunks/") ||
149    url.pathname.startsWith("/_next/image")
150  ) {
151    // Let these requests pass through without service worker interception
152    return;
153  }
154
155  // Handle static assets with cache-first strategy
156  if (isStaticAsset(url.pathname)) {
157    event.respondWith(handleStaticAsset(request));
158    return;
159  }
160
161  // Navigations are intercepted for exactly one reason: serving `offline.html`
162  // to an INSTALLED PWA that is genuinely offline. For an ordinary browser tab
163  // `pwaModeClients` is empty, so there is nothing this worker can add to a
164  // navigation -- and everything to lose. Mediating it meant a transport-level
165  // failure (a blocking extension, a reset connection, flaky DNS) was answered
166  // with a synthetic 503 reading "Page not available", which the browser treats
167  // as a *successful* response: no native error page naming the real cause, no
168  // reload affordance, no automatic recovery. Handing navigations back to the
169  // browser is what makes the real error visible to the visitor and to a bug
170  // report.
171  if (isNavigationRequest(request) && pwaModeClients.size === 0) {
172    return;
173  }
174
175  // Handle other requests with network-first strategy + offline fallback
176  event.respondWith(handleGenericRequest(request));
177});
178
179/**
180 * Handle static assets (cache-first with network fallback)
181 * Images, CSS, JS, etc.
182 * For images, we use network-first to ensure they load properly
183 */
184async function handleStaticAsset(request) {
185  const { RUNTIME_CACHE } = getCacheNames();
186
187  // For images, try network first to ensure they load properly
188  // This prevents stale cache from blocking image loading
189  if (request.destination === "image" || isImagePath(request.url)) {
190    try {
191      const response = await fetch(request);
192
193      // Cache successful responses
194      if (response.ok) {
195        const cache = await caches.open(RUNTIME_CACHE);
196        cache.put(request, response.clone());
197      }
198
199      return response;
200    } catch (error) {
201      // If network fails, try cache
202      const cached = await caches.match(request);
203      if (cached) {
204        return cached;
205      }
206
207      // Only return placeholder if truly offline and no cache
208      console.log("[SW] Failed to fetch image:", request.url);
209      // Return a transparent 1x1 PNG placeholder to avoid unhandled rejection
210      // This prevents breaking offline UX while still allowing Next.js Image to handle gracefully
211      return new Response(
212        new Uint8Array([
213          0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
214          0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
215          0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
216          0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
217          0x42, 0x60, 0x82
218        ]),
219        {
220          status: 200,
221          statusText: "OK",
222          headers: { "Content-Type": "image/png" },
223        }
224      );
225    }
226  }
227
228  // For other static assets, use cache-first
229  const cached = await caches.match(request);
230  if (cached) {
231    return cached;
232  }
233
234  try {
235    const response = await fetch(request);
236
237    // Cache successful responses
238    if (response.ok) {
239      const cache = await caches.open(RUNTIME_CACHE);
240      cache.put(request, response.clone());
241    }
242
243    return response;
244  } catch (error) {
245    console.log("[SW] Failed to fetch static asset:", request.url);
246    // For other assets, return error
247    return new Response("Asset not available offline", {
248      status: 503,
249      statusText: "Service Unavailable",
250    });
251  }
252}
253
254/**
255 * Check if URL is an image path
256 */
257function isImagePath(url) {
258  const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif"];
259  try {
260    const urlObj = new URL(url);
261    return imageExtensions.some((ext) => urlObj.pathname.toLowerCase().endsWith(ext));
262  } catch {
263    return false;
264  }
265}
266
267// Store PWA mode status per client (in-memory cache)
268const pwaModeClients = new Set();
269
270// IndexedDB database name and store name for PWA mode persistence
271const IDB_DB_NAME = "pwa-mode-db";
272const IDB_STORE_NAME = "pwa-clients";
273
274/**
275 * Initialize IndexedDB for PWA mode persistence
276 */
277async function initPWAIndexedDB() {
278  return new Promise((resolve, reject) => {
279    try {
280      const request = indexedDB.open(IDB_DB_NAME, 1);
281      
282      request.onerror = () => {
283        console.warn("[SW] Failed to open IndexedDB for PWA mode, using in-memory only");
284        resolve(null);
285      };
286      
287      request.onsuccess = () => {
288        resolve(request.result);
289      };
290      
291      request.onupgradeneeded = (event) => {
292        const db = event.target.result;
293        if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
294          db.createObjectStore(IDB_STORE_NAME, { keyPath: "clientId" });
295        }
296      };
297    } catch (error) {
298      console.warn("[SW] IndexedDB not available, using in-memory only:", error);
299      resolve(null);
300    }
301  });
302}
303
304/**
305 * Load PWA mode clients from IndexedDB
306 */
307async function loadPWAClientsFromIDB() {
308  try {
309    const db = await initPWAIndexedDB();
310    if (!db) {
311      return; // Fall back to in-memory only
312    }
313    
314    const transaction = db.transaction([IDB_STORE_NAME], "readonly");
315    const store = transaction.objectStore(IDB_STORE_NAME);
316    const request = store.getAll();
317    
318    request.onsuccess = () => {
319      const clients = request.result || [];
320      clients.forEach((item) => {
321        if (item.clientId) {
322          pwaModeClients.add(item.clientId);
323        }
324      });
325      console.log("[SW] Loaded PWA clients from IndexedDB:", clients.length);
326    };
327    
328    request.onerror = () => {
329      console.warn("[SW] Failed to load PWA clients from IndexedDB");
330    };
331  } catch (error) {
332    console.warn("[SW] Error loading PWA clients from IndexedDB:", error);
333  }
334}
335
336/**
337 * Store PWA client in IndexedDB
338 */
339async function storePWAClientInIDB(clientId, isPWA) {
340  try {
341    const db = await initPWAIndexedDB();
342    if (!db) {
343      return; // Fall back to in-memory only
344    }
345    
346    const transaction = db.transaction([IDB_STORE_NAME], "readwrite");
347    const store = transaction.objectStore(IDB_STORE_NAME);
348    
349    if (isPWA) {
350      store.put({ clientId, isPWA, timestamp: Date.now() });
351    } else {
352      store.delete(clientId);
353    }
354  } catch (error) {
355    console.warn("[SW] Error storing PWA client in IndexedDB:", error);
356  }
357}
358
359/**
360 * Check if a client is in PWA mode (standalone display mode)
361 * Clients should send a message with type "PWA_MODE" and isPWA boolean
362 */
363async function isPWAMode(clientId) {
364  // If we have stored info about this client, use it
365  if (clientId && pwaModeClients.has(clientId)) {
366    return true;
367  }
368
369  // Default: don't assume PWA mode (conservative approach)
370  // offline.html should only show for confirmed PWA users
371  return false;
372}
373
374// Load PWA clients from IndexedDB on service worker startup
375loadPWAClientsFromIDB();
376
377/**
378 * Check if request is a navigation request (HTML page request)
379 */
380function isNavigationRequest(request) {
381  // Check if request mode is navigate (browser navigation)
382  if (request.mode === "navigate") {
383    return true;
384  }
385
386  // Check Accept header for HTML content
387  const acceptHeader = request.headers.get("Accept");
388  if (acceptHeader && acceptHeader.includes("text/html")) {
389    return true;
390  }
391
392  // Check if request destination is document
393  if (request.destination === "document") {
394    return true;
395  }
396
397  return false;
398}
399
400/**
401 * Check if error indicates offline state (network error)
402 */
403function isNetworkError(error) {
404  // Network errors typically have no response or are TypeError
405  // Server errors (4xx, 5xx) are not network errors
406  return (
407    error instanceof TypeError ||
408    error?.name === "NetworkError" ||
409    error.message?.includes("Failed to fetch") ||
410    error.message?.includes("NetworkError")
411  );
412}
413
414/**
415 * Handle generic requests (network-first)
416 * Try network first, fall back to cache
417 * Only show offline.html for PWA users when actually offline and for navigation requests
418 */
419async function handleGenericRequest(request) {
420  const { RUNTIME_CACHE } = getCacheNames();
421
422  try {
423    const response = await fetch(request);
424
425    return response;
426  } catch (error) {
427    // Check if this is a network error (offline) vs server error
428    const isOffline = isNetworkError(error);
429
430    // Try cache first
431    const cached = await caches.match(request);
432    if (cached) {
433      return cached;
434    }
435
436    // Only show offline.html if:
437    // 1. It's a navigation request (HTML page)
438    // 2. User is actually offline (network error, not server error)
439    // 3. User is in PWA mode (confirmed via client message)
440    const isNav = isNavigationRequest(request);
441
442    if (isNav && isOffline) {
443      // Check if user is in PWA mode by checking stored client info
444      try {
445        const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
446        let isPWA = false;
447
448        // Check if any of the clients are in PWA mode
449        for (const client of clients) {
450          const clientId = client.id || client.url || "default";
451          if (pwaModeClients.has(clientId)) {
452            isPWA = true;
453            break;
454          }
455        }
456
457        // Only return offline.html for PWA users
458        if (isPWA) {
459          const offlinePage = await caches.match("/offline.html");
460          if (offlinePage) {
461            console.log("[SW] Returning offline.html for PWA user");
462            return offlinePage;
463          }
464        } else {
465          console.log("[SW] Not showing offline.html - user is not in PWA mode");
466        }
467      } catch (err) {
468        console.log("[SW] Error checking PWA mode:", err);
469      }
470    }
471
472    // Only a PWA navigation still reaches here (see the guard in the fetch
473    // listener), and only when `offline.html` was not available to serve.
474    // Re-throw rather than synthesising a 503: a rejected `respondWith` gives
475    // the browser its own error page, which carries a Reload affordance and
476    // retries normally, instead of a dead plain-text body the browser believes
477    // succeeded. The page itself is generic (`ERR_FAILED`) -- the specific
478    // cause survives only in the console line below, which is why it logs the
479    // error name and message verbatim.
480    //
481    // A 4xx/5xx from the origin never reaches this branch -- `fetch` rejects
482    // only below HTTP -- so this cannot mask a server-rendered error page.
483    if (isNavigationRequest(request)) {
484      console.error(
485        "[SW] Navigation fetch failed; deferring to the browser error page:",
486        request.url,
487        error && (error.name || error.message) ? `${error.name}: ${error.message}` : error,
488      );
489      throw error;
490    }
491
492    // For API or other requests, return JSON error
493    return new Response(JSON.stringify({ error: "Request failed", message: error.message }), {
494      status: 503,
495      statusText: "Service Unavailable",
496      headers: { "Content-Type": "application/json" },
497    });
498  }
499}
500
501/**
502 * Check if a path is a static asset
503 */
504function isStaticAsset(pathname) {
505  const staticExtensions = [
506    ".js",
507    ".css",
508    ".png",
509    ".jpg",
510    ".jpeg",
511    ".gif",
512    ".svg",
513    ".woff",
514    ".woff2",
515    ".ttf",
516    ".eot",
517  ];
518  return staticExtensions.some((ext) => pathname.endsWith(ext));
519}
520
521/**
522 * Handle messages from clients
523 */
524self.addEventListener("message", (event) => {
525  console.log("[SW] Message received:", event.data);
526
527  if (event.data && event.data.type === "SKIP_WAITING") {
528    self.skipWaiting();
529  }
530
531  if (event.data && event.data.type === "CLEAR_CACHE") {
532    clearAllCaches();
533  }
534
535  // Handle PWA mode status from client
536  if (event.data && event.data.type === "PWA_MODE") {
537    // Get the client that sent the message
538    const client = event.source;
539    if (client) {
540      const clientId = client.id || client.url || "default";
541      const isPWA = event.data.isPWA;
542      
543      if (isPWA) {
544        // Store client ID in memory and IndexedDB
545        pwaModeClients.add(clientId);
546        storePWAClientInIDB(clientId, true);
547        console.log("[SW] Client is in PWA mode:", clientId);
548      } else {
549        // Remove client ID from memory and IndexedDB
550        pwaModeClients.delete(clientId);
551        storePWAClientInIDB(clientId, false);
552        console.log("[SW] Client is not in PWA mode:", clientId);
553      }
554    }
555  }
556});
557
558/**
559 * Clear all caches
560 */
561async function clearAllCaches() {
562  const cacheNames = await caches.keys();
563  return Promise.all(cacheNames.map((name) => caches.delete(name)));
564}
565
566console.log("[SW] Service worker loaded");

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.