mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
fix(cyclones): distinguish deliberate clears from feed eviction
This commit is contained in:
1 parent
1330605c18
commit
b358b5dba1
4 files changed
+74
-11
No files matched your search
@@ -1,4 +1,4 @@
|
||||
- Explicit cyclone selections publish only the currently selected NHC/CPHC
|
||||
storm into shared context. Switching storms evicts the previous identity
|
||||
instead of leaving stale coordinates active (kvnloo, #847; reviewed by
|
||||
quanticsoul4772).
|
||||
storm into shared context. Switching storms removes the previous identity
|
||||
without a false eviction event. Deliberate clears remain distinct from feed
|
||||
disappearance (kvnloo, #847; reviewed by quanticsoul4772).
|
||||
@@ -394,6 +394,8 @@ with official advisory provenance and source position. Switching storms removes
|
||||
the prior record; automatic row selection never claims global context, and
|
||||
refreshes do not re-announce selection. Clearing or losing an explicitly selected
|
||||
storm removes shared context rather than assigning a fallback.
|
||||
Deliberate clear/disable reports `deliberate`; feed disappearance reports
|
||||
`evicted`. Switching storms selects the replacement before pruning the old record.
|
||||
Consult the linked official advisory for safety decisions.
|
||||
|
||||
Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user