refactor: extract application construction and lifecycle

This commit is contained in:
Sameh Khamis committed 2026-09-11 17:02:54 -07:00
1 parent 092f28e301
commit 4d37760314
36 files changed
+1363 -382

No files matched your search

+22 -2
View File
@@ -101,6 +101,7 @@ export function createAnnotationEngine({
}) {
/** @type {Map<string, object>} live annotations keyed by id */
const annotations = new Map();
let destroyed = false;
let tickHandle = null;
let assistFlightUntil = 0; // camera-assist debounce (see ensureMarksVisible)
// Generation token + in-flight fetch controllers guard against clear / new-topic
@@ -174,7 +175,7 @@ export function createAnnotationEngine({
}
function ensureTicking() {
if (tickHandle != null) return;
if (destroyed || tickHandle != null) return;
const tick = () => {
const now = performance.now();
let changed = false;
@@ -222,6 +223,7 @@ export function createAnnotationEngine({
* @returns {Promise<{ok, drawn, failed, ids, results}>}
*/
async function annotate(requests, opts = {}) {
if (destroyed) return { ok: false, drawn: 0, failed: 0, ids: [], results: [], error: 'destroyed' };
const list = Array.isArray(requests) ? requests : [requests];
if (opts.clearPrevious) clear(); // bumps generation + aborts older pending work
@@ -865,6 +867,17 @@ export function createAnnotationEngine({
const engine = {
annotate,
clear,
destroy() {
if (destroyed) return;
destroyed = true;
try { clear(); } finally {
if (tickHandle != null) cancelAnimationFrame(tickHandle);
tickHandle = null;
outlineListeners.clear();
releaseContinuousRender('annotations');
renderer.destroy();
}
},
fadeOutAll,
count: () => annotations.size,
list: () => Array.from(annotations.values()),
@@ -875,7 +888,7 @@ export function createAnnotationEngine({
* upgrade task, AFTER the mark was mutated in place. Returns an unsubscribe fn.
*/
onOutlineEvent(listener) {
if (typeof listener !== 'function') return () => {};
if (destroyed || typeof listener !== 'function') return () => {};
outlineListeners.add(listener);
return () => outlineListeners.delete(listener);
},
@@ -901,16 +914,22 @@ export function createAnnotationEngine({
async tour() {
clear();
flyTo({ lon: -122.4486, lat: 37.7960, height: 520, heading: 0, pitch: -26, duration: 3 });
if (destroyed) return { ok: false };
await wait(3200);
await annotate([{ type: 'highlight', target: 'Palace of Fine Arts, San Francisco', label: 'Palace of Fine Arts', color: 'amber' }], { persist: true });
if (destroyed) return { ok: false };
await wait(2600);
await annotate([{ type: 'arrow', target: 'Palace of Fine Arts, San Francisco', toTarget: 'Marina Green, San Francisco', label: 'next to the Marina', color: 'cyan' }], { persist: true });
if (destroyed) return { ok: false };
await wait(2600);
flyTo({ lon: -122.4545, lat: 37.7880, height: 1500, heading: 18, pitch: -32, duration: 3 });
if (destroyed) return { ok: false };
await wait(3200);
await annotate([{ type: 'area', target: 'Presidio of San Francisco', label: 'The Presidio — a former Army base', color: 'green', footprint: true }], { persist: true });
if (destroyed) return { ok: false };
await wait(2800);
await annotate([{ type: 'pin', target: 'Letterman Digital Arts Center, San Francisco', label: 'ILM / Lucasfilm', color: 'red' }], { persist: true });
if (destroyed) return { ok: false };
await wait(2600);
await annotate([{ type: 'route', color: 'amber', label: 'Crissy Field shoreline', points: [
{ target: 'Palace of Fine Arts, San Francisco' },
@@ -922,6 +941,7 @@ export function createAnnotationEngine({
};
function flyTo({ lon, lat, height, heading = 0, pitch = -30, duration = 2.5 }) {
if (destroyed) return;
try {
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lon, lat, height),
+41
View File
@@ -668,3 +668,44 @@ test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing
assert.equal(retry.drawn, 1);
assert.equal(live.size, 1, 'the recoloured mark redraws once, with no orphan underneath');
});
test('destroy releases the renderer and prevents subsequent annotation work', async (t) => {
installAnimationFrameStubs(t);
const { renderer, calls } = fakeRenderer();
let destroyed = 0;
renderer.destroy = () => { destroyed++; };
const engine = createAnnotationEngine({ viewer: {}, renderer });
const drawn = await engine.annotate([{ type: 'pin', longitude: -97.74, latitude: 30.27 }]);
assert.equal(drawn.drawn, 1);
engine.destroy();
const additions = calls.add;
const result = await engine.annotate([{ type: 'pin', target: 'Must not resolve' }]);
assert.equal(result.error, 'destroyed');
assert.equal(calls.add, additions);
assert.equal(engine.count(), 0);
engine.destroy();
assert.equal(destroyed, 1);
assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations'));
});
test('a late annotation resolver cannot redraw after destruction', async (t) => {
installAnimationFrameStubs(t);
const { renderer, calls } = fakeRenderer();
renderer.destroy = () => {};
let release;
let signal;
const engine = createAnnotationEngine({
viewer: {}, renderer,
resolveTarget: (options) => {
signal = options.signal;
return new Promise((resolve) => { release = resolve; });
},
});
const pending = engine.annotate([{ type: 'pin', target: 'Pending place' }]);
engine.destroy();
assert.equal(signal.aborted, true);
release({ lon: -97.74, lat: 30.27 });
await pending;
assert.equal(calls.add, 0);
assert.equal(engine.count(), 0);
});