cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
root / app / static / prefetch.js

prefetch.js (10131B)


      1 /*!
      2  * cdnV2 link prefetcher.
      3  *
      4  * Once a page has settled, quietly fetches the pages it links to (folders, file pages, the navbar
      5  * destinations) so the server's listing cache is already warm when you click. The expensive part of a
      6  * click is the cold Hugging Face lookup behind a folder, and that cache only lives for 60 seconds, so
      7  * this re-warms it just ahead of use.
      8  *
      9  * Deliberately conservative:
     10  *   - same-origin links only; never /api/*, /admin, /mask/*, /pack/*, /static/*, /raw/*, downloads,
     11  *     links with target/download, or anything inside data-turbo="false" / data-no-prefetch
     12  *   - at most 16 pages per page view, 3 at a time, page content before navbar links, visible links
     13  *     first, and only while the tab is visible
     14  *   - idle time only; cancelled the moment you navigate so it never competes with a real click
     15  *   - skipped entirely on Save-Data, 2g connections and prefers-reduced-data
     16  *
     17  * Opt out: <meta name="cdn-prefetch" content="off"> on a page, or data-no-prefetch on a link/container.
     18  * Works with Turbo (re-runs on turbo:load) and without it. Loaded once from <head>; safe to include twice.
     19  */
     20 (function (global) {
     21   'use strict';
     22 
     23   // ttlMs stays below the pages' Cache-Control max-age (app/fast_html.py, 30 s): a page this old is
     24   // re-fetched, one younger is still in the browser's cache and needs no work.
     25   const DEFAULTS = { maxLinks: 16, concurrency: 3, ttlMs: 25000 };
     26 
     27   // First path segments that belong to the app itself, not to the CDN's content. Anything under them is
     28   // an API, a redirect, an admin page or a download, so it must never be fetched speculatively.
     29   const RESERVED_SEGMENTS = new Set(['api', 'admin', 'mask', 'pack', 'static', 'raw']);
     30 
     31   function pathAllowed(pathname) {
     32     if (pathname === '/favicon.ico') return false;
     33     const first = pathname.split('/')[1] || '';
     34     return !RESERVED_SEGMENTS.has(first);
     35   }
     36 
     37   // Returns the normalised URL to prefetch for a link, or null if it should be left alone.
     38   // `link` is plain data ({href, target, download, noPrefetch, turboOff}) and `base` a URL for the
     39   // current page, so this stays testable without a DOM.
     40   function candidateUrl(link, base) {
     41     if (link.download || link.noPrefetch || link.turboOff) return null;
     42     if (link.target && link.target !== '_self') return null;
     43     let url;
     44     try {
     45       url = new URL(link.href, base);
     46     } catch (e) {
     47       return null;
     48     }
     49     if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
     50     if (url.origin !== base.origin) return null;
     51     if (!pathAllowed(url.pathname)) return null;
     52     if (url.pathname === base.pathname && url.search === base.search) return null; // this page
     53     url.hash = '';
     54     return url.href;
     55   }
     56 
     57   // A small bounded work queue. `fetchFn(url, signal)` must return a promise that settles when the
     58   // response has been fully read (the server only finishes warming its cache once it has answered).
     59   function createPrefetcher(opts) {
     60     const cfg = Object.assign({}, DEFAULTS, opts.config);
     61     const now = opts.now || Date.now;
     62     const seen = new Map(); // url -> when it last finished; stops repeat fetches inside the cache TTL
     63     const inflight = new Map(); // url -> AbortController, so one request can be cancelled on its own
     64     let queue = [];
     65     let queued = new Set();
     66     let active = 0;
     67     let used = 0; // pages accepted for the current page view (the budget)
     68     let paused = false;
     69 
     70     function fresh(url) {
     71       const t = seen.get(url);
     72       return t !== undefined && now() - t < cfg.ttlMs;
     73     }
     74 
     75     function run(url) {
     76       active++;
     77       const ctl = new AbortController();
     78       inflight.set(url, ctl);
     79       Promise.resolve()
     80         .then(() => opts.fetchFn(url, ctl.signal))
     81         .then(
     82           () => seen.set(url, now()),
     83           // A failed fetch is remembered too, so a flaky or 404 link is not retried in a loop.
     84           () => { if (!ctl.signal.aborted) seen.set(url, now()); }
     85         )
     86         .then(() => {
     87           active--;
     88           inflight.delete(url);
     89           pump();
     90         });
     91     }
     92 
     93     function pump() {
     94       while (!paused && active < cfg.concurrency && queue.length) run(queue.shift());
     95     }
     96 
     97     return {
     98       // Start a new page view: fresh budget, drop anything still waiting. Finished work is remembered.
     99       newPage() {
    100         queue = [];
    101         queued = new Set();
    102         used = 0;
    103       },
    104       remaining() {
    105         return cfg.maxLinks - used;
    106       },
    107       enqueue(urls) {
    108         let added = 0;
    109         for (const url of urls) {
    110           if (used >= cfg.maxLinks) break;
    111           if (queued.has(url) || inflight.has(url) || fresh(url)) continue;
    112           queue.push(url);
    113           queued.add(url);
    114           used++;
    115           added++;
    116         }
    117         pump();
    118         return added;
    119       },
    120       // Cancel in-flight requests and forget the queue (called when navigating away). A request for
    121       // `keepUrl` (the page being opened) is left running: it is exactly what the click needs, and the
    122       // browser can hand its response to the navigation instead of starting over.
    123       abort(keepUrl) {
    124         for (const [url, ctl] of inflight) if (url !== keepUrl) ctl.abort();
    125         queue = [];
    126         queued = new Set();
    127       },
    128       setPaused(value) {
    129         paused = !!value;
    130         if (!paused) pump();
    131       },
    132       stats() {
    133         return { queued: queue.length, active, remembered: seen.size, used };
    134       },
    135     };
    136   }
    137 
    138   function shouldSkipDevice(win) {
    139     const conn = win.navigator && (win.navigator.connection || win.navigator.mozConnection);
    140     if (conn && (conn.saveData || /2g$/.test(conn.effectiveType || ''))) return true;
    141     if (win.matchMedia && win.matchMedia('(prefers-reduced-data: reduce)').matches) return true;
    142     return false;
    143   }
    144 
    145   function boot(win) {
    146     const doc = win.document;
    147     if (!doc || win.__cdnPrefetch || typeof win.fetch !== 'function') return;
    148     if (shouldSkipDevice(win)) return;
    149 
    150     const conn = win.navigator && win.navigator.connection;
    151     const config = conn && conn.effectiveType === '3g' ? { maxLinks: 6, concurrency: 1 } : {};
    152 
    153     const pf = createPrefetcher({
    154       config,
    155       fetchFn: (url, signal) =>
    156         win
    157           .fetch(url, {
    158             credentials: 'same-origin',
    159             signal,
    160             priority: 'low', // ignored by browsers that don't support it
    161             headers: { Accept: 'text/html', 'X-Purpose': 'prefetch' },
    162           })
    163           .then((res) => res.arrayBuffer()),
    164     });
    165     win.__cdnPrefetch = pf; // handy for debugging: __cdnPrefetch.stats()
    166 
    167     function scan(freshPage) {
    168       if (doc.querySelector('meta[name="cdn-prefetch"][content="off"]')) return;
    169       if (freshPage) pf.newPage();
    170       if (pf.remaining() <= 0) return;
    171 
    172       const base = new URL(win.location.href);
    173       const viewport = win.innerHeight || 800;
    174       const found = [];
    175       const dedupe = new Set();
    176 
    177       doc.querySelectorAll('a[href]').forEach((a) => {
    178         if (a.getClientRects().length === 0) return; // hidden: filtered-out rows, collapsed menus
    179         const url = candidateUrl(
    180           {
    181             href: a.href,
    182             target: a.getAttribute('target'),
    183             download: a.hasAttribute('download'),
    184             noPrefetch: !!a.closest('[data-no-prefetch]'),
    185             turboOff: !!a.closest('[data-turbo="false"]'),
    186           },
    187           base
    188         );
    189         if (!url || dedupe.has(url)) return;
    190         // A folder the instant-navigation tree can build locally needs no server round trip at all.
    191         if (win.__cdnFast && win.__cdnFast.handles(url)) return;
    192         dedupe.add(url);
    193         const r = a.getBoundingClientRect();
    194         // Page content first (on-screen links, plus a screenful below, ahead of the rest), then site
    195         // chrome (navbar, footer). Those pages are cheap for the server, so they only get what's left.
    196         const chrome = !!a.closest('header, nav, footer');
    197         found.push({ url, rank: chrome ? 2 : r.bottom > 0 && r.top < viewport * 1.5 ? 0 : 1 });
    198       });
    199 
    200       found.sort((x, y) => x.rank - y.rank);
    201       pf.enqueue(found.map((f) => f.url));
    202     }
    203 
    204     // Wait for the document to be parsed and the browser to go idle before touching the network.
    205     let pending = false;
    206     let pendingFresh = false;
    207     function request(freshPage) {
    208       pendingFresh = pendingFresh || freshPage;
    209       if (pending) return;
    210       pending = true;
    211       const go = () => {
    212         const run = () => {
    213           pending = false;
    214           const f = pendingFresh;
    215           pendingFresh = false;
    216           scan(f);
    217         };
    218         if (win.requestIdleCallback) win.requestIdleCallback(run, { timeout: 500 });
    219         else win.setTimeout(run, 100);
    220       };
    221       // The HTML is enough to know the links; don't wait for every image and script to finish.
    222       if (doc.readyState !== 'loading') go();
    223       else doc.addEventListener('DOMContentLoaded', go, { once: true });
    224     }
    225 
    226     request(true);
    227     doc.addEventListener('turbo:load', () => request(true));
    228 
    229     // Pages that build their list with JS (search results, history) add links after load.
    230     let timer = 0;
    231     new win.MutationObserver(() => {
    232       win.clearTimeout(timer);
    233       timer = win.setTimeout(() => request(false), 800);
    234     }).observe(doc.documentElement, { childList: true, subtree: true });
    235 
    236     pf.setPaused(doc.hidden);
    237     doc.addEventListener('visibilitychange', () => pf.setPaused(doc.hidden));
    238 
    239     // Navigating: stop prefetching so the real request gets the connection to itself.
    240     doc.addEventListener('turbo:before-visit', (event) => {
    241       let keep = null;
    242       try {
    243         const u = new URL(event.detail.url);
    244         u.hash = '';
    245         keep = u.href;
    246       } catch (e) { /* no usable URL: cancel everything */ }
    247       pf.abort(keep);
    248     });
    249     win.addEventListener('pagehide', () => pf.abort());
    250   }
    251 
    252   if (typeof module === 'object' && module.exports) {
    253     module.exports = { DEFAULTS, pathAllowed, candidateUrl, createPrefetcher, boot };
    254   } else {
    255     boot(global);
    256   }
    257 })(typeof window !== 'undefined' ? window : globalThis);