mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +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
|
- Explicit cyclone selections publish only the currently selected NHC/CPHC
|
||||||
storm into shared context. Switching storms evicts the previous identity
|
storm into shared context. Switching storms removes the previous identity
|
||||||
instead of leaving stale coordinates active (kvnloo, #847; reviewed by
|
without a false eviction event. Deliberate clears remain distinct from feed
|
||||||
quanticsoul4772).
|
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
|
the prior record; automatic row selection never claims global context, and
|
||||||
refreshes do not re-announce selection. Clearing or losing an explicitly selected
|
refreshes do not re-announce selection. Clearing or losing an explicitly selected
|
||||||
storm removes shared context rather than assigning a fallback.
|
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.
|
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.
|
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 notify = () => listener?.();
|
||||||
const selected = () =>
|
const selected = () =>
|
||||||
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
||||||
function clearSelectedContext() {
|
function clearSelectedContext({ evicted = false } = {}) {
|
||||||
|
if (!evicted)
|
||||||
|
context?.clearSelectedEntityContextForLayer?.('weather-cyclones');
|
||||||
context?.removeEntityContextsForLayer?.('weather-cyclones');
|
context?.removeEntityContextsForLayer?.('weather-cyclones');
|
||||||
selectedContextStormId = null;
|
selectedContextStormId = null;
|
||||||
selectedContextEntity = null;
|
selectedContextEntity = null;
|
||||||
@@ -98,14 +100,11 @@ export function createCyclonesLayer({
|
|||||||
function publishSelectedContext({ announce = false } = {}) {
|
function publishSelectedContext({ announce = false } = {}) {
|
||||||
const storm = selected();
|
const storm = selected();
|
||||||
if (!storm || !context) {
|
if (!storm || !context) {
|
||||||
if (!storm) clearSelectedContext();
|
if (!storm) clearSelectedContext({ evicted: true });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
selectedContextEntity ||= { show: true };
|
selectedContextEntity ||= { show: true };
|
||||||
const contextId = `cyclone:${storm.id}`;
|
const contextId = `cyclone:${storm.id}`;
|
||||||
context.removeEntityContextsForLayer?.('weather-cyclones', {
|
|
||||||
retainIds: new Set([contextId]),
|
|
||||||
});
|
|
||||||
const record = context.registerEntityContext?.(selectedContextEntity, {
|
const record = context.registerEntityContext?.(selectedContextEntity, {
|
||||||
id: contextId,
|
id: contextId,
|
||||||
layerId: 'weather-cyclones',
|
layerId: 'weather-cyclones',
|
||||||
@@ -131,6 +130,9 @@ export function createCyclonesLayer({
|
|||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
selectedContextStormId = storm.id;
|
selectedContextStormId = storm.id;
|
||||||
if (announce) context.selectEntityContext?.(selectedContextEntity);
|
if (announce) context.selectEntityContext?.(selectedContextEntity);
|
||||||
|
context.removeEntityContextsForLayer?.('weather-cyclones', {
|
||||||
|
retainIds: new Set([contextId]),
|
||||||
|
});
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +304,7 @@ export function createCyclonesLayer({
|
|||||||
snapshot = next;
|
snapshot = next;
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
clearSelectedContext();
|
clearSelectedContext({ evicted: true });
|
||||||
error = next.reason || ADVISORIES_UNAVAILABLE;
|
error = next.reason || ADVISORIES_UNAVAILABLE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -327,7 +329,7 @@ export function createCyclonesLayer({
|
|||||||
else rendering.setSelection(selectedId);
|
else rendering.setSelection(selectedId);
|
||||||
if (selectedContextStormId) {
|
if (selectedContextStormId) {
|
||||||
if (selectedId === selectedContextStormId) publishSelectedContext();
|
if (selectedId === selectedContextStormId) publishSelectedContext();
|
||||||
else clearSelectedContext();
|
else clearSelectedContext({ evicted: true });
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -338,7 +340,7 @@ export function createCyclonesLayer({
|
|||||||
snapshot = null;
|
snapshot = null;
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
++navigationGeneration;
|
++navigationGeneration;
|
||||||
clearSelectedContext();
|
clearSelectedContext({ evicted: true });
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
signal?.removeEventListener('abort', abort);
|
signal?.removeEventListener('abort', abort);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
isPointerFree,
|
isPointerFree,
|
||||||
} from '../../data/inputOwnership.js';
|
} from '../../data/inputOwnership.js';
|
||||||
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
|
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
|
||||||
|
import * as contextStore from '../../data/contextStore.js';
|
||||||
|
|
||||||
const time = '2026-09-16T03:00:00.000Z';
|
const time = '2026-09-16T03:00:00.000Z';
|
||||||
const storm = (id = 'ep152026') => ({
|
const storm = (id = 'ep152026') => ({
|
||||||
@@ -481,6 +482,64 @@ test('explicit cyclone selection publishes shared context without auto-claiming
|
|||||||
h.layer.destroy();
|
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 () => {
|
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
|
||||||
let next = snapshot([storm(), storm('ep162026')]);
|
let next = snapshot([storm(), storm('ep162026')]);
|
||||||
const h = harness({ getSnapshot: async () => next });
|
const h = harness({ getSnapshot: async () => next });
|
||||||
|
|||||||
Reference in new issue
Block a user