mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
fix(news): keep the feed cursor in step with the list and drop Local reads for an area left
Co-Authored-By: Claude Fable 5.2 <[email protected]>
This commit is contained in:
1 parent
7cd2af46e4
commit
fd9373c09c
4 files changed
+142
-6
No files matched your search
@@ -190,6 +190,7 @@ export function createNewsLayer({
|
|||||||
request = null,
|
request = null,
|
||||||
feedRequest = null,
|
feedRequest = null,
|
||||||
inViewRequest = null,
|
inViewRequest = null,
|
||||||
|
inViewRequestKey = null,
|
||||||
areaRequest = null,
|
areaRequest = null,
|
||||||
settleTimer = null,
|
settleTimer = null,
|
||||||
pulseTimer = null,
|
pulseTimer = null,
|
||||||
@@ -958,7 +959,9 @@ export function createNewsLayer({
|
|||||||
// clears it, not only the next refresh.
|
// clears it, not only the next refresh.
|
||||||
needs = { ...needs, conflicts: null };
|
needs = { ...needs, conflicts: null };
|
||||||
feed = {
|
feed = {
|
||||||
...mergeFeedPage(feed, page, mode, now(), undefined, windowMs),
|
// Kept for the server's whole window (the shown one filters the
|
||||||
|
// view), so the rows between the head and the cursor are never lost.
|
||||||
|
...mergeFeedPage(feed, page, mode, now(), undefined, maxWindowMs),
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -1039,18 +1042,24 @@ export function createNewsLayer({
|
|||||||
// The section changed, or the layer went off, while the area was read.
|
// The section changed, or the layer went off, while the area was read.
|
||||||
if (!local() || !enabled) return;
|
if (!local() || !enabled) return;
|
||||||
const readKey = inView.area?.key ?? '';
|
const readKey = inView.area?.key ?? '';
|
||||||
if (
|
const held = inView.readKey === readKey && inView.minor;
|
||||||
!force &&
|
if (!force && (inViewRequest ? inViewRequestKey === readKey : held)) {
|
||||||
inView.readKey === readKey &&
|
|
||||||
(inView.minor || inViewRequest)
|
|
||||||
) {
|
|
||||||
render();
|
render();
|
||||||
notify();
|
notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A read still out for another area must not land over this one.
|
||||||
inViewRequest?.abort();
|
inViewRequest?.abort();
|
||||||
|
if (!force && held) {
|
||||||
|
inViewRequest = null;
|
||||||
|
inView = { ...inView, loading: false };
|
||||||
|
render();
|
||||||
|
notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
inViewRequest = controller;
|
inViewRequest = controller;
|
||||||
|
inViewRequestKey = readKey;
|
||||||
inView = { ...inView, loading: true };
|
inView = { ...inView, loading: true };
|
||||||
notify();
|
notify();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -852,3 +852,107 @@ test('a failed read keeps the layer on with an error; disable clears the overlay
|
|||||||
assert.deepEqual(entries.at(-1), []);
|
assert.deepEqual(entries.at(-1), []);
|
||||||
assert.deepEqual(layer.getRowControls().feed.items, []);
|
assert.deepEqual(layer.getRowControls().feed.items, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('narrowing the window and widening it again loses no report already read', async (t) => {
|
||||||
|
const { layer } = fixture(t, {
|
||||||
|
source: {
|
||||||
|
// Two pages: the head page, and an older one behind its cursor.
|
||||||
|
getFeed: async ({ before } = {}) =>
|
||||||
|
before
|
||||||
|
? {
|
||||||
|
reports: [
|
||||||
|
report('earlier', { publishedAt: '2026-10-28T05:00:00.000Z' }),
|
||||||
|
],
|
||||||
|
nextBefore: null,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
reports: [
|
||||||
|
report('recent', { publishedAt: '2026-10-28T09:30:00.000Z' }),
|
||||||
|
],
|
||||||
|
nextBefore: '2026-10-28T09:30:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const shown = () => layer.getRowControls().feed.items.map((item) => item.id);
|
||||||
|
layer.enable();
|
||||||
|
await layer.update();
|
||||||
|
for (let i = 0; i < 3; i++) await settle();
|
||||||
|
assert.deepEqual(shown(), ['recent', 'earlier']);
|
||||||
|
layer.setParams({ filters: { hours: 1 } });
|
||||||
|
for (let i = 0; i < 3; i++) await settle();
|
||||||
|
assert.deepEqual(shown(), ['recent']);
|
||||||
|
layer.setParams({ filters: { hours: 24 } });
|
||||||
|
for (let i = 0; i < 3; i++) await settle();
|
||||||
|
assert.deepEqual(shown(), ['recent', 'earlier']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a slower Local read for the area just left never replaces the current area's reports", async (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
|
let centre = { lat: 49, lon: 36 };
|
||||||
|
const pending = [];
|
||||||
|
const russia = {
|
||||||
|
area: { key: 'RU', name: 'Russia', kind: 'country', country: 'RU' },
|
||||||
|
polygons: [
|
||||||
|
{
|
||||||
|
outer: [
|
||||||
|
[38, 48],
|
||||||
|
[40, 48],
|
||||||
|
[40, 50.5],
|
||||||
|
[38, 50.5],
|
||||||
|
],
|
||||||
|
holes: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
borders: [],
|
||||||
|
disputed: [],
|
||||||
|
};
|
||||||
|
const { layer, camera } = fixture(t, {
|
||||||
|
height: 1_000_000,
|
||||||
|
centre: () => centre,
|
||||||
|
source: {
|
||||||
|
getAreaOutline: async (point) => (point.lon < 37.5 ? ukraine : russia),
|
||||||
|
getFeed: (query) => {
|
||||||
|
if (!(query.relevance === '' && query.limit === 500))
|
||||||
|
return Promise.resolve({ reports: [], nextBefore: null });
|
||||||
|
if (query.at.lon < 37.5)
|
||||||
|
return Promise.resolve({
|
||||||
|
reports: [report('kharkiv', { severity: 1 })],
|
||||||
|
nextBefore: null,
|
||||||
|
});
|
||||||
|
// Russia's read is slow.
|
||||||
|
return new Promise((resolve) => pending.push(resolve));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const look = async (at) => {
|
||||||
|
centre = at;
|
||||||
|
camera.moveEnd.raiseEvent();
|
||||||
|
t.mock.timers.tick(800);
|
||||||
|
await flush();
|
||||||
|
};
|
||||||
|
const shown = () =>
|
||||||
|
layer.getRowControls().feed.inView.items.map((item) => item.id);
|
||||||
|
layer.enable();
|
||||||
|
await layer.update();
|
||||||
|
await flush();
|
||||||
|
layer.setParams({ filters: { section: 'local' } });
|
||||||
|
await flush();
|
||||||
|
assert.deepEqual(shown(), ['kharkiv']);
|
||||||
|
await look({ lat: 49, lon: 39 });
|
||||||
|
assert.equal(pending.length, 1);
|
||||||
|
await look({ lat: 49, lon: 36 });
|
||||||
|
pending[0]({
|
||||||
|
reports: [
|
||||||
|
report('belgorod', {
|
||||||
|
severity: 1,
|
||||||
|
pins: [{ lat: 49, lon: 39, place: 'Rossosh', mappable: true }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
nextBefore: null,
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
const view = layer.getRowControls().feed;
|
||||||
|
assert.equal(view.inView.area, 'Ukraine');
|
||||||
|
assert.equal(view.inView.loading, false);
|
||||||
|
assert.deepEqual(shown(), ['kharkiv']);
|
||||||
|
});
|
||||||
@@ -617,6 +617,8 @@ export function reportMarkerPosition(report, pin = 0) {
|
|||||||
* only the window and at most `max` rows (a long session does not grow
|
* only the window and at most `max` rows (a long session does not grow
|
||||||
* the list). `mode` 'head' refreshes the top and keeps the older cursor;
|
* the list). `mode` 'head' refreshes the top and keeps the older cursor;
|
||||||
* 'append' adds an older page and takes its cursor; 'replace' starts over.
|
* 'append' adds an older page and takes its cursor; 'replace' starts over.
|
||||||
|
* A head page that has older pages behind it and shares no row with the list
|
||||||
|
* left a gap (more than a page arrived since), so it starts over too.
|
||||||
*/
|
*/
|
||||||
export function mergeFeedPage(
|
export function mergeFeedPage(
|
||||||
feed,
|
feed,
|
||||||
@@ -627,6 +629,10 @@ export function mergeFeedPage(
|
|||||||
windowMs = NEWS_WINDOW_MS,
|
windowMs = NEWS_WINDOW_MS,
|
||||||
) {
|
) {
|
||||||
const byId = new Map();
|
const byId = new Map();
|
||||||
|
if (mode === 'head' && page.nextBefore && feed.items.length) {
|
||||||
|
const held = new Set(feed.items.map((item) => item.id));
|
||||||
|
if (!page.reports.some((item) => held.has(item.id))) mode = 'replace';
|
||||||
|
}
|
||||||
const items =
|
const items =
|
||||||
mode === 'replace'
|
mode === 'replace'
|
||||||
? page.reports
|
? page.reports
|
||||||
|
|||||||
@@ -318,6 +318,23 @@ test('feed pages merge newest first without duplicates', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a head page that does not reach the rows held starts the list over', () => {
|
||||||
|
const held = report('h', '2026-10-28T06:00:00Z');
|
||||||
|
const fresh = report('n', '2026-10-28T09:30:00Z');
|
||||||
|
// More than a page arrived since the last refresh (a suspended tab): the
|
||||||
|
// reports between this page and the rows held are behind its cursor.
|
||||||
|
const feed = mergeFeedPage(
|
||||||
|
{ items: [held], nextBefore: 'OLD' },
|
||||||
|
{ reports: [fresh], nextBefore: 'NEW' },
|
||||||
|
'head',
|
||||||
|
NOW,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
[feed.items.map((r) => r.id), feed.nextBefore],
|
||||||
|
[['n'], 'NEW'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('the feed list keeps only the last 24 hours and at most its cap', () => {
|
test('the feed list keeps only the last 24 hours and at most its cap', () => {
|
||||||
const fresh = report('f', '2026-10-28T09:00:00Z');
|
const fresh = report('f', '2026-10-28T09:00:00Z');
|
||||||
const stale = report('s', '2026-10-27T09:00:00Z');
|
const stale = report('s', '2026-10-27T09:00:00Z');
|
||||||
|
|||||||
Reference in new issue
Block a user