fix(news): keep a refused key refused during a slow read, and read Local when Conflicts fails

A slow re-read of a refused key read as unavailable until it landed; it
now stays refused. The first-read wait is 10 s, for a margin under the
browser's 15 s. A failed Conflicts read no longer stops Local's refresh,
which the server reads on its own. Tests cover GDELT's wait, the refused
key and a caller abort past the timeout, and fail rather than hang; the
docs say what a section serves meanwhile.
This commit is contained in:
Denis Groshev committed 2026-11-06 12:02:58 +01:00
1 parent c6ae4c44d7
commit 61424e4256
7 files changed
+176 -42

No files matched your search

+2
View File
@@ -1428,6 +1428,8 @@ export function createNewsLayer({
}
unavailable = cause?.unavailable === true;
error = cause?.message || FEED_UNAVAILABLE;
// Local is read apart: Conflicts failing leaves it reading.
void refreshInView({ force: inView.active });
return true;
} finally {
signal?.removeEventListener('abort', abort);
+41
View File
@@ -799,6 +799,47 @@ test('without Liveuamap reports or a Local area, nothing is credited', async (t)
assert.deepEqual(credits, []);
});
test('a failed Conflicts read leaves Local reading with each refresh', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const failure = () => new Error('News feed unavailable');
let local = failure();
const { layer } = fixture(t, {
height: 1_000_000,
source: {
getHotspots: async () => {
throw failure();
},
getFeed: async (query) => {
if (!query.at) throw failure();
if (local) throw local;
return {
reports: [report('inside', { relevance: 'minor' })],
nextBefore: null,
};
},
getAreaOutline: async () => ukraine,
},
});
layer.enable();
await layer.update();
layer.setParams({ filters: { section: 'local' } });
await flush();
assert.equal(
layer.getRowControls().feed.inView.error,
'News feed unavailable',
);
// The area's read lands meanwhile; Conflicts still fails.
local = null;
await layer.update();
await flush();
const view = layer.getRowControls().feed;
assert.deepEqual(
view.inView.items.map((item) => item.id),
['inside'],
);
assert.equal(view.error, 'News feed unavailable');
});
test('a failed read keeps the layer on with an error; disable clears the overlay', async (t) => {
const { layer, entries } = fixture(t, {
source: { getFeed: async () => Promise.reject(new Error('boom')) },
+13 -1
View File
@@ -176,12 +176,24 @@ test('reads time out and follow the caller signal', async () => {
);
});
const slow = createNewsSource({ fetchImpl: hang, timeoutMs: 20 });
// Out of time it reads unavailable, in the app's words, and is retried.
// Out of time it reads unavailable, in the app's words, as retryable.
await assert.rejects(slow.getHotspots(), {
message: 'News feed unavailable',
code: 'timeout',
retryable: true,
});
// The caller's abort stays its own, even once the time is up too.
const late = (_url, { signal }) =>
new Promise((_, reject) => {
setTimeout(() => reject(signal.reason), 40);
});
const leaving = new AbortController();
const gone = createNewsSource({ fetchImpl: late, timeoutMs: 20 }).getFeed(
{},
{ signal: leaving.signal },
);
leaving.abort(new Error('gone'));
await assert.rejects(gone, /gone/);
const caller = new AbortController();
const pending = createNewsSource({ fetchImpl: hang }).getFeed(
{},