mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor: isolate Context controls and layer restoration
This commit is contained in:
1 parent
7f70b97536
commit
fc369cc65a
26 files changed
+1471
-1271
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user