homepage: one refresh at a time, so a slow signed-in pass still renders
Signed in, one refresh pass (session, then the vault index and the vault list, then the extension) takes longer than the 1.5 s poll. Every poll started a new pass, and the pass in flight saw the newer generation and returned before rendering, so on persimmons.app the Master vault stayed an empty box while every request returned 200 (Tea logs, 2026-10-12: session every ~1.5 s, page-index and vaults ~1 s each, almost never a full pass). refresh() now runs one pass at a time: the poll skips while one is in flight, and a refresh asked for during one (an action, focus) runs once more afterwards. The three identical copies (app.js, assets/app.js, backend/web/app.js) change together. New test extension/tests/homepage-poll.test.mjs holds every request until after the next poll: it fails on the old code (nothing renders) and passes now. Extension suite: 26/26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wyb3QQDESFkosKRXzrXqwr
This commit is contained in:
1 parent
1aa3cffaf2
commit
97f4a739bb
4 files changed
+87
-6
No files matched your search
@@ -47,7 +47,21 @@ async function latestVersion() {
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
// One refresh at a time. Signed in, a pass (session, then the vault index and the vault list, then the extension)
|
||||
// takes longer than the 1.5 s poll, and each new pass used to make the one in flight bail out before rendering, so
|
||||
// the vault stayed empty forever. Now a refresh asked for during another runs once more after it (actions, focus),
|
||||
// and the poll simply skips while one is running.
|
||||
let refreshing = null, refreshAgain = false;
|
||||
function refresh() {
|
||||
if (refreshing) { refreshAgain = true; return refreshing; }
|
||||
refreshing = (async () => {
|
||||
try { do { refreshAgain = false; await refreshOnce(); } while (refreshAgain); }
|
||||
finally { refreshing = null; }
|
||||
})();
|
||||
return refreshing;
|
||||
}
|
||||
|
||||
async function refreshOnce() {
|
||||
if (dragging || juiceBusy) return;
|
||||
const current = ++generation;
|
||||
const response = await fetch('/api/session', {credentials: 'same-origin', cache: 'no-store'});
|
||||
@@ -550,5 +564,5 @@ window.addEventListener('pointerdown', event => {
|
||||
window.addEventListener('pointerup', () => { dragging = false; }, true);
|
||||
window.addEventListener('pageshow', () => refresh().catch(failed));
|
||||
window.addEventListener('focus', () => refresh().catch(failed));
|
||||
setInterval(() => { if (!document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
setInterval(() => { if (!refreshing && !document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
refresh().catch(failed);
|
||||
+16
-2
@@ -47,7 +47,21 @@ async function latestVersion() {
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
// One refresh at a time. Signed in, a pass (session, then the vault index and the vault list, then the extension)
|
||||
// takes longer than the 1.5 s poll, and each new pass used to make the one in flight bail out before rendering, so
|
||||
// the vault stayed empty forever. Now a refresh asked for during another runs once more after it (actions, focus),
|
||||
// and the poll simply skips while one is running.
|
||||
let refreshing = null, refreshAgain = false;
|
||||
function refresh() {
|
||||
if (refreshing) { refreshAgain = true; return refreshing; }
|
||||
refreshing = (async () => {
|
||||
try { do { refreshAgain = false; await refreshOnce(); } while (refreshAgain); }
|
||||
finally { refreshing = null; }
|
||||
})();
|
||||
return refreshing;
|
||||
}
|
||||
|
||||
async function refreshOnce() {
|
||||
if (dragging || juiceBusy) return;
|
||||
const current = ++generation;
|
||||
const response = await fetch('/api/session', {credentials: 'same-origin', cache: 'no-store'});
|
||||
@@ -550,5 +564,5 @@ window.addEventListener('pointerdown', event => {
|
||||
window.addEventListener('pointerup', () => { dragging = false; }, true);
|
||||
window.addEventListener('pageshow', () => refresh().catch(failed));
|
||||
window.addEventListener('focus', () => refresh().catch(failed));
|
||||
setInterval(() => { if (!document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
setInterval(() => { if (!refreshing && !document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
refresh().catch(failed);
|
||||
+16
-2
@@ -47,7 +47,21 @@ async function latestVersion() {
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
// One refresh at a time. Signed in, a pass (session, then the vault index and the vault list, then the extension)
|
||||
// takes longer than the 1.5 s poll, and each new pass used to make the one in flight bail out before rendering, so
|
||||
// the vault stayed empty forever. Now a refresh asked for during another runs once more after it (actions, focus),
|
||||
// and the poll simply skips while one is running.
|
||||
let refreshing = null, refreshAgain = false;
|
||||
function refresh() {
|
||||
if (refreshing) { refreshAgain = true; return refreshing; }
|
||||
refreshing = (async () => {
|
||||
try { do { refreshAgain = false; await refreshOnce(); } while (refreshAgain); }
|
||||
finally { refreshing = null; }
|
||||
})();
|
||||
return refreshing;
|
||||
}
|
||||
|
||||
async function refreshOnce() {
|
||||
if (dragging || juiceBusy) return;
|
||||
const current = ++generation;
|
||||
const response = await fetch('/api/session', {credentials: 'same-origin', cache: 'no-store'});
|
||||
@@ -550,5 +564,5 @@ window.addEventListener('pointerdown', event => {
|
||||
window.addEventListener('pointerup', () => { dragging = false; }, true);
|
||||
window.addEventListener('pageshow', () => refresh().catch(failed));
|
||||
window.addEventListener('focus', () => refresh().catch(failed));
|
||||
setInterval(() => { if (!document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
setInterval(() => { if (!refreshing && !document.hidden && !$('siteModal')?.open) refresh().catch(() => {}); }, 1500);
|
||||
refresh().catch(failed);
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
|
||||
// Signed in, one refresh pass outlasts the 1.5 s poll. The page must still render: a poll that fires while a pass
|
||||
// is in flight may not cancel it (that left the Master vault empty forever, 2026-10-12).
|
||||
test('a slow signed-in refresh still renders while the poll keeps firing', async () => {
|
||||
const nodes = new Map();
|
||||
const node = id => {
|
||||
if (!nodes.has(id)) nodes.set(id, {hidden:false, textContent:'', innerHTML:'', open:false,
|
||||
addEventListener(){}, append(){}, appendChild(){}, replaceChildren(){}, style:{}});
|
||||
return nodes.get(id);
|
||||
};
|
||||
globalThis.document = {hidden:false, getElementById:node,
|
||||
createElement:tag => ({tagName:tag.toUpperCase(), hidden:false, textContent:'', innerHTML:'', style:{}, open:false,
|
||||
setAttribute(){}, append(){}, appendChild(){}, replaceChildren(){}, showModal(){}, close(){}, addEventListener(){},
|
||||
querySelector(){ return {value:'', disabled:false}; }}),
|
||||
body:{append(){}}};
|
||||
globalThis.window = {addEventListener(){}};
|
||||
let poll = null;
|
||||
globalThis.setInterval = fn => { poll = fn; return 1; };
|
||||
globalThis.chrome = {runtime:{sendMessage:(id, message, callback) => callback({connected:true, initialized:true, unlocked:true, version:'0.4.4'}), lastError:null}};
|
||||
// Every request waits until the test lets it through, like a slow network.
|
||||
const pending = [];
|
||||
let requests = 0;
|
||||
globalThis.fetch = url => { requests++; return new Promise(resolve => pending.push(() => resolve(new Response(
|
||||
JSON.stringify(url === '/api/session' ? {owner:'fixture', login:'fixture'} : url === '/api/vault-index' ? {items:[]} :
|
||||
url === '/api/vaults' ? {vaults:[]} : {version:'0.4.4'}),
|
||||
{status:200, headers:{'content-type':'application/json'}})))); };
|
||||
const flush = async () => { for (const go of pending.splice(0)) go(); for (let i = 0; i < 20; i++) await new Promise(r => setImmediate(r)); };
|
||||
|
||||
const source = (await readFile(new URL('../../backend/web/app.js', import.meta.url), 'utf8'))
|
||||
.replace('./version.mjs', new URL('../version.mjs', import.meta.url).href);
|
||||
await import('data:text/javascript,' + encodeURIComponent(source));
|
||||
for (let tick = 0; tick < 4; tick++) { poll(); await flush(); } // the poll fires before every response
|
||||
assert.equal(node('userIdentity').textContent, 'Signed in as fixture');
|
||||
assert.equal(node('vaultCount').textContent, '0 saved credentials');
|
||||
assert.ok(requests <= 12, `the poll must not stack passes (${requests} requests)`);
|
||||
});
|
||||
Reference in new issue
Block a user