mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
Keep the debug handle off window between engines and recover the page after a failed recovery
The application's debug handle is one object, present on window only while a globe is attached, so scripts reading __godsEyeView?.engine.kind see no null engine mid-switch. A detached application has no live components: settle no longer waits for a frame of a missing engine, and an engine that runs again after a failed recovery hides the loading screen the error showed.
This commit is contained in:
1 parent
a99b862580
commit
8e1a3bab3e
3 files changed
+104
-6
No files matched your search
@@ -0,0 +1,89 @@
|
||||
// The debug handle is one object for the page; it is on window only while a
|
||||
// globe is attached, as when every engine built its own application, so
|
||||
// scripts reading `__godsEyeView?.engine.kind` never see a null engine.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApplicationTools } from './tools.js';
|
||||
import { createSceneHost } from './sceneHost.js';
|
||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
|
||||
|
||||
test('the debug handle leaves window between engines and comes back the same', async (t) => {
|
||||
const saved = {
|
||||
window: globalThis.window,
|
||||
document: globalThis.document,
|
||||
localStorage: globalThis.localStorage,
|
||||
};
|
||||
globalThis.window = {};
|
||||
globalThis.localStorage = {
|
||||
getItem: () => null,
|
||||
setItem() {},
|
||||
removeItem() {},
|
||||
};
|
||||
globalThis.document = {
|
||||
hidden: false,
|
||||
getElementById: () => null,
|
||||
querySelector: () => null,
|
||||
body: {
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
||||
appendChild() {},
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
};
|
||||
t.after(() => {
|
||||
_resetRenderGovernorForTest();
|
||||
for (const [name, value] of Object.entries(saved)) {
|
||||
if (value === undefined) delete globalThis[name];
|
||||
else globalThis[name] = value;
|
||||
}
|
||||
});
|
||||
const sceneHost = createSceneHost();
|
||||
const navara = (kind) => ({
|
||||
kind,
|
||||
viewer: null,
|
||||
engine: createFakeEngine({ kind, canvas: new EventTarget() }),
|
||||
tileset: null,
|
||||
mapStackController: { kind: 'maps' },
|
||||
operations: { surface: {} },
|
||||
});
|
||||
let current = navara('navara');
|
||||
await sceneHost.attach(current);
|
||||
const scene = {
|
||||
operations: { surface: {} },
|
||||
sceneHost,
|
||||
current: () => current,
|
||||
};
|
||||
const cleanups = [];
|
||||
await createApplicationTools({
|
||||
scene,
|
||||
voice: {
|
||||
createControl: () => ({ update() {}, on: () => () => {}, remove() {} }),
|
||||
createCard: () => null,
|
||||
createSession: () => ({
|
||||
async start() {},
|
||||
stop() {},
|
||||
sendText() {},
|
||||
sendMapEvent() {},
|
||||
}),
|
||||
},
|
||||
controls: { styleManager: {}, weatherEffects: null },
|
||||
data: {
|
||||
dataManager: { layers: new Map(), getAll: () => [] },
|
||||
presentation: { flushVisible() {} },
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
defer: (dispose) => cleanups.push(dispose),
|
||||
});
|
||||
const handle = window.__godsEyeView;
|
||||
assert.equal(handle.engine, current.engine);
|
||||
await sceneHost.detach({ handoff: true });
|
||||
assert.equal(window.__godsEyeView, undefined);
|
||||
assert.equal(handle.engine, null);
|
||||
current = navara('navara');
|
||||
await sceneHost.attach(current);
|
||||
assert.equal(window.__godsEyeView, handle);
|
||||
assert.equal(handle.engine, current.engine);
|
||||
while (cleanups.length) await cleanups.pop()();
|
||||
assert.equal(window.__godsEyeView, undefined);
|
||||
});
|
||||
+6
-2
@@ -162,7 +162,8 @@ export async function createApplicationTools({
|
||||
{ defer },
|
||||
);
|
||||
|
||||
// The application's debug handle; its globe fields follow the engine.
|
||||
// The application's debug handle, the same object on every globe; its
|
||||
// globe fields follow the engine.
|
||||
const debug = {
|
||||
viewer: null,
|
||||
engine: null,
|
||||
@@ -179,7 +180,6 @@ export async function createApplicationTools({
|
||||
surfaceServices: operations.surface,
|
||||
requestRender: governorRequestRender,
|
||||
};
|
||||
window.__godsEyeView = debug;
|
||||
defer(() => {
|
||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||
});
|
||||
@@ -232,8 +232,12 @@ export async function createApplicationTools({
|
||||
cockpitCloudEffects: controls.cockpitCloudEffects ?? null,
|
||||
voiceCommands: attached.voiceCommands,
|
||||
});
|
||||
// Present only while a globe is attached, as when every engine
|
||||
// built its own application: scripts read `engine.kind` from it.
|
||||
window.__godsEyeView = debug;
|
||||
},
|
||||
detachScene() {
|
||||
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
||||
Object.assign(debug, {
|
||||
viewer: null,
|
||||
engine: null,
|
||||
|
||||
+9
-4
@@ -91,10 +91,12 @@ function createApplication(kind, engineSwitch) {
|
||||
return application;
|
||||
}
|
||||
|
||||
// The running application while a globe is attached to it; null while the
|
||||
// page has no engine (between engines, or after a failed recovery).
|
||||
function liveComponents() {
|
||||
return application?.getState().status === 'ready'
|
||||
? application.getComponents()
|
||||
: null;
|
||||
if (application?.getState().status !== 'ready') return null;
|
||||
const components = application.getComponents();
|
||||
return components.scene?.engine ? components : null;
|
||||
}
|
||||
|
||||
// The page's application is created once; a switch detaches and attaches
|
||||
@@ -108,7 +110,7 @@ const legacySwitch =
|
||||
// parked for a later switch back.
|
||||
async function leave(from) {
|
||||
const components = liveComponents();
|
||||
const attached = Boolean(components?.scene.engine);
|
||||
const attached = Boolean(components);
|
||||
if (attached) {
|
||||
cover ??= keepGlobeFrame(components.scene.engine);
|
||||
cockpit = components.controls.styleManager.getCockpitState();
|
||||
@@ -206,6 +208,9 @@ async function settle() {
|
||||
layerSession.handoffLayers = null;
|
||||
cockpit = null;
|
||||
withdrawEngineRetry();
|
||||
// An engine runs again after a failed recovery showed its error: the
|
||||
// application, built once, does not start its loading chrome again.
|
||||
loading.classList.add('hidden');
|
||||
// The running engine owns the address again, including an entry visited
|
||||
// before its share link listened or while it was held.
|
||||
components.controls.styleManager.shareLinkManager?.resumeNavigation?.();
|
||||
|
||||
Reference in new issue
Block a user