aboutsummaryrefslogtreecommitdiffstats
path: root/tools/thread-instant-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-16 18:17:21 -0700
committerAdam Soltys <[email protected]>2026-10-16 18:17:21 -0700
commitca3b61ec61e2c2f0141eefd373fade86e9925c85 (patch)
treeb3ab7450e7f4a9b3fbb25cf92921e785aef012ad /tools/thread-instant-test.js
parent46181214dd880934f620424376d8aa29ba8794eb (diff)
downloadcoinosv3-ca3b61ec61e2c2f0141eefd373fade86e9925c85.tar.gz
coinosv3-ca3b61ec61e2c2f0141eefd373fade86e9925c85.zip
A thread never spins: the post reads at once, its replies come in whole — faces and pictures fetched ahead.
Adam, 2026-10-16: "I still see spinner when there's no replies. I'd rather have replies pop in if they have to than wait just to see there are none. … for ones we've preloaded they appear fast but they still take a moment to render and shift the page when they do, can we not pre-render them as well so they're just instantly there on screen?" (1) No spinner at all now: the post is on screen the moment it is tapped, replies land as they arrive, and "no replies" is said only once the relays have spoken. (2) What shifted was each row's face, pictures, previews and quote cards landing after the row: a thread's rows in hand — seeded from the count fetch, fetched ahead on a moment's reading, adopted when the count answers — are now warmed the way the feed warms a row before painting it (warmThread → noteReady: the face thumb, the pictures' sizes, the link cards, the quoted notes), so the tap paints the thread in its final shape; a reply arriving from a relay waits up to 1.5 s for its face before it goes in, so it pops in whole rather than shifting as it fills. tools/thread-instant-test.js: the post fetched ahead opens with its reply and, at its first paint, the replier's face already a thumb; a post with nothing in hand reads at once with no spinner and no "no replies", its first reply popping in at ~480 ms with the slow one still to come, never a spinner; the rest as before. thread-cache, thread-pager, thread-back-scroll, feed-conversations, moderation, reply-count, feed-live and notif-report-fold pass. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
Diffstat (limited to 'tools/thread-instant-test.js')
-rw-r--r--tools/thread-instant-test.js20
1 files changed, 13 insertions, 7 deletions
diff --git a/tools/thread-instant-test.js b/tools/thread-instant-test.js
index 095b6cf..bd80408 100644
--- a/tools/thread-instant-test.js
+++ b/tools/thread-instant-test.js
@@ -24,9 +24,11 @@ const S2 = post('alice on bob, slow relay', 300, SKA, [['e', B1.id, '', 'root'],
const C1 = post('carol posts last, fetched ahead', 200, SKC);
const U1 = post('bob on carol, held back', 100, SKB, [['e', C1.id, '', 'root'], ['p', CAROL]]);
const html = await buildHtml({ minify: true, pwa: false });
+const FACE = Bun.spawnSync(['ffmpeg', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=96x96', '-frames:v', '1', '-f', 'image2', '-c:v', 'png', 'pipe:1']).stdout;
const server = Bun.serve({ port: 0, fetch: (req) => {
const path = new URL(req.url).pathname;
if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path));
+ if (path === '/bob.png') return new Response(FACE, { headers: { 'content-type': 'image/png' } });
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
@@ -82,6 +84,7 @@ const thread = () => page.evaluate(() => ({
rows: [...document.querySelectorAll('.thread-page [data-zap-post]')].map((r) => r.getAttribute('data-zap-post')),
spinner: !!document.querySelector('.thread-card .spinner'),
text: document.querySelector('.thread-card')?.innerText || '',
+ face: [...document.querySelectorAll('.thread-page [data-zap-post]')].slice(-1)[0]?.querySelector('.note-avatar')?.style.backgroundImage || '',
}));
const openRow = (id) => page.evaluate((id) => document.querySelector('.notes-feed .note-post[data-zap-post="' + id + '"] > .note-content > .note-text').click(), id);
@@ -100,7 +103,7 @@ try {
await page.evaluate(([k, alice, bob, carol, notes]) => {
localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', alice], ['p', bob]], c: '', at: Math.floor(Date.now() / 1000) }));
localStorage.setItem(k + ':feedNotes', JSON.stringify(notes));
- localStorage.setItem(k + ':profiles', JSON.stringify({ [alice]: { name: 'Alice Asker', t: Date.now() }, [bob]: { name: 'Bob Quiet', t: Date.now() }, [carol]: { name: 'Carol Quick', t: Date.now() } }));
+ localStorage.setItem(k + ':profiles', JSON.stringify({ [alice]: { name: 'Alice Asker', t: Date.now() }, [bob]: { name: 'Bob Quiet', picture: location.origin + '/bob.png', t: Date.now() }, [carol]: { name: 'Carol Quick', t: Date.now() } }));
localStorage.setItem('btc-wallet-feed:' + k, 'following');
}, [base, ALICE, BOB, CAROL, [C1, B1, A1]]);
await page.reload({ waitUntil: 'domcontentloaded' });
@@ -109,15 +112,16 @@ try {
check('the feed paints the posts', await waitFor(() => document.querySelectorAll('.notes-feed .note-post[data-zap-post]').length >= 3, null, 20000));
console.log('\n[fetched ahead before the count answered]');
check('reading carol\'s post for a moment fetches its thread ahead', await waitFor((id) => window.__reqs.some((r) => r.fs.some((f) => (f['#e'] || []).includes(id) && f.limit)), C1.id, 15000));
+ await page.evaluate(() => { window.__conn.saveData = true; }); // no more fetching ahead: the later parts want their threads untouched
await sleep(300);
await page.evaluate(() => window.__releaseC()); // now the count answer lands, after the thread entry was made
check('…and the count answer brings its reply', await waitFor((id) => /\b1\b/.test(document.querySelector('.notes-feed .note-post[data-zap-post="' + id + '"]')?.innerText || ''), C1.id, 8000));
- await page.evaluate(() => { window.__conn.saveData = true; }); // no more fetching ahead: the next parts want threads untouched
const t2 = Date.now();
await openRow(C1.id);
let ahead = null;
for (let i = 0; i < 40; i++) { const th = await thread(); if (th.rows.includes(U1.id)) { ahead = { ...th, ms: Date.now() - t2 }; break; } await sleep(25); }
check('the thread opens with the reply from the count, not after its own 6 s ask', !!ahead && ahead.ms < 1500 && !ahead.spinner, ahead ? ahead.ms + ' ms' : 'never');
+ check('…painted in its final shape: bob\'s face was fetched ahead, on the row at its first paint', !!ahead && /blob:|data:image/.test(ahead.face || ''), ahead ? String(ahead.face).slice(0, 60) : '');
await page.goBack(); await sleep(800);
await waitFor(() => !!document.querySelector('.feed-chips'), null, 6000);
@@ -140,11 +144,13 @@ try {
console.log('\n[a post with nothing in hand]');
const t1 = Date.now();
await openRow(B1.id);
- check('a spinner while nothing is there to read', await waitFor(() => !!document.querySelector('.thread-card .spinner'), null, 1500, 20));
- let streamed = null;
- for (let i = 0; i < 80; i++) { const th = await thread(); if (th.rows.includes(S1.id)) { streamed = { ...th, ms: Date.now() - t1 }; break; } await sleep(25); }
- check('the first reply shows as it arrives, the slow one still on its way', !!streamed && streamed.ms < 2000 && !streamed.rows.includes(S2.id), streamed ? streamed.ms + ' ms' : 'never');
- check('…the spinner gone once there is something to read', !!streamed && !streamed.spinner);
+ let opened = null;
+ for (let i = 0; i < 40; i++) { const th = await thread(); if (th.rows[0] === B1.id) { opened = { ...th, ms: Date.now() - t1 }; break; } await sleep(20); }
+ check('the post reads at once: no spinner, no "no replies" while they are on their way', !!opened && !opened.spinner && !/no replies/i.test(opened.text), opened ? opened.ms + ' ms' : 'never');
+ let streamed = null, spun = false;
+ for (let i = 0; i < 80; i++) { const th = await thread(); spun ||= th.spinner; if (th.rows.includes(S1.id)) { streamed = { ...th, ms: Date.now() - t1 }; break; } await sleep(25); }
+ check('the first reply pops in as it arrives, the slow one still on its way', !!streamed && streamed.ms < 2500 && !streamed.rows.includes(S2.id), streamed ? streamed.ms + ' ms' : 'never');
+ check('…never a spinner', !spun && !!streamed && !streamed.spinner);
check('the slow one lands in its place', await waitFor((id) => { const rows = [...document.querySelectorAll('.thread-page [data-zap-post]')].map((r) => r.getAttribute('data-zap-post')); return rows.length === 3 && rows[2] === id && !document.querySelector('.thread-card .spinner'); }, S2.id, 6000));
check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(true); }