mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
273 lines
9.1 KiB
JavaScript
273 lines
9.1 KiB
JavaScript
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;
|
|
}
|
|
}
|