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

-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) =>