mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
style: adopt application modules into scoped formatting
This commit is contained in:
1 parent
4d37760314
commit
8666705e12
17 files changed
+528
-291
No files matched your search
+45
-12
@@ -9,10 +9,21 @@ const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
|
||||
* @param {{createScene:Function,createControls:Function,createData:Function,createTools:Function}} constructors
|
||||
* @returns {{start:Function,destroy:Function,subscribe:Function,getState:Function,getComponents:Function}}
|
||||
*/
|
||||
export function createApplication({ createScene, createControls, createData, createTools }) {
|
||||
const factories = { scene: createScene, controls: createControls, data: createData, tools: createTools };
|
||||
export function createApplication({
|
||||
createScene,
|
||||
createControls,
|
||||
createData,
|
||||
createTools,
|
||||
}) {
|
||||
const factories = {
|
||||
scene: createScene,
|
||||
controls: createControls,
|
||||
data: createData,
|
||||
tools: createTools,
|
||||
};
|
||||
for (const phase of START_ORDER) {
|
||||
if (typeof factories[phase] !== 'function') throw new TypeError(`Missing ${phase} constructor`);
|
||||
if (typeof factories[phase] !== 'function')
|
||||
throw new TypeError(`Missing ${phase} constructor`);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const cleanups = Object.fromEntries(START_ORDER.map((phase) => [phase, []]));
|
||||
@@ -26,7 +37,11 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
function publish(status, phase = null) {
|
||||
state = Object.freeze({ status, phase });
|
||||
for (const listener of [...listeners]) {
|
||||
try { listener(state); } catch { console.error('Application state listener failed'); }
|
||||
try {
|
||||
listener(state);
|
||||
} catch {
|
||||
console.error('Application state listener failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +50,16 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
const errors = [];
|
||||
for (const phase of STOP_ORDER) {
|
||||
while (cleanups[phase].length) {
|
||||
try { await cleanups[phase].pop()(); } catch (error) { errors.push(error); }
|
||||
try {
|
||||
await cleanups[phase].pop()();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
delete components[phase];
|
||||
}
|
||||
if (errors.length) throw new AggregateError(errors, 'Application cleanup failed');
|
||||
if (errors.length)
|
||||
throw new AggregateError(errors, 'Application cleanup failed');
|
||||
});
|
||||
return cleanupPromise;
|
||||
}
|
||||
@@ -57,7 +77,9 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
signal: controller.signal,
|
||||
defer(dispose) {
|
||||
if (!acceptingCleanup || typeof dispose !== 'function') {
|
||||
throw new TypeError('Register cleanup during component construction');
|
||||
throw new TypeError(
|
||||
'Register cleanup during component construction',
|
||||
);
|
||||
}
|
||||
cleanups[phase].push(dispose);
|
||||
},
|
||||
@@ -73,8 +95,13 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
} catch (error) {
|
||||
controller.abort();
|
||||
let failure = error;
|
||||
try { await cleanup(); } catch (cleanupError) {
|
||||
failure = new AggregateError([error, cleanupError], 'Application startup and cleanup failed');
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (cleanupError) {
|
||||
failure = new AggregateError(
|
||||
[error, cleanupError],
|
||||
'Application startup and cleanup failed',
|
||||
);
|
||||
}
|
||||
if (!destroyPromise) publish('failed');
|
||||
throw failure;
|
||||
@@ -83,7 +110,8 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
|
||||
return Object.freeze({
|
||||
start() {
|
||||
if (destroyPromise) return Promise.reject(new Error('Application has been destroyed'));
|
||||
if (destroyPromise)
|
||||
return Promise.reject(new Error('Application has been destroyed'));
|
||||
// Assign before notifying subscribers, including reentrant start/destroy.
|
||||
startPromise ||= Promise.resolve().then(initialize);
|
||||
return startPromise;
|
||||
@@ -109,10 +137,15 @@ export function createApplication({ createScene, createControls, createData, cre
|
||||
getState: () => state,
|
||||
getComponents: () => Object.freeze({ ...components }),
|
||||
subscribe(listener) {
|
||||
if (typeof listener !== 'function') throw new TypeError('Expected a state listener');
|
||||
if (typeof listener !== 'function')
|
||||
throw new TypeError('Expected a state listener');
|
||||
if (state.status === 'destroyed') return () => {};
|
||||
listeners.add(listener);
|
||||
try { listener(state); } catch { console.error('Application state listener failed'); }
|
||||
try {
|
||||
listener(state);
|
||||
} catch {
|
||||
console.error('Application state listener failed');
|
||||
}
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,31 +6,43 @@ import { createApplication } from './application.js';
|
||||
const phases = ['Scene', 'Controls', 'Data', 'Tools'];
|
||||
function fixture(overrides = {}) {
|
||||
const events = [];
|
||||
const constructors = Object.fromEntries(phases.map((phase) => [
|
||||
`create${phase}`,
|
||||
({ defer }) => {
|
||||
events.push(`start:${phase}`);
|
||||
defer(() => events.push(`stop:${phase}`));
|
||||
return { name: phase };
|
||||
},
|
||||
]));
|
||||
const constructors = Object.fromEntries(
|
||||
phases.map((phase) => [
|
||||
`create${phase}`,
|
||||
({ defer }) => {
|
||||
events.push(`start:${phase}`);
|
||||
defer(() => events.push(`stop:${phase}`));
|
||||
return { name: phase };
|
||||
},
|
||||
]),
|
||||
);
|
||||
return { events, app: createApplication({ ...constructors, ...overrides }) };
|
||||
}
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((done) => { resolve = done; });
|
||||
const promise = new Promise((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test('importing and constructing the package is inactive without browser globals', () => {
|
||||
const result = spawnSync(process.execPath, ['--input-type=module', '-e', `
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--input-type=module',
|
||||
'-e',
|
||||
`
|
||||
globalThis.fetch = () => { throw new Error('unexpected request'); };
|
||||
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
|
||||
const { createApplication } = await import('gods-eye-view/application');
|
||||
const fail = () => { throw new Error('unexpected construction'); };
|
||||
const app = createApplication({ createScene: fail, createControls: fail, createData: fail, createTools: fail });
|
||||
if (app.getState().status !== 'created') process.exit(1);
|
||||
`], { cwd: new URL('../../', import.meta.url), encoding: 'utf8' });
|
||||
`,
|
||||
],
|
||||
{ cwd: new URL('../../', import.meta.url), encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
|
||||
@@ -39,13 +51,15 @@ test('constructors are validated before allocating anything', () => {
|
||||
});
|
||||
|
||||
test('startup shares one promise and teardown keeps data alive for controls', async () => {
|
||||
const { app, events } = fixture({ createTools({ scene, controls, data, defer }) {
|
||||
assert.equal(scene.name, 'Scene');
|
||||
assert.equal(controls.name, 'Controls');
|
||||
assert.equal(data.name, 'Data');
|
||||
defer(() => events.push('stop:Tools'));
|
||||
return { name: 'Tools' };
|
||||
} });
|
||||
const { app, events } = fixture({
|
||||
createTools({ scene, controls, data, defer }) {
|
||||
assert.equal(scene.name, 'Scene');
|
||||
assert.equal(controls.name, 'Controls');
|
||||
assert.equal(data.name, 'Data');
|
||||
defer(() => events.push('stop:Tools'));
|
||||
return { name: 'Tools' };
|
||||
},
|
||||
});
|
||||
const states = [];
|
||||
app.subscribe((state) => states.push(`${state.status}:${state.phase}`));
|
||||
const started = app.start();
|
||||
@@ -57,7 +71,12 @@ test('startup shares one promise and teardown keeps data alive for controls', as
|
||||
const stopped = app.destroy();
|
||||
assert.equal(stopped, app.destroy());
|
||||
await stopped;
|
||||
assert.deepEqual(events.slice(-4), ['stop:Tools', 'stop:Controls', 'stop:Data', 'stop:Scene']);
|
||||
assert.deepEqual(events.slice(-4), [
|
||||
'stop:Tools',
|
||||
'stop:Controls',
|
||||
'stop:Data',
|
||||
'stop:Scene',
|
||||
]);
|
||||
assert.deepEqual(app.getComponents(), {});
|
||||
assert.equal(states.at(-1), 'destroyed:null');
|
||||
await assert.rejects(app.start(), /destroyed/);
|
||||
@@ -65,16 +84,24 @@ test('startup shares one promise and teardown keeps data alive for controls', as
|
||||
|
||||
for (const phase of phases) {
|
||||
test(`failure during ${phase} releases partially acquired resources`, async () => {
|
||||
const { app, events } = fixture({ [`create${phase}`]({ defer }) {
|
||||
defer(() => events.push('partial:released'));
|
||||
throw new Error('broken constructor');
|
||||
} });
|
||||
const { app, events } = fixture({
|
||||
[`create${phase}`]({ defer }) {
|
||||
defer(() => events.push('partial:released'));
|
||||
throw new Error('broken constructor');
|
||||
},
|
||||
});
|
||||
await assert.rejects(app.start(), /broken constructor/);
|
||||
assert.equal(events.filter((value) => value === 'partial:released').length, 1);
|
||||
assert.equal(
|
||||
events.filter((value) => value === 'partial:released').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(app.getState().status, 'failed');
|
||||
assert.deepEqual(app.getComponents(), {});
|
||||
await app.destroy();
|
||||
assert.equal(events.filter((value) => value === 'partial:released').length, 1);
|
||||
assert.equal(
|
||||
events.filter((value) => value === 'partial:released').length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,18 +118,22 @@ test('destroy aborts an in-flight constructor and cleans late acquisitions befor
|
||||
const gate = deferred();
|
||||
const entered = deferred();
|
||||
let observedSignal;
|
||||
const { app, events } = fixture({ async createScene({ signal, defer }) {
|
||||
observedSignal = signal;
|
||||
entered.resolve();
|
||||
await gate.promise;
|
||||
defer(() => events.push('late:released'));
|
||||
return {};
|
||||
} });
|
||||
const { app, events } = fixture({
|
||||
async createScene({ signal, defer }) {
|
||||
observedSignal = signal;
|
||||
entered.resolve();
|
||||
await gate.promise;
|
||||
defer(() => events.push('late:released'));
|
||||
return {};
|
||||
},
|
||||
});
|
||||
const started = app.start();
|
||||
const failed = assert.rejects(started, { name: 'AbortError' });
|
||||
await entered.promise;
|
||||
let settled = false;
|
||||
const stopped = app.destroy().then(() => { settled = true; });
|
||||
const stopped = app.destroy().then(() => {
|
||||
settled = true;
|
||||
});
|
||||
assert.equal(observedSignal.aborted, true);
|
||||
await Promise.resolve();
|
||||
assert.equal(settled, false);
|
||||
@@ -113,24 +144,35 @@ test('destroy aborts an in-flight constructor and cleans late acquisitions befor
|
||||
});
|
||||
|
||||
test('cleanup errors are reported after all remaining resources are attempted', async () => {
|
||||
const { app, events } = fixture({ createTools({ defer }) {
|
||||
defer(() => events.push('tools:earlier'));
|
||||
defer(() => { throw new Error('dispose failed'); });
|
||||
return {};
|
||||
} });
|
||||
const { app, events } = fixture({
|
||||
createTools({ defer }) {
|
||||
defer(() => events.push('tools:earlier'));
|
||||
defer(() => {
|
||||
throw new Error('dispose failed');
|
||||
});
|
||||
return {};
|
||||
},
|
||||
});
|
||||
await app.start();
|
||||
await assert.rejects(app.destroy(), (error) => {
|
||||
assert.ok(error instanceof AggregateError);
|
||||
assert.equal(error.errors[0].message, 'dispose failed');
|
||||
return true;
|
||||
});
|
||||
assert.deepEqual(events.slice(-4), ['tools:earlier', 'stop:Controls', 'stop:Data', 'stop:Scene']);
|
||||
assert.deepEqual(events.slice(-4), [
|
||||
'tools:earlier',
|
||||
'stop:Controls',
|
||||
'stop:Data',
|
||||
'stop:Scene',
|
||||
]);
|
||||
assert.equal(app.getState().status, 'failed');
|
||||
});
|
||||
|
||||
test('subscribers can destroy during startup without allowing the next constructor', async () => {
|
||||
const { app, events } = fixture();
|
||||
app.subscribe((state) => { if (state.phase === 'controls') void app.destroy(); });
|
||||
app.subscribe((state) => {
|
||||
if (state.phase === 'controls') void app.destroy();
|
||||
});
|
||||
await assert.rejects(app.start(), { name: 'AbortError' });
|
||||
await app.destroy();
|
||||
assert.deepEqual(events, ['start:Scene', 'stop:Scene']);
|
||||
@@ -138,7 +180,9 @@ test('subscribers can destroy during startup without allowing the next construct
|
||||
|
||||
test('destroy during ready notification rejects startup and tears everything down', async () => {
|
||||
const { app } = fixture();
|
||||
app.subscribe((state) => { if (state.status === 'ready') void app.destroy(); });
|
||||
app.subscribe((state) => {
|
||||
if (state.status === 'ready') void app.destroy();
|
||||
});
|
||||
await assert.rejects(app.start(), { name: 'AbortError' });
|
||||
await app.destroy();
|
||||
assert.equal(app.getState().status, 'destroyed');
|
||||
@@ -146,7 +190,12 @@ test('destroy during ready notification rejects startup and tears everything dow
|
||||
|
||||
test('cleanup registration closes when a constructor settles', async () => {
|
||||
let register;
|
||||
const { app } = fixture({ createScene({ defer }) { register = defer; return {}; } });
|
||||
const { app } = fixture({
|
||||
createScene({ defer }) {
|
||||
register = defer;
|
||||
return {};
|
||||
},
|
||||
});
|
||||
await app.start();
|
||||
assert.throws(() => register(() => {}), /during component construction/);
|
||||
await app.destroy();
|
||||
@@ -163,7 +212,8 @@ test('different application instances do not share lifecycle state', async () =>
|
||||
});
|
||||
|
||||
test('the separate viewer export imports without constructing a browser viewer', async () => {
|
||||
const { createApplicationViewer } = await import('gods-eye-view/application/viewer');
|
||||
const { createApplicationViewer } =
|
||||
await import('gods-eye-view/application/viewer');
|
||||
assert.equal(typeof createApplicationViewer, 'function');
|
||||
assert.throws(() => createApplicationViewer({}), /containers are required/);
|
||||
});
|
||||
@@ -2,17 +2,28 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MapStackController } from '../mapStackController.js';
|
||||
import {
|
||||
installRenderGovernor, uninstallRenderGovernor,
|
||||
holdContinuousRender, governorRequestRender, getRenderGovernorDiagnostics,
|
||||
installRenderGovernor,
|
||||
uninstallRenderGovernor,
|
||||
holdContinuousRender,
|
||||
governorRequestRender,
|
||||
getRenderGovernorDiagnostics,
|
||||
} from '../renderGovernor.js';
|
||||
|
||||
test('destroy invalidates a pending imagery provider before it can touch the viewer', async () => {
|
||||
let resolveProvider;
|
||||
let removed = 0;
|
||||
const changes = [];
|
||||
const controller = new MapStackController({}, { onChange: (state) => changes.push(state.status) });
|
||||
controller._getImageryProvider = () => new Promise((resolve) => { resolveProvider = resolve; });
|
||||
controller._removeImageryErrorListener = () => { removed++; };
|
||||
const controller = new MapStackController(
|
||||
{},
|
||||
{ onChange: (state) => changes.push(state.status) },
|
||||
);
|
||||
controller._getImageryProvider = () =>
|
||||
new Promise((resolve) => {
|
||||
resolveProvider = resolve;
|
||||
});
|
||||
controller._removeImageryErrorListener = () => {
|
||||
removed++;
|
||||
};
|
||||
const switching = controller.setStack('osm');
|
||||
controller.destroy();
|
||||
resolveProvider({ provider: {} });
|
||||
@@ -26,7 +37,13 @@ test('destroy invalidates a pending imagery provider before it can touch the vie
|
||||
|
||||
test('uninstall only releases the current viewer and makes later render requests inactive', () => {
|
||||
let renders = 0;
|
||||
const viewer = { scene: { requestRender: () => { renders++; } } };
|
||||
const viewer = {
|
||||
scene: {
|
||||
requestRender: () => {
|
||||
renders++;
|
||||
},
|
||||
},
|
||||
};
|
||||
installRenderGovernor(viewer);
|
||||
holdContinuousRender('fixture');
|
||||
uninstallRenderGovernor({});
|
||||
|
||||
@@ -8,7 +8,15 @@ test('teardown before the initial camera delay prevents a late flight', (t) => {
|
||||
let cancelled = 0;
|
||||
const stop = flyToAustin({
|
||||
isDestroyed: () => false,
|
||||
camera: { setView() {}, flyTo() { flights++; }, cancelFlight() { cancelled++; } },
|
||||
camera: {
|
||||
setView() {},
|
||||
flyTo() {
|
||||
flights++;
|
||||
},
|
||||
cancelFlight() {
|
||||
cancelled++;
|
||||
},
|
||||
},
|
||||
});
|
||||
stop();
|
||||
t.mock.timers.tick(1000);
|
||||
|
||||
+9
-8
@@ -2,7 +2,8 @@ import * as Cesium from 'cesium';
|
||||
|
||||
/** Create the standard globe viewer in caller-owned, visible containers. */
|
||||
export function createApplicationViewer({ container, creditContainer }) {
|
||||
if (!container || !creditContainer) throw new TypeError('Viewer and credit containers are required');
|
||||
if (!container || !creditContainer)
|
||||
throw new TypeError('Viewer and credit containers are required');
|
||||
const viewer = new Cesium.Viewer(container, {
|
||||
timeline: false,
|
||||
animation: false,
|
||||
@@ -21,13 +22,13 @@ export function createApplicationViewer({ container, creditContainer }) {
|
||||
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
||||
});
|
||||
try {
|
||||
viewer.targetFrameRate = 60;
|
||||
viewer.scene.globe.show = false;
|
||||
viewer.scene.skyAtmosphere.show = true;
|
||||
viewer.scene.skyAtmosphere.atmosphereLightIntensity = 18;
|
||||
viewer.scene.skyAtmosphere.saturationShift = -0.12;
|
||||
viewer.scene.skyAtmosphere.brightnessShift = -0.08;
|
||||
return viewer;
|
||||
viewer.targetFrameRate = 60;
|
||||
viewer.scene.globe.show = false;
|
||||
viewer.scene.skyAtmosphere.show = true;
|
||||
viewer.scene.skyAtmosphere.atmosphereLightIntensity = 18;
|
||||
viewer.scene.skyAtmosphere.saturationShift = -0.12;
|
||||
viewer.scene.skyAtmosphere.brightnessShift = -0.08;
|
||||
return viewer;
|
||||
} catch (error) {
|
||||
viewer.destroy();
|
||||
throw error;
|
||||
|
||||
Reference in new issue
Block a user