mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
fix(control-pane): order data and outcomes of every load separately
Two cases the finished-load rule got wrong. When the first snapshot was still loading and a live refresh failed, the first snapshot arrived late and was dropped, leaving the pane empty. And manual loads, the Refresh button and a query, reported their failures outside the ordering, so a late manual failure could cover newer data and an older success could clear a newer manual failure. The board now shows the newest data any load brought, and the error box the outcome of the newest load that has finished. load() reports its own fetch failures, as a live-refresh failure when the interval started it and as a plain error otherwise, so every load is ordered the same way. Data that arrives after a newer failure still shows, and the failure stays, dated by that data.
This commit is contained in:
@@ -439,17 +439,24 @@ function renderControlPaneHtml() {
|
||||
// The board keeps the last snapshot on screen, so a failed refresh has to
|
||||
// say that the data is no longer live, and since when.
|
||||
let loadedAt = null;
|
||||
// Loads are numbered as they start. The board follows the newest load that
|
||||
// has finished, with its data or with its failure, so a load that finishes
|
||||
// after a newer one leaves the board alone.
|
||||
// Loads are numbered as they start and finish in any order. The board shows
|
||||
// the newest data any load brought, and the error box the outcome of the
|
||||
// newest load that has finished, so a load that finishes late can neither
|
||||
// replace newer data nor overrule a newer outcome.
|
||||
let loadsStarted = 0;
|
||||
let newestFinished = 0;
|
||||
let shownLoad = 0;
|
||||
let failure = null;
|
||||
function showRefreshFailure(error) {
|
||||
const since = loadedAt
|
||||
? ' The data below is from ' + loadedAt.toLocaleString() + '.'
|
||||
: '';
|
||||
showError('#app', 'Live refresh failed.' + since + '\\n' + formatError(error));
|
||||
}
|
||||
function showFailure() {
|
||||
if (failure.live) showRefreshFailure(failure.error);
|
||||
else showError('#app', failure.error);
|
||||
}
|
||||
|
||||
async function readJsonResponse(response) {
|
||||
let payload;
|
||||
@@ -639,14 +646,37 @@ function renderControlPaneHtml() {
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
async function load(live = false) {
|
||||
const id = ++loadsStarted;
|
||||
const url = new URL('/api/snapshot', window.location.href);
|
||||
if (state.query) url.searchParams.set('query', state.query);
|
||||
const response = await fetch(url);
|
||||
const snapshot = await readJsonResponse(response);
|
||||
if (id < newestFinished) return;
|
||||
let snapshot;
|
||||
try {
|
||||
const url = new URL('/api/snapshot', window.location.href);
|
||||
if (state.query) url.searchParams.set('query', state.query);
|
||||
const response = await fetch(url);
|
||||
snapshot = await readJsonResponse(response);
|
||||
} catch (error) {
|
||||
if (id < newestFinished) return;
|
||||
newestFinished = id;
|
||||
failure = { error, live };
|
||||
showFailure();
|
||||
return;
|
||||
}
|
||||
if (id > shownLoad) {
|
||||
shownLoad = id;
|
||||
render(snapshot);
|
||||
}
|
||||
if (id < newestFinished) {
|
||||
// A newer load failed first. This data is still the newest on the
|
||||
// board, so the failure stays, dated by it.
|
||||
if (failure && id === shownLoad) showFailure();
|
||||
return;
|
||||
}
|
||||
newestFinished = id;
|
||||
failure = null;
|
||||
clearError('#app');
|
||||
}
|
||||
|
||||
function render(snapshot) {
|
||||
$('#query').value = snapshot.knowledge.query || state.query;
|
||||
$('#db-path').textContent = snapshot.database.exists ? snapshot.dbPath : 'database missing';
|
||||
state.allowActions = Boolean(snapshot.execution.allowActions);
|
||||
@@ -661,7 +691,6 @@ function renderControlPaneHtml() {
|
||||
executable: snapshot.execution.allowActions && action.executable
|
||||
})));
|
||||
loadedAt = new Date();
|
||||
clearError('#app');
|
||||
}
|
||||
|
||||
$('#query-form').addEventListener('submit', event => {
|
||||
@@ -701,14 +730,7 @@ function renderControlPaneHtml() {
|
||||
// Live board: refresh on a gentle interval; pause while a prompt/tab is hidden.
|
||||
setInterval(() => {
|
||||
if (document.hidden) return;
|
||||
const refresh = load();
|
||||
// load() numbers itself before its first await.
|
||||
const id = loadsStarted;
|
||||
refresh.catch(error => {
|
||||
if (id < newestFinished) return;
|
||||
newestFinished = id;
|
||||
showRefreshFailure(error);
|
||||
});
|
||||
load(true).catch(error => showError('#app', error));
|
||||
}, 15000);
|
||||
|
||||
load().catch(error => showError('#app', error));
|
||||
|
||||
@@ -41,7 +41,9 @@ class PageDate extends Date {
|
||||
// elements looked up by selector, fetch, a fixed clock, and a setInterval
|
||||
// whose callback the test fires itself. While `hold` is set, a fetch waits in
|
||||
// `pending` until the test settles it, with the page's snapshot or another one.
|
||||
function openPage(snapshot) {
|
||||
// With `hold`, the first load is held too. Listeners are kept per element, so a
|
||||
// test can press a button.
|
||||
function openPage(snapshot, { hold = false } = {}) {
|
||||
const elements = new Map();
|
||||
const element = selector => {
|
||||
if (!elements.has(selector)) {
|
||||
@@ -51,12 +53,15 @@ function openPage(snapshot) {
|
||||
innerHTML: '',
|
||||
value: '',
|
||||
dataset: {},
|
||||
addEventListener() {}
|
||||
listeners: {},
|
||||
addEventListener(type, listener) {
|
||||
this.listeners[type] = listener;
|
||||
}
|
||||
});
|
||||
}
|
||||
return elements.get(selector);
|
||||
};
|
||||
const page = { online: true, hold: false, pending: [], refresh: null, element };
|
||||
const page = { online: true, hold, pending: [], refresh: null, element };
|
||||
vm.runInNewContext(inlineScript(renderControlPaneHtml()), {
|
||||
document: { hidden: false, querySelector: element, querySelectorAll: () => [] },
|
||||
window: { location: { href: 'http://127.0.0.1:8765/' } },
|
||||
@@ -219,6 +224,63 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await test('the first snapshot still shows when a live refresh fails before it arrives', async () => {
|
||||
const page = openPage(snapshot, { hold: true });
|
||||
const answer = query => ({ ...snapshot, knowledge: { ...snapshot.knowledge, query } });
|
||||
|
||||
page.refresh();
|
||||
page.pending[1].fail();
|
||||
await settle();
|
||||
page.pending[0].succeed(answer('first'));
|
||||
await settle();
|
||||
assert.strictEqual(page.element('#query').value, 'first', 'the pane is not left empty');
|
||||
const box = page.element('#app');
|
||||
assert.strictEqual(box.hidden, false, 'the newer failure stays up');
|
||||
assert.match(box.textContent, /Live refresh failed\. The data below is from /);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await test('a manual refresh that fails after a newer load succeeded is not shown', async () => {
|
||||
const page = openPage(snapshot);
|
||||
await settle();
|
||||
|
||||
page.hold = true;
|
||||
page.element('#refresh').listeners.click();
|
||||
page.refresh();
|
||||
page.pending[1].succeed();
|
||||
await settle();
|
||||
page.pending[0].fail();
|
||||
await settle();
|
||||
assert.strictEqual(page.element('#app').hidden, true, 'the newer success decides the board');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await test('an older load that succeeds after a newer manual refresh failed leaves the failure up', async () => {
|
||||
const page = openPage(snapshot);
|
||||
await settle();
|
||||
|
||||
page.hold = true;
|
||||
page.refresh();
|
||||
page.element('#refresh').listeners.click();
|
||||
page.pending[1].fail();
|
||||
await settle();
|
||||
page.pending[0].succeed();
|
||||
await settle();
|
||||
const box = page.element('#app');
|
||||
assert.strictEqual(box.hidden, false, 'the newer failure decides the board');
|
||||
assert.match(box.textContent, /Failed to fetch/);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user