refactor: isolate Context controls and layer restoration

This commit is contained in:
Sameh Khamis committed 2026-09-13 01:40:59 -07:00
1 parent 7f70b97536
commit fc369cc65a
26 files changed
+1471 -1271

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
import { contextAllowedLayerIds, mergeContextTransitionErrors } from '../contextModePolicy.js';
export async function _selectContextMode(mode, { notificationToken = null, signal = null } = {}) {
if (this.destroyed || !this._dataManager) return false;
if (this._clearSelectedLayersPromise) return false;
this._contextTransitionFailedLayerIds = [];
const generation = ++this._contextModeGeneration;
const isCurrent = () => !this.destroyed && generation === this._contextModeGeneration;
this._contextModeEntryIntent = null;
this._contextModeReplacementIntent = null;
this._contextModeChanging = true;
this._syncContextModeButtons();
try {
if (mode !== 'flights' && this.cockpitView?.active) {
this.cockpitView.exit({ restoreTracking: false });
}
if (!mode) {
this._contextMode = null;
this._syncContextModeButtons();
await this._restoreContextSession({ notificationToken, signal });
return isCurrent();
}
// A cross-mode switch dismantles the prior mode BEFORE the new one is
// committed. If the caller aborts in that window the switch never lands,
// and the resting state is Context OFF — reported as such by
// setContextMode rather than dressed up as a clean cancellation. See the
// note above _restoreContextSessionAfterLayerSettles.
const crossModeSwitch = Boolean(this._contextMode && this._contextMode !== mode);
if (crossModeSwitch) {
this._contextMode = null;
await this._restoreContextSession({ notificationToken, signal });
if (!isCurrent()) return false;
if (signal?.aborted) return false;
}
this._captureContextSessionSnapshot();
this._contextMode = mode;
this._syncContextModeButtons();
// Replay isolation must settle before Space Missions starts. Contacts
// keeps its non-dependency teardown in the background so slow source
// shutdown does not delay cockpit entry.
try {
await this._clearLayersOutsideContextMode(mode, { notificationToken, signal });
} catch (error) {
if (!isCurrent()) return false;
let transitionError = error;
console.warn(`[Context] ${mode} isolation failed`, error);
this._contextMode = null;
this._syncContextModeButtons();
try {
await this._restoreContextSession({ notificationToken });
} catch (restoreError) {
transitionError = mergeContextTransitionErrors(transitionError, restoreError);
this._contextTransitionFailedLayerIds = [...(transitionError.failedLayerIds || [])];
throw transitionError;
}
this._contextTransitionFailedLayerIds = [...(transitionError?.failedLayerIds || [])];
return false;
}
if (!isCurrent()) return false;
// Entry is one transaction: isolation succeeded above, so a failed mode
// activation must roll the cleared layers back instead of stranding the
// user in a half-entered mode with an orphaned snapshot.
const entryLayerId = mode === 'flights' ? 'military-awareness' : 'rocket-launches';
if (mode === 'flights') {
this._dataManager.setLayerParams('military-awareness', { passive: false });
}
let activated = false;
let activationError = null;
let activationIntent = null;
let terminalIntentOutcome = null;
try {
activationIntent = this._dataManager._setEnabledWithIntent(
entryLayerId,
true,
{ notificationToken, ...(signal ? { signal } : {}) },
);
this._contextModeEntryIntent = {
generation,
layerId: entryLayerId,
intentEpoch: activationIntent.intentEpoch,
};
activated = await activationIntent.promise;
terminalIntentOutcome = await this._dataManager._waitForVisibilityIntent?.(
entryLayerId,
activationIntent.intentEpoch,
);
} catch (error) {
activationError = error;
}
if (!isCurrent()) return false;
let replacementIntent = mode === 'space-missions'
&& this._contextModeReplacementIntent?.generation === generation
&& this._contextModeReplacementIntent.layerId === entryLayerId
? this._contextModeReplacementIntent
: null;
while (replacementIntent) {
const outcome = await this._dataManager._waitForVisibilityIntent?.(
entryLayerId,
replacementIntent.intentEpoch,
);
terminalIntentOutcome = outcome;
if (!isCurrent()) return false;
const replacementOwnsMode = outcome?.intentEpoch === replacementIntent.intentEpoch
&& outcome.enabled === true
&& outcome.succeeded === true;
if (replacementOwnsMode) {
this._contextModeEntering = null;
this._contextModeEntryIntent = null;
this._contextModeReplacementIntent = null;
this._syncContextModeButtons();
return true;
}
const successorEpoch = outcome?.cancellationReason === 'superseded'
&& outcome.successorEnabled === true
&& Number.isInteger(outcome.successorIntentEpoch)
&& outcome.successorIntentEpoch > replacementIntent.intentEpoch
? outcome.successorIntentEpoch
: null;
replacementIntent = successorEpoch === null ? null : {
generation,
layerId: entryLayerId,
intentEpoch: successorEpoch,
};
}
if (activationError || activated === false || !this._dataManager.isEnabled(entryLayerId)) {
const cancelledAndSettled = terminalIntentOutcome?.succeeded === false
&& ['caller-abort', 'resource-abort', 'superseded'].includes(
terminalIntentOutcome.cancellationReason,
);
let transitionError = null;
if (!cancelledAndSettled) {
transitionError = activationError instanceof Error
? activationError
: new Error(`Context activation failed for: ${entryLayerId}`);
transitionError.failedLayerIds = [...new Set([
...(transitionError.failedLayerIds || []),
entryLayerId,
])];
this._contextTransitionFailedLayerIds = [...transitionError.failedLayerIds];
console.warn(`[Context] ${mode} activation failed; restoring previous layers`, activationError || 'not enabled');
}
this._contextMode = null;
this._contextModeEntryIntent = null;
this._contextModeReplacementIntent = null;
this._syncContextModeButtons();
try {
await this._restoreContextSession({
excludeLayerIds: [entryLayerId],
notificationToken,
});
} catch (restoreError) {
transitionError = mergeContextTransitionErrors(transitionError, restoreError);
this._contextTransitionFailedLayerIds = [...(transitionError?.failedLayerIds || [])];
throw transitionError;
}
// `null` means the requested entry was cancelled and its exact rollback
// completed. The action wrapper treats that as a silent non-commit,
// while callers still require literal `true` before expanding Context.
return cancelledAndSettled ? null : false;
}
this._contextModeEntryIntent = null;
return true;
} finally {
if (isCurrent()) {
this._contextModeChanging = false;
this._syncContextModeButtons();
}
}
}
export async function _deactivateContextForLayerChange({ notificationToken = null } = {}) {
this._contextModeGeneration += 1;
this._contextModeChanging = true;
this._contextMode = null;
this._contextModeEntryIntent = null;
this._contextModeReplacementIntent = null;
if (this.cockpitView?.active) this.cockpitView.exit({ restoreTracking: false });
this._syncContextModeButtons();
try {
await this._restoreContextSession({ notificationToken });
} finally {
this._contextModeChanging = false;
this._syncContextModeButtons();
}
}
export async function _clearLayersOutsideContextMode(
mode = null,
{ notificationToken = null, signal = null } = {},
) {
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 } : {}),
}),
});
}
}
const results = await Promise.all(pending.map(({ transition }) => transition));
const failed = pending
.filter(({ layerId }, index) => results[index] === false || this._dataManager.isEnabled(layerId))
.map(({ layerId }) => layerId);
if (failed.length > 0) {
const error = new Error(`Context isolation failed for: ${failed.join(', ')}`);
error.failedLayerIds = failed;
throw error;
}
}