PageSourceSearch

https://notetube.ai/sw.js

js notetube.ai collected 2026-10-05 14:14:06 UTC 4,756 bytes, 154 lines download raw bytes

1// NoteTube Service Worker
2const CACHE_NAME = 'notetube-v3';
3const OFFLINE_URL = '/offline.html';
4
5// Assets to cache on install.
6// Do NOT precache '/' — its HTML references content-hashed JS/CSS chunks that
7// rotate every deploy, so a precached copy goes stale immediately. Navigation
8// requests are served network-first with /offline.html as the only fallback.
9const STATIC_ASSETS = [
10  '/manifest.json',
11  '/offline.html',
12];
13
14// Install event - cache static assets
15self.addEventListener('install', (event) => {
16  event.waitUntil(
17    caches.open(CACHE_NAME).then((cache) => {
18      // Cache static assets, but don't fail if some aren't available
19      return cache.addAll(STATIC_ASSETS).catch((error) => {
20        console.warn('Failed to cache some static assets:', error);
21      });
22    })
23  );
24  // Activate immediately
25  self.skipWaiting();
26});
27
28// Activate event - clean up old caches
29self.addEventListener('activate', (event) => {
30  event.waitUntil(
31    caches.keys().then((cacheNames) => {
32      return Promise.all(
33        cacheNames
34          .filter((name) => name !== CACHE_NAME)
35          .map((name) => caches.delete(name))
36      );
37    })
38  );
39  // Take control of all pages immediately
40  self.clients.claim();
41});
42
43// Fetch event - network first, fallback to cache
44self.addEventListener('fetch', (event) => {
45  const { request } = event;
46  const url = new URL(request.url);
47
48  // Skip non-GET requests
49  if (request.method !== 'GET') {
50    return;
51  }
52
53  // Skip cross-origin requests
54  if (url.origin !== self.location.origin) {
55    return;
56  }
57
58  // Skip API routes - always go to network
59  if (url.pathname.startsWith('/api/')) {
60    return;
61  }
62
63  // Skip auth-related routes
64  if (url.pathname.startsWith('/auth/')) {
65    return;
66  }
67
68  // For navigation requests (HTML pages): always network, never cache HTML.
69  // HTML references content-hashed JS/CSS chunks; caching it long-term causes
70  // drift across deploys (stale HTML points at dead hashes → MIME errors →
71  // hydration never runs). Only fall back to offline page when network fails.
72  if (request.mode === 'navigate') {
73    event.respondWith(
74      fetch(request).catch(async () => {
75        // Network failed. Fall back to the cached offline page. If install's
76        // best-effort addAll never cached it, caches.match resolves to
77        // undefined — respondWith(undefined) throws "Failed to convert value
78        // to 'Response'", so always hand back a real Response.
79        const offline = await caches.match(OFFLINE_URL);
80        return (
81          offline ||
82          new Response(
83            '<!doctype html><title>Offline</title><h1>You are offline</h1>',
84            { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
85          )
86        );
87      })
88    );
89    return;
90  }
91
92  // For other requests (assets, images, etc.)
93  event.respondWith(
94    fetch(request)
95      .then((response) => {
96        // Cache successful responses
97        if (response.ok) {
98          const responseClone = response.clone();
99          caches.open(CACHE_NAME).then((cache) => {
100            cache.put(request, responseClone);
101          });
102        }
103        return response;
104      })
105      .catch(async () => {
106        // Network failed. Serve the cached copy if we have one; otherwise
107        // return a network-error Response (NOT undefined, which would throw
108        // "Failed to convert value to 'Response'"). Response.error() reads to
109        // the page as a genuine failed fetch — the correct outcome for an
110        // uncached asset whose network request died.
111        const cached = await caches.match(request);
112        return cached || Response.error();
113      })
114  );
115});
116
117// Handle push notifications (for future use)
118self.addEventListener('push', (event) => {
119  if (event.data) {
120    const data = event.data.json();
121    const options = {
122      body: data.body || 'New notification from NoteTube',
123      icon: '/icon-192x192.png',
124      badge: '/icon-72x72.png',
125      vibrate: [100, 50, 100],
126      data: {
127        url: data.url || '/',
128      },
129    };
130    event.waitUntil(
131      self.registration.showNotification(data.title || 'NoteTube', options)
132    );
133  }
134});
135
136// Handle notification click
137self.addEventListener('notificationclick', (event) => {
138  event.notification.close();
139  const url = event.notification.data?.url || '/';
140  event.waitUntil(
141    clients.matchAll({ type: 'window' }).then((clientList) => {
142      // Focus existing window if available
143      for (const client of clientList) {
144        if (client.url === url && 'focus' in client) {
145          return client.focus();
146        }
147      }
148      // Open new window
149      if (clients.openWindow) {
150        return clients.openWindow(url);
151      }
152    })
153  );
154});

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.