// Exercise the production refill scheduler with delayed and sparse sources. // Run: bun tools/pager-loading-test.js import { readFileSync } from 'node:fs'; import vm from 'node:vm'; import assert from 'node:assert/strict'; const source = readFileSync(new URL('../src/features/messages.js', import.meta.url), 'utf8'); const scheduler = source.slice(source.indexOf(' const PAGER_INITIAL ='), source.indexOf(' // Slides for feed items', source.indexOf(' const PAGER_INITIAL ='))); function fixture(kind = 'images', grid = false) { let clock = 100000, calls = 0, pending = null, count = 3; const groups = Array.from({ length: 300 }, () => ({})); const cache = { shown: 3, notes: Array(3), stopped: false }; const index = { until: 10000, images: new Map(), videos: new Map() }; const p = { kind, cur: { closest: () => groups[0] } }; const more = async () => { calls++; if (pending) await pending; count += 10; cache.shown += 10; cache.notes.length += 10; index.until -= 100; }; const src = { c: cache, more, done: () => false }; const ctx = vm.createContext({ pager: p, pagerGroups: () => groups.slice(0, count), pagerSource: () => grid ? null : src, feedNow: () => cache, mediaIndex: () => index, feedDef: () => ({}), crawlMedia: (c, k) => { assert.equal(c, cache); assert.equal(k, kind); return more(); }, growPager: () => {}, Date: { now: () => clock }, }); vm.runInContext(scheduler, ctx); return { ctx, p, src, cache, index, load: () => ctx.pagerLoadAhead(), go: (i) => { p.cur = { closest: () => groups[i] }; }, advance: () => { clock += 11000; }, delay: () => { let resolve; pending = new Promise((r) => { resolve = r; }); return () => { pending = null; resolve(); }; }, count: () => count, calls: () => calls, }; } for (const grid of [false, true]) for (const kind of ['images', 'videos']) { const f = fixture(kind, grid); await f.load(); assert.equal(f.calls(), 3, 'opening starts loading immediately to at least 30 posts'); f.advance(); await f.load(); assert.equal(f.calls(), 3, 'a full buffer does not download forever'); f.go(29); f.advance(); const finish = f.delay(); const loading = f.load(); assert.equal(f.calls(), 4, 'refill starts well before the end'); f.go(0); await f.load(); assert.equal(f.calls(), 4, 'wrapping during a request does not start a concurrent one'); finish(); await loading; assert.equal(f.count(), 53, 'refill continues after wrapping, keeping 20 beyond the furthest post'); console.log('✓ early loading and wrap refill:', grid ? 'grid' : 'feed/profile', kind); } { const f = fixture(); f.src.more = async () => { f.cache.notes.length++; }; await f.load(); assert.equal(f.cache.notes.length, 11, 'sparse results are limited to eight requests per run'); f.advance(); await f.load(); assert.equal(f.cache.notes.length, 19, 'timer can resume sparse results from the first post'); } { const f = fixture(); let calls = 0; f.src.more = async () => { calls++; }; await f.load(); await f.load(); assert.equal(calls, 1, 'an empty result backs off'); f.advance(); await f.load(); assert.equal(calls, 2, 'an empty source can retry'); f.src.done = () => true; f.advance(); await f.load(); assert.equal(calls, 2, 'an exhausted source is not requested'); } { const f = fixture(); const finish = f.delay(), loading = f.load(); f.ctx.pager = { kind: 'videos' }; finish(); await loading; assert.equal(f.calls(), 1, 'closing or switching pagers stops the old refill'); } console.log('✓ sparse sources, backoff, exhaustion, and pager lifecycle');