(() => { "use strict"; let cancelled = false; // Handle messages from clients self.addEventListener("message", (event) => { if (event.data && event.data.type === "SKIP_WAITING") { self.skipWaiting(); } if (event.data && event.data.type === "CLEAR_CACHES") { caches .keys() .then((cacheNames) => Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))) ); } }); // Immediately install and skip waiting self.addEventListener("install", (event) => { self.skipWaiting(); }); // Claim clients on activation and clear caches self.addEventListener("activate", (event) => { event.waitUntil( Promise.all([ clients.claim(), self.skipWaiting(), caches .keys() .then((cacheNames) => Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))) ), ]) ); }); // Handle fetch events self.addEventListener("fetch", (event) => { const requestURL = event.request.url; // Only handle WebTorrent streaming requests if (!requestURL.includes("/webtorrent/")) { return; } // Create a promise to handle the request const responsePromise = (async () => { // 1) Keepalive check if (requestURL.includes("/webtorrent/keepalive/")) { return new Response(); } // 2) Cancel check if (requestURL.includes("/webtorrent/cancel/")) { return new Response( new ReadableStream({ cancel() { cancelled = true; }, }) ); } // 3) Streaming requests // We define an async function and immediately invoke it with `event` return (async function ({ request }) { const { url, method, headers, destination } = request; // 3a) Find open window clients const windowClients = await clients.matchAll({ type: "window", includeUncontrolled: true, }); // 3b) We send a message to each client with a MessageChannel const [clientResponse, port] = await new Promise((resolve) => { for (const client of windowClients) { const channel = new MessageChannel(); channel.port1.onmessage = ({ data }) => { resolve([data, channel.port1]); }; client.postMessage( { url, method, headers: Object.fromEntries(headers.entries()), scope: self.registration.scope, destination, type: "webtorrent", }, [channel.port2] ); } }); // 3c) Setup a small helper to close the port let timeoutId = null; const closeChannel = () => { port.postMessage(false); clearTimeout(timeoutId); port.onmessage = null; }; // 3d) Build a Headers object that prevents caching const responseHeaders = new Headers(clientResponse.headers); responseHeaders.set( "Cache-Control", "no-cache, no-store, must-revalidate, max-age=0" ); responseHeaders.set("Pragma", "no-cache"); responseHeaders.set("Expires", "0"); // 3e) If the response is not a streaming request, return it directly if (clientResponse.body !== "STREAM") { closeChannel(); return new Response(clientResponse.body, { status: clientResponse.status, statusText: clientResponse.statusText, headers: responseHeaders, }); } // 3f) Otherwise, create a streaming response return new Response( new ReadableStream({ pull(controller) { return new Promise((resolvePull) => { port.onmessage = ({ data }) => { if (data) { controller.enqueue(data); } else { closeChannel(); controller.close(); } resolvePull(); }; // If not cancelled, auto‐close after 5s of no data // *** Increase the timeout to avoid frequent CPU wake-ups *** if (!cancelled && destination !== "document") { clearTimeout(timeoutId); timeoutId = setTimeout(() => { closeChannel(); resolvePull(); }, 30000); // 30 seconds } // Request next chunk from client port.postMessage(true); }); }, cancel() { closeChannel(); }, }), { status: clientResponse.status, statusText: clientResponse.statusText, headers: responseHeaders, } ); })(event); })(); // respondWith the promise if it exists event.respondWith(responsePromise); }); })();