aboutsummaryrefslogtreecommitdiffstats
path: root/src/thread-prefetch.js
blob: 6d34db68b480d724f175a0bc62e6da35da3fc81e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Dwell is measured on mounted rows, including posts taller than the viewport.
// Sampling also catches fast movement within a large post (IO alone cannot).
export function createThreadPrefetch({ load, key, cached, doc = globalThis.document,
  nav = globalThis.navigator, now = Date.now, schedule = setTimeout, cancel = clearTimeout }) {
  const seeds = new Map(), dwell = new Map();
  let timer = null, busy = false, starts = [], generation = 0;
  const allowed = () => !doc.hidden && nav?.onLine !== false && !nav?.connection?.saveData
    && !['slow-2g', '2g'].includes(nav?.connection?.effectiveType);
  function tick() {
    timer = null;
    const time = now(), visible = new Set(), mounted = new Set(), ready = [];
    const height = doc.documentElement.clientHeight;
    for (const row of doc.querySelectorAll('[data-thread-prefetch]')) {
      const id = row.getAttribute('data-thread-prefetch'), seed = seeds.get(id);
      mounted.add(id);
      if (!seed || !allowed() || cached(seed)) continue;
      const r = row.getBoundingClientRect();
      const overlap = Math.min(r.bottom, height) - Math.max(r.top, 0);
      if (overlap < Math.min(r.height, height) * 0.5 || overlap <= 0) continue;
      visible.add(id);
      let d = dwell.get(id);
      // More than 100px/s is still scrolling past; slow reading can qualify.
      if (!d || Math.abs(r.top - d.top) > 25) d = { since: time, top: r.top };
      d.top = r.top;
      dwell.set(id, d);
      if (time - d.since >= 1200) ready.push(seed);
    }
    for (const id of seeds.keys()) if (!mounted.has(id)) seeds.delete(id);
    for (const id of dwell.keys()) if (!visible.has(id)) dwell.delete(id);
    starts = starts.filter((t) => time - t < 60000);
    if (!busy && starts.length < 6 && ready.length) {
      const seed = ready[0], token = generation;
      busy = true; starts.push(time);
      // The loader inserts synchronously, so clicks share its in-flight work.
      Promise.resolve().then(() => token === generation && load(seed)).catch(() => {}).finally(() => {
        if (token === generation) busy = false;
      });
      for (const [id, ev] of seeds) if (key(ev) === key(seed)) dwell.delete(id);
    }
    if (seeds.size) timer = schedule(tick, 250);
  }
  return {
    watch(seed) {
      if (!doc) return;
      seeds.set(seed.id, seed);
      if (timer === null) timer = schedule(tick, 250);
    },
    stop() {
      generation++; cancel(timer); timer = null; busy = false;
      seeds.clear(); dwell.clear(); starts = [];
    },
  };
}