PageSourceSearch

https://cryptobeast.ai/service-worker.js

js cryptobeast.ai collected 2026-10-05 16:23:22 UTC 10,045 bytes, 312 lines download raw bytes

1/**
2 * Service Worker for AI Crypto News
3 * Offline support + tuned caching strategies
4 * NOTE: this file lives in /public and must be self-contained (no imports).
5 */
6
7// ===== Versioning =====
8const VERSION = "v3.2.219"; // ↑ bump on SW changes (added push notifications)
9
10// ===== Brand config =====
11const BRAND = {
12    name: "CryptoBeast",
13    bg: "#0a0a0f",
14    fg: "#E85A32"
15};
16
17// Cache keys (isolate per version)
18const HOST = self.location.host;
19const CACHE_PREFIX = `aicrypto-${HOST}`;
20const RUNTIME_CACHE = `${CACHE_PREFIX}-runtime-${VERSION}`;
21const STATIC_CACHE = `${CACHE_PREFIX}-static-${VERSION}`;
22const HTML_CACHE = `${CACHE_PREFIX}-html-${VERSION}`;
23
24// Assets to precache (root-relative; middleware prepisuje na /skins/{skin}/…)
25const PRECACHE_ASSETS = [
26    "/",
27    "/favicon.ico",
28    "/favicon-48x48.png",
29    "/favicon-32x32.png", // + (opciono, ali korisno)
30    "/apple-touch-icon.png", // + (opciono, iOS add-to-home)
31    "/logo.png",
32    "/android-chrome-192x192.png",
33    "/android-chrome-512x512.png",
34    "/site.webmanifest",
35];
36
37// File type helpers
38const STATIC_EXT_RE = /\.(?:png|jpg|jpeg|webp|svg|gif|ico|css|js|mjs|woff2?|ttf|otf|eot|json)$/i;
39
40// Strategije
41const STRATEGY = {
42    NETWORK_FIRST: "network-first",
43    CACHE_FIRST: "cache-first",
44    NETWORK_ONLY: "network-only",
45    CACHE_ONLY: "cache-only",
46};
47
48// Enable navigation preload if available
49self.addEventListener("activate", (event) => {
50    event.waitUntil(
51        (async () => {
52            if (self.registration.navigationPreload) {
53                try {
54                    await self.registration.navigationPreload.enable();
55                } catch {}
56            }
57        })()
58    );
59});
60
61// Install – precache osnovnih asseta
62self.addEventListener("install", (event) => {
63    event.waitUntil(
64        caches
65            .open(STATIC_CACHE)
66            .then((cache) => cache.addAll(PRECACHE_ASSETS))
67            .catch(() => {})
68    );
69    self.skipWaiting();
70});
71
72// Activate – cleanup starih keševa (po prefiksu)
73self.addEventListener("activate", (event) => {
74    event.waitUntil(
75        caches
76            .keys()
77            .then((keys) =>
78                Promise.all(
79                    keys.map((k) =>
80                        k.startsWith(`${CACHE_PREFIX}-`) &&
81                        ![RUNTIME_CACHE, STATIC_CACHE, HTML_CACHE].includes(k)
82                            ? caches.delete(k)
83                            : Promise.resolve()
84                    )
85                )
86            )
87    );
88    self.clients.claim();
89});
90
91// Fetch
92self.addEventListener("fetch", (event) => {
93    const req = event.request;
94    const url = new URL(req.url);
95
96    // GET only
97    if (req.method !== "GET") return;
98
99    // non-http(s), chrome-extension, data:, etc.
100    if (!url.protocol.startsWith("http")) return;
101
102    // Different origin → let it go (ili primeni network-first ako želiš)
103    // Ovde ostavljamo default: ne cache-ujemo cross-origin (manje bugova)
104    if (url.origin !== self.location.origin) return;
105
106    // Skip Next.js internals koji nisu static buildovi
107    if (url.pathname.startsWith("/_next/image")) return; // image optimizer ima sopstvenu cache logiku
108    // _next/static & /skins/ → static cache-first
109    const isNextStatic = url.pathname.startsWith("/_next/static/");
110    const isSkinsPath = url.pathname.startsWith("/skins/");
111    const isOg = url.pathname.startsWith("/og/"); // middleware rewrite → /skins/{skin}/og/*
112
113    // Odredi strategiju
114    let strategy;
115    if (url.pathname.startsWith("/api/")) {
116        strategy = STRATEGY.NETWORK_FIRST; // API uvek svež
117    } else if (isNextStatic || isSkinsPath || STATIC_EXT_RE.test(url.pathname)) {
118        strategy = STRATEGY.CACHE_FIRST; // static i assets
119    } else if (isOg) {
120        strategy = STRATEGY.CACHE_FIRST; // OG slike – brzo učitavanje
121    } else if (acceptsHtml(req)) {
122        strategy = STRATEGY.NETWORK_FIRST; // HTML (stranice)
123    } else {
124        strategy = STRATEGY.NETWORK_FIRST; // default
125    }
126
127    event.respondWith(handle(req, strategy));
128});
129
130// ===== Helpers =====
131
132function acceptsHtml(request) {
133    const accept = request.headers.get("accept") || "";
134    return accept.includes("text/html");
135}
136
137async function handle(request, strategy) {
138    switch (strategy) {
139        case STRATEGY.CACHE_FIRST:
140            return cacheFirst(request);
141        case STRATEGY.NETWORK_FIRST:
142            return networkFirst(request);
143        case STRATEGY.NETWORK_ONLY:
144            return fetch(request);
145        case STRATEGY.CACHE_ONLY:
146            return caches.match(request);
147        default:
148            return networkFirst(request);
149    }
150}
151
152async function cacheFirst(request) {
153    const cache = await caches.open(STATIC_CACHE);
154    const cached = await cache.match(request);
155    if (cached) {
156        // quiet background refresh
157        fetchAndUpdate(cache, request);
158        return cached;
159    }
160    try {
161        const res = await fetch(request);
162        if (isCacheable(res)) cache.put(request, res.clone());
163        return res;
164    } catch {
165        return offlineFallback(request);
166    }
167}
168
169async function networkFirst(request) {
170    const cacheStore = acceptsHtml(request) ? HTML_CACHE : RUNTIME_CACHE;
171    const cache = await caches.open(cacheStore);
172
173    // Try navigationPreload first for HTML
174    if (acceptsHtml(request) && self.registration.navigationPreload) {
175        try {
176            const preloaded = await self.registration.navigationPreload.getState();
177            // if enabled, browser may provide a preloaded response
178        } catch {}
179    }
180
181    try {
182        const res = await fetch(request);
183        if (isCacheable(res)) cache.put(request, res.clone());
184        return res;
185    } catch {
186        const cached = await cache.match(request);
187        if (cached) return cached;
188        return offlineFallback(request);
189    }
190}
191
192function isCacheable(response) {
193    return (
194        response &&
195        response.status === 200 &&
196        (response.type === "basic" || response.type === "default")
197    );
198}
199
200function fetchAndUpdate(cache, request) {
201    fetch(request)
202        .then((res) => {
203            if (isCacheable(res)) cache.put(request, res.clone());
204        })
205        .catch(() => {});
206}
207
208// Branded offline fallback
209function offlineFallback(request) {
210    if (acceptsHtml(request)) {
211        const html = `
212      <!DOCTYPE html>
213      <html lang="en">
214      <head>
215        <meta charset="UTF-8" />
216        <meta name="viewport" content="width=device-width, initial-scale=1" />
217        <title>Offline - ${escapeHtml(BRAND.name)}</title>
218        <style>
219          :root { --bg:${BRAND.bg}; --fg:${BRAND.fg}; }
220          html,body{height:100%}
221          body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;background:var(--bg);color:var(--fg);display:flex;align-items:center;justify-content:center;padding:24px;text-align:center}
222          .wrap{max-width:520px}
223          h1{font-size:24px;margin:0 0 12px}
224          p{opacity:.85;line-height:1.6;margin:0 0 20px}
225          button{padding:12px 20px;border:0;border-radius:10px;background:var(--fg);color:var(--bg);font-weight:700;cursor:pointer}
226          button:active{transform:translateY(1px)}
227        </style>
228      </head>
229      <body>
230        <div class="wrap">
231          <h1>📡 You're Offline</h1>
232          <p>Unable to connect to ${escapeHtml(BRAND.name)}. Check your internet connection and try again.</p>
233          <button onclick="location.reload()">Retry</button>
234        </div>
235      </body>
236      </html>`;
237        return new Response(html, { status: 503, headers: { "Content-Type": "text/html" } });
238    }
239    return new Response("Offline", { status: 503 });
240}
241
242function escapeHtml(s) {
243    return String(s).replace(
244        /[&<>"']/g,
245        (m) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" })[m]
246    );
247}
248
249// Messages (optional commands)
250self.addEventListener("message", (event) => {
251    if (!event.data) return;
252    if (event.data.type === "SKIP_WAITING") self.skipWaiting();
253    if (event.data.type === "CLEAR_CACHE") {
254        event.waitUntil(
255            (async () => {
256                const keys = await caches.keys();
257                await Promise.all(
258                    keys.filter((k) => k.startsWith(CACHE_PREFIX)).map((k) => caches.delete(k))
259                );
260                // eslint-disable-next-line no-console
261                console.log("[SW] Cleared caches for", CACHE_PREFIX);
262            })()
263        );
264    }
265});
266
267// ===== Push Notifications =====
268self.addEventListener("push", (event) => {
269    if (!event.data) return;
270
271    let payload;
272    try {
273        payload = event.data.json();
274    } catch {
275        payload = { title: BRAND.name, body: event.data.text() };
276    }
277
278    const { title, body, icon, badge, url, tag, data, actions } = payload;
279
280    event.waitUntil(
281        self.registration.showNotification(title || BRAND.name, {
282            body: body || "",
283            icon: icon || "/android-chrome-192x192.png",
284            badge: badge || "/favicon-48x48.png",
285            tag: tag || "cb-default",
286            data: { url: url || "/", ...data },
287            vibrate: [100, 50, 100],
288            actions: actions || [],
289        })
290    );
291});
292
293self.addEventListener("notificationclick", (event) => {
294    event.notification.close();
295    const targetUrl = event.notification.data?.url || "/";
296
297    event.waitUntil(
298        clients.matchAll({ type: "window", includeUncontrolled: true }).then((windowClients) => {
299            for (const client of windowClients) {
300                if (client.url.includes(self.location.origin) && "focus" in client) {
301                    client.navigate(targetUrl);
302                    return client.focus();
303                }
304            }
305            return clients.openWindow(targetUrl);
306        })
307    );
308});
309
310// Log
311// eslint-disable-next-line no-console
312console.log("[SW] Loaded", { VERSION, HOST, RUNTIME_CACHE, STATIC_CACHE, HTML_CACHE });

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.