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