fix: recover live refresh from stalled requests (#303)
CI / lint (pull_request) Successful in 31s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-08 13:15:20 +00:00
1 parent 7356530318
commit 40c66df143
5 files changed
+193 -14

No files matched your search

+110
View File
@@ -114,3 +114,113 @@ const poller = createContextPoller({{
{"context": "timmy", "event": 2, "age": 2, "changed": []},
],
}
def test_context_poller_aborts_a_stalled_request_and_recovers_on_schedule():
script = f"""
const createContextPoller = require({json.dumps(str(POLLER))});
const timers = [];
const deadlineTimers = [];
const errors = [];
const snapshots = [];
const signals = [];
let calls = 0;
const poller = createContextPoller({{
fetchContext: (revisions, options) => {{
calls += 1;
signals.push(options.signal);
if (calls === 1) return new Promise(() => {{}});
return Promise.resolve({{ context: {{ user: {{ login: 'timmy' }} }} }});
}},
onSnapshot: snapshot => snapshots.push(snapshot.context.user.login),
onError: error => errors.push({{ name: error.name, message: error.message }}),
setTimer: (callback, delay) => {{
const timer = {{ callback, delay, cancelled: false }};
timers.push(timer);
return timer;
}},
clearTimer: timer => {{ timer.cancelled = true; }},
setDeadlineTimer: (callback, delay) => {{
const timer = {{ callback, delay, cancelled: false }};
deadlineTimers.push(timer);
return timer;
}},
clearDeadlineTimer: timer => {{ timer.cancelled = true; }},
intervalMs: 8000,
timeoutMs: 25,
}});
(async () => {{
const first = poller.start();
const deadline = deadlineTimers.find(timer => timer.delay === 25 && !timer.cancelled);
deadline.callback();
await first;
const retry = timers.find(timer => timer.delay === 8000 && !timer.cancelled);
retry.callback();
await new Promise(resolve => setImmediate(resolve));
process.stdout.write(JSON.stringify({{
calls,
firstAborted: signals[0].aborted,
errors,
snapshots,
}}));
}})();
"""
assert run_node(script) == {
"calls": 2,
"firstAborted": True,
"errors": [
{"name": "TimeoutError", "message": "Live update timed out after 25ms."}
],
"snapshots": ["timmy"],
}
def test_forced_refresh_retires_stale_generation_without_late_overwrite():
script = f"""
const createContextPoller = require({json.dumps(str(POLLER))});
let resolveFirst;
let firstSignal;
const rendered = [];
const errors = [];
let calls = 0;
const poller = createContextPoller({{
fetchContext: (revisions, options) => {{
calls += 1;
if (calls === 1) {{
firstSignal = options.signal;
return new Promise(resolve => {{ resolveFirst = resolve; }});
}}
return Promise.resolve({{ context: {{ user: {{ login: 'fresh' }} }} }});
}},
onSnapshot: snapshot => rendered.push(snapshot.context.user.login),
onError: error => errors.push(error.name),
setTimer: () => 1,
clearTimer: () => {{}},
setDeadlineTimer: () => 1,
clearDeadlineTimer: () => {{}},
}});
(async () => {{
const stale = poller.start();
const fresh = poller.refresh({{ force: true }});
await fresh;
resolveFirst({{ context: {{ user: {{ login: 'stale' }} }} }});
await stale;
await new Promise(resolve => setImmediate(resolve));
process.stdout.write(JSON.stringify({{
calls,
firstAborted: firstSignal.aborted,
rendered,
errors,
}}));
}})();
"""
assert run_node(script) == {
"calls": 2,
"firstAborted": True,
"rendered": ["fresh"],
"errors": [],
}
+18
View File
@@ -57,3 +57,21 @@ async def test_dashboard_reports_each_live_section_from_its_own_freshness():
assert "const notificationsFresh = Array.isArray(snapshot.notifications)" not in html
assert "Unread updates unavailable · showing last known updates" in html
assert "Activity refresh failed · showing last activity" in html
@pytest.mark.anyio
async def test_operator_refresh_and_reconnect_supersede_a_stalled_live_request():
html = await dashboard()
assert "async function fetchLiveSnapshot(revisions = {}, { signal } = {})" in html
assert "headers: { Accept: 'application/json' },\n signal," in html
assert "function load() { return contextPoller.refresh({ force: true }); }" in html
assert "contextPoller.refresh({ force: true });" in html
@pytest.mark.anyio
async def test_stalled_refresh_keeps_the_last_snapshot_with_specific_guidance():
html = await dashboard()
assert "e.name === 'TimeoutError'" in html
assert "Update delayed · showing last snapshot" in html
+1 -1
View File
@@ -103,7 +103,7 @@ async def test_dashboard_announces_offline_mode_and_refreshes_after_reconnect():
bootstrap = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js").read_text()
assert "window.addEventListener('offline'" in bootstrap
assert "window.addEventListener('online'" in bootstrap
assert "contextPoller.refresh()" in bootstrap
assert "contextPoller.refresh({ force: true })" in bootstrap
@pytest.mark.anyio