A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322/// <reference lib="webworker" />
import { isAudioFile } from "~/components/input/common.js";import { create as createCid } from "./common/cid.js";
/** @type {Record<string, string>} */const FILE_TREE = JSON.parse("__FILE_TREE__");
/** * Identifier for the current build, injected at build time. Used to version * the offline caches so a new deploy never reads cache entries written by a * previous build (which may hold stale or poisoned bodies, e.g. a cached 404 * for a file that only recently started existing). */const BUILD_ID = "__BUILD_ID__";
/** Media content types to ignore */const MEDIA_CONTENT_TYPE = /^(audio|video)\//;
/** Multicodec code for raw binary content. */const RAW_CODEC = 0x55;
const { searchParams } = new URL(location.href);const CACHE_NAME = searchParams.get("cache-name") ?? "diffuse-offline";
/** * All caches created by this worker are prefixed with this value plus the * build id, so each build gets its own isolated set of caches. */const CACHE_PREFIX = `${CACHE_NAME}@${BUILD_ID}`;
const thyself = /** @type {ServiceWorkerGlobalScope & typeof globalThis} */ (/** @type {unknown} */ (self));
////////////////////////////////////////////// INSTALL////////////////////////////////////////////
self.addEventListener("install", (_event) => { // Activate immediately without waiting for existing clients to close. /** @type {ExtendableEvent} */ (_event).waitUntil(thyself.skipWaiting());});
////////////////////////////////////////////// ACTIVATE////////////////////////////////////////////
self.addEventListener("activate", (event) => { // Delete caches belonging to previous builds so stale content is never // served after a deploy. Caches from the current build share `CACHE_PREFIX` // and are kept; anything else this worker created (older builds, or the // unversioned `diffuse-offline:*` caches from before versioning existed) // is removed. const dropStaleCaches = caches.keys().then((keys) => Promise.all( keys .filter((key) => key === CACHE_NAME || key.startsWith(`${CACHE_NAME}:`) || key.startsWith(`${CACHE_NAME}@`) ) .filter((key) => !key.startsWith(CACHE_PREFIX)) .map((key) => caches.delete(key)), ), );
// Claim clients, then signal each window to reload so no old code stays // active after the new SW takes over. Signalling via postMessage (rather // than client.navigate) lets the page debounce the reload and break the // loop that would otherwise occur when the SW script itself changes between // reloads (e.g. esbuild chunk-hash churn during development). /** @type {ExtendableEvent} */ (event).waitUntil( dropStaleCaches.then(() => thyself.clients.claim().then(() => thyself.clients.matchAll({ type: "window" }).then((clients) => { for (const client of clients) { client.postMessage({ type: "sw-activated" }); } }), ) ), );});
////////////////////////////////////////////// FETCH////////////////////////////////////////////
self.addEventListener("fetch", (_event) => { const event = /** @type {FetchEvent} */ (_event); const { request } = event;
if (!request.url.startsWith("http")) return;
// Intercept credentialed URLs before the offline cache (any method). const intercepted = interceptCredentials(request); if (intercepted) { event.respondWith(intercepted); return; }
// Only cache GET requests. if (request.method !== "GET") return;
// Skip audio/video and partial content const pathname = new URL(request.url).pathname;
if (request.headers.get("range")) return; if (isAudioFile(pathname)) return;
// ⚙️ event.respondWith(handleFetch(request));});
////////////////////////////////////////////// CREDENTIAL INTERCEPT////////////////////////////////////////////
/** * If the request URL contains a `_auth` query parameter (base64 Basic credentials), * strip it and re-issue the request with a proper Authorization header instead. * Also handles the legacy `user:pass@host` form for any callers that still use it. * * Returns a Promise<Response> when credentials are detected, or null to fall through. * * @param {Request} request * @returns {Promise<Response> | null} */function interceptCredentials(request) { const url = new URL(request.url);
if (url.username) { const credentials = `${decodeURIComponent(url.username)}:${ decodeURIComponent(url.password) }`; url.username = ""; url.password = ""; const headers = new Headers(request.headers); headers.set( "Authorization", `Basic ${btoa(unescape(encodeURIComponent(credentials)))}`, ); return fetch(url.href, { method: request.method, headers, signal: request.signal, }); }
const auth = url.searchParams.get("diffuse:basic-auth"); if (!auth) return null;
url.searchParams.delete("diffuse:basic-auth"); const headers = new Headers(request.headers); headers.set("Authorization", `Basic ${auth}`); return fetch(url.href, { method: request.method, headers, signal: request.signal, });}
////////////////////////////////////////////// CONTENT-ADDRESSED CACHE////////////////////////////////////////////
/** * @param {string} cid */function cidUrl(cid) { return `https://diffuse.offline.worker/${cid}`;}
/** * Opens the two caches used for content-addressed storage. * * - `<prefix>:index` maps original request URL → CID string (text/plain) * - `<prefix>:content` maps `https://diffuse.offline.worker/<cid>` → full response (deduplicated) */async function openCaches() { const [index, content] = await Promise.all([ caches.open(CACHE_PREFIX + ":index"), caches.open(CACHE_PREFIX + ":content"), ]); return { index, content };}
/** * Looks up a pathname in the embedded file tree and returns its CID, or * `undefined` if the entry is absent. * * @param {string} pathname - e.g. "/components/foo.js" * @returns {string | undefined} */function cidFromTree(pathname) { const key = pathname.replace(/^\//, ""); return FILE_TREE[key];}
/** * Computes the CID of `response`'s body and writes it into the two-level cache. * The same content is stored only once, regardless of how many URLs reference it. * * The CID is always derived from the actual response bytes so the cache key * is guaranteed to match the stored content — we never trust the pre-built * file tree here because the response may have been served from the browser's * HTTP cache and could be stale. * * @param {Request} request * @param {Response} response - a clone; its body is fully consumed here */async function store(request, response) { const cid = await createCid( RAW_CODEC, new Uint8Array(await response.clone().arrayBuffer()), ); const cidKey = cidUrl(cid);
const caches = await openCaches();
// Only store the content if we haven't seen this CID before if (!(await caches.content.match(cidKey))) { await caches.content.put(new Request(cidKey), response); }
// Update the URL → CID map await caches.index.put( new Request(request.url), new Response(cid, { headers: { "content-type": "text/plain" } }), );}
/** * Resolves the cached response for a request via the URL → CID index. * * @param {Request} request * @returns {Promise<Response | undefined>} */async function lookup(request) { const caches = await openCaches();
const indexEntry = await caches.index.match(request); if (!indexEntry) return undefined;
const cid = await indexEntry.text(); return caches.content.match(cidUrl(cid));}
////////////////////////////////////////////// HANDLER////////////////////////////////////////////
/** * Cache-first for file-tree entries, network-first for everything else. * * Online + in file tree → serve from cache if present, otherwise fetch and store. * Online + not in tree → fetch from network, store response by CID, return it. * Offline → resolve the URL through the index, serve by CID from the content cache. * * Partial responses (206) are passed through without caching so that * range requests for audio streaming work as normal. * * @param {Request} request * @returns {Promise<Response>} */async function handleFetch(request) { if (navigator.onLine) { const { pathname } = new URL(request.url); const cid = cidFromTree(pathname); if (cid !== undefined) { const { content } = await openCaches(); const cached = await content.match(cidUrl(cid)); if (cached) return cached; }
try { const response = await fetchAndStore(request); if (response.status !== 404) return response; if (cid === undefined) return response; } catch { if (cid === undefined) { return new Response(null, { status: 503, statusText: "Unavailable asset, not cached", }); } } }
const cached = await lookup(request); if (cached) return cached;
return new Response(null, { status: 503, statusText: "Unavailable asset, not cached", });}
/** * @param {Request} request */async function fetchAndStore(request) { // Validate with the server so the browser's HTTP cache never serves stale // content to the offline cache. (The first request after a deploy would // otherwise return the old page from the HTTP cache, and store() would // permanently slot it under the new CID.) const response = await fetch(request, { cache: "no-cache" });
// Partial content (range requests) — return as-is, do not cache. if (response.status === 206) return response;
// Skip caching audio/video. const contentType = response.headers.get("content-type") ?? ""; if (MEDIA_CONTENT_TYPE.test(contentType)) return response;
// Cache full successful responses, including opaque cross-origin ones. if (response.status === 200 || response.type === "opaque") { store(request, response.clone()).catch(() => {}); }
return response;}