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 = [];
},
};
}
|