Suspend only the layers Context isolation is switching off

The session snapshot recorded a user's switch only after the CCTV panel had
already rendered the layer off, so a first switch-off by the user inside
Contacts, or after a partly failed restore, still kept the panel open.
Context controls now hold the layers _clearLayersOutsideContextMode is
switching off while its transitions run; suspendsLayer reads that set, and
the snapshot is no longer consulted.
This commit is contained in:
Sameh Khamis committed 2026-11-13 18:32:57 -08:00
1 parent f0ee1b95ca
commit 9b46692c64
6 files changed
+50 -63

No files matched your search

+3 -1
View File
@@ -2235,7 +2235,9 @@ the collapse, so a panel the desktop accordion only folded has its saved and
shared state closed too. Clear selected data layers and Context isolation
(Contacts, Space Missions) switch the layer off without the user asking: the
panel and its share preference stay as they are, and the layer comes back to an
open panel. A panel opened by hand while the layer is off stays open, and its
open panel. Only the switch-off isolation makes counts (`suspendsLayer`, true
while `_clearLayersOutsideContextMode` switches the layer off): the user
switching CCTV off inside Contacts closes the panel as anywhere else. A panel opened by hand while the layer is off stays open, and its
meta line reads the catalog count, not the last active camera.
## UI disposal
-1
View File
@@ -277,7 +277,6 @@ export function recordContextSessionUserChange({
) {
return false;
}
snapshot.userSwitched?.add(change.layerId);
const isCompanion = CONTEXT_COMPANIONS.has(change.layerId);
if (change.enabled) {
if (CONTEXT_ENTRY_LAYER_IDS.includes(change.layerId)) return false;
+6 -8
View File
@@ -51,6 +51,8 @@ export class ContextControls {
this._contextRestoreState = null;
this._contextLayerReactionPromises = new Set();
this._preservePanelStateDuringLayerClear = false;
/** Layers Context isolation is switching off right now. */
this._isolatingLayerIds = new Set();
this._userFacingContextNotificationTokens = new Set();
this._clearSelectedLayersPromise = null;
this._clearSelectedLayersManagerPromise = null;
@@ -60,18 +62,14 @@ export class ContextControls {
if (!this.destroyed) this.actions.showToast(...args);
}
/**
* Whether Context isolation switched this layer off and leaving the mode
* restores it. Such a layer is suspended, not switched off by the user: a
* layer the user switched during the session is theirs again.
* Whether Context isolation is switching this layer off right now: it is
* suspended, and leaving the mode restores it. A switch-off by the user,
* inside the mode or out of it, is not.
* @param {string} layerId Layer id.
* @returns {boolean}
*/
suspendsLayer(layerId) {
const snapshot = this._contextSessionSnapshot;
return Boolean(
snapshot?.enabledLayerIds?.has(layerId) &&
!snapshot.userSwitched?.has(layerId),
);
return this._isolatingLayerIds.has(layerId);
}
setClearBusy(...args) {
if (!this.destroyed) this.actions.setClearBusy(...args);
+14 -33
View File
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { ContextControls } from './contextControls.js';
import { recordContextSessionUserChange } from '../contextModePolicy.js';
class Button extends EventTarget {
constructor() {
@@ -155,7 +154,7 @@ test('a rejected tracked reaction settles without an unhandled rejection', async
assert.equal(controls._contextLayerReactionPromises.size, 0);
});
test('a layer Context isolation switches off is suspended until the mode is left', async (t) => {
test('only a layer Context isolation is switching off is suspended', async (t) => {
const { controls } = fixture(t);
const enabled = new Set(['cctv']);
// What the CCTV panel is told at each visibility change of its layer.
@@ -168,6 +167,7 @@ test('a layer Context isolation switches off is suspended until the mode is left
isEnabled: (layerId) => enabled.has(layerId),
isEffectivelyEnabled: (layerId) => enabled.has(layerId),
async setEnabled(layerId, next) {
// The layer tells its panel it is off while it is being disabled.
if (next) enabled.add(layerId);
else enabled.delete(layerId);
seen.push([layerId, next, controls.suspendsLayer(layerId)]);
@@ -181,47 +181,28 @@ test('a layer Context isolation switches off is suspended until the mode is left
controls._captureContextSessionSnapshot();
await controls._clearLayersOutsideContextMode('flights');
assert.deepEqual(seen, [['cctv', false, true]]);
assert.equal(controls.suspendsLayer('cctv'), true);
assert.equal(controls.suspendsLayer('street-level'), false);
assert.equal(controls.suspendsLayer('cctv'), false, 'only while isolating');
// Inside the mode the user turns CCTV back on, then off: their own.
await controls._dataManager.setEnabled('cctv', true);
await controls._dataManager.setEnabled('cctv', false);
assert.deepEqual(seen.at(-1), ['cctv', false, false]);
await controls._restoreContextSession();
assert.equal(enabled.has('cctv'), true);
assert.equal(controls.suspendsLayer('cctv'), false);
// A switch-off after the mode is left is the user's own.
// A switch-off after the mode is left is the user's own too.
await controls._dataManager.setEnabled('cctv', false);
assert.deepEqual(seen.at(-1), ['cctv', false, false]);
});
test('a layer the user switches inside the mode is no longer suspended', async (t) => {
test('a failed isolation leaves no layer suspended', async (t) => {
const { controls } = fixture(t);
const enabled = new Set(['cctv']);
controls._dataManager = {
layers: new Map([['cctv', {}]]),
getEnabledLayerIds: () => [...enabled],
getLayerParams: () => null,
isEffectivelyEnabled: (layerId) => enabled.has(layerId),
isEnabled: (layerId) => enabled.has(layerId),
async setEnabled(layerId, next) {
if (next) enabled.add(layerId);
else enabled.delete(layerId);
return true;
isEffectivelyEnabled: () => true,
isEnabled: () => true,
setEnabled: async () => {
throw new Error('disable failed');
},
};
controls._captureContextSessionSnapshot();
await controls._clearLayersOutsideContextMode('flights');
assert.equal(controls.suspendsLayer('cctv'), true);
// Inside Contacts the user turns CCTV back on, then off again: the panel
// must close like any other switch-off.
for (const next of [true, false]) {
recordContextSessionUserChange({
snapshot: controls._contextSessionSnapshot,
change: {
type: 'visibility',
origin: 'user',
layerId: 'cctv',
enabled: next,
},
effectiveContextMode: 'flights',
});
}
await assert.rejects(controls._clearLayersOutsideContextMode('flights'));
assert.equal(controls.suspendsLayer('cctv'), false);
});
-3
View File
@@ -36,8 +36,6 @@ export function _captureContextSessionSnapshot({ excludeLayerIds = [] } = {}) {
),
userAdded: new Set(),
userRemoved: new Set(),
// Every layer the user switched during the session, either way.
userSwitched: new Set(),
params,
};
}
@@ -151,7 +149,6 @@ export async function _restoreContextSession({
enabledLayerIds: new Set(restoreState.enabledLayerIds),
userAdded: new Set(),
userRemoved: new Set(),
userSwitched: new Set(),
params: snapshot.params,
};
}
+27 -17
View File
@@ -240,25 +240,35 @@ export async function _clearLayersOutsideContextMode(
) {
const allowed = contextAllowedLayerIds(mode);
const pending = [];
for (const [layerId] of this._dataManager.layers || []) {
// Effective visibility: a disallowed layer still mid-ENABLING must be
// isolated too, or it settles ON inside the exclusive mode.
if (
!allowed.has(layerId) &&
this._dataManager.isEffectivelyEnabled(layerId)
) {
pending.push({
layerId,
transition: this._dataManager.setEnabled(layerId, false, {
notificationToken,
...(signal ? { signal } : {}),
}),
});
// Panels read this while their layer goes off (suspendsLayer): a layer
// isolation switches off is suspended, not switched off by the user.
const isolating = [];
let results;
try {
for (const [layerId] of this._dataManager.layers || []) {
// Effective visibility: a disallowed layer still mid-ENABLING must be
// isolated too, or it settles ON inside the exclusive mode.
if (
!allowed.has(layerId) &&
this._dataManager.isEffectivelyEnabled(layerId)
) {
if (!this._isolatingLayerIds.has(layerId)) {
this._isolatingLayerIds.add(layerId);
isolating.push(layerId);
}
pending.push({
layerId,
transition: this._dataManager.setEnabled(layerId, false, {
notificationToken,
...(signal ? { signal } : {}),
}),
});
}
}
results = await Promise.all(pending.map(({ transition }) => transition));
} finally {
for (const layerId of isolating) this._isolatingLayerIds.delete(layerId);
}
const results = await Promise.all(
pending.map(({ transition }) => transition),
);
const failed = pending
.filter(
({ layerId }, index) =>