fix(cyclones): distinguish deliberate clears from feed eviction

This commit is contained in:
Said Turksever committed 2026-10-20 14:17:39 +01:00
1 parent 1330605c18
commit b358b5dba1
4 files changed
+74 -11

No files matched your search

+10 -8
View File
@@ -89,7 +89,9 @@ export function createCyclonesLayer({
const notify = () => listener?.();
const selected = () =>
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
function clearSelectedContext() {
function clearSelectedContext({ evicted = false } = {}) {
if (!evicted)
context?.clearSelectedEntityContextForLayer?.('weather-cyclones');
context?.removeEntityContextsForLayer?.('weather-cyclones');
selectedContextStormId = null;
selectedContextEntity = null;
@@ -98,14 +100,11 @@ export function createCyclonesLayer({
function publishSelectedContext({ announce = false } = {}) {
const storm = selected();
if (!storm || !context) {
if (!storm) clearSelectedContext();
if (!storm) clearSelectedContext({ evicted: true });
return null;
}
selectedContextEntity ||= { show: true };
const contextId = `cyclone:${storm.id}`;
context.removeEntityContextsForLayer?.('weather-cyclones', {
retainIds: new Set([contextId]),
});
const record = context.registerEntityContext?.(selectedContextEntity, {
id: contextId,
layerId: 'weather-cyclones',
@@ -131,6 +130,9 @@ export function createCyclonesLayer({
if (!record) return null;
selectedContextStormId = storm.id;
if (announce) context.selectEntityContext?.(selectedContextEntity);
context.removeEntityContextsForLayer?.('weather-cyclones', {
retainIds: new Set([contextId]),
});
return record;
}
@@ -302,7 +304,7 @@ export function createCyclonesLayer({
snapshot = next;
selectedId = null;
++navigationGeneration;
clearSelectedContext();
clearSelectedContext({ evicted: true });
error = next.reason || ADVISORIES_UNAVAILABLE;
return true;
}
@@ -327,7 +329,7 @@ export function createCyclonesLayer({
else rendering.setSelection(selectedId);
if (selectedContextStormId) {
if (selectedId === selectedContextStormId) publishSelectedContext();
else clearSelectedContext();
else clearSelectedContext({ evicted: true });
}
return true;
} catch (cause) {
@@ -338,7 +340,7 @@ export function createCyclonesLayer({
snapshot = null;
selectedId = null;
++navigationGeneration;
clearSelectedContext();
clearSelectedContext({ evicted: true });
return true;
} finally {
signal?.removeEventListener('abort', abort);
+59
View File
@@ -7,6 +7,7 @@ import {
isPointerFree,
} from '../../data/inputOwnership.js';
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
import * as contextStore from '../../data/contextStore.js';
const time = '2026-09-16T03:00:00.000Z';
const storm = (id = 'ep152026') => ({
@@ -481,6 +482,64 @@ test('explicit cyclone selection publishes shared context without auto-claiming
h.layer.destroy();
});
test('shared cyclone context distinguishes switches, deliberate clears and feed eviction', async (t) => {
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
const host = new EventTarget();
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: host,
});
let h;
t.after(() => {
try {
h?.layer.destroy();
} finally {
if (previousWindow)
Object.defineProperty(globalThis, 'window', previousWindow);
else delete globalThis.window;
}
});
const events = [];
host.addEventListener('gev:entity-selected', ({ detail }) =>
events.push(['selected', detail.id]),
);
host.addEventListener('gev:entity-selection-cleared', ({ detail }) =>
events.push(['cleared', detail.reason]),
);
let next = snapshot([storm(), storm('ep162026')]);
h = harness({ getSnapshot: async () => next }, { context: contextStore });
h.layer.enable();
await h.layer.update();
h.layer.setParams({ stormId: 'ep152026' });
h.layer.setParams({ stormId: 'ep162026' });
assert.deepEqual(events, [
['selected', 'cyclone:ep152026'],
['selected', 'cyclone:ep162026'],
]);
assert.deepEqual(
[...contextStore.getContextStore().entities.keys()],
['cyclone:ep162026'],
);
await h.layer.update();
assert.equal(events.length, 2, 'refresh must not announce selection');
h.layer.setParams({ clear: true });
assert.deepEqual(events.at(-1), ['cleared', 'deliberate']);
assert.equal(contextStore.getContextStore().entities.size, 0);
h.layer.setParams({ stormId: 'ep162026' });
next = snapshot([storm()]);
await h.layer.update();
assert.deepEqual(events.at(-1), ['cleared', 'evicted']);
assert.equal(contextStore.getContextStore().entities.size, 0);
assert.equal(contextStore.getContextStore().selectedEntityId, null);
h.layer.setParams({ stormId: 'ep152026' });
h.layer.disable();
assert.deepEqual(events.at(-1), ['cleared', 'deliberate']);
assert.equal(contextStore.getContextStore().entities.size, 0);
});
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
let next = snapshot([storm(), storm('ep162026')]);
const h = harness({ getSnapshot: async () => next });