perf(map): stage cesium provider startup

This commit is contained in:
Codex
2026-08-09 22:03:13 +03:00
parent f645725e8c
commit 88cbef1aba
5 changed files with 435 additions and 51 deletions
+85 -50
View File
@@ -37,6 +37,11 @@ import type {
MapPresentationProfile,
} from "./mapPresentationProfile.js";
import { fixedGridOrigin } from "./mapSectorGrid.mjs";
import {
MAP_PROVIDER_STARTUP_POLICY,
runStagedMapProviders,
waitForGlobeViewportReady,
} from "./mapProviderStartup.mjs";
import type {
CameraSpiralConfig,
CameraSpiralState,
@@ -941,14 +946,15 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
let removeRefreshRenderListener: (() => void) | undefined;
let removeRenderErrorListener: (() => void) | undefined;
const removeProviderFailureListeners: Array<() => void> = [];
const providerStartupAbort = new AbortController();
let cancelled = false;
const start = async () => {
try {
const response = await fetch("/api/map/runtime-config");
const response = await fetch("/api/map/runtime-config", { signal: providerStartupAbort.signal });
const config = response.ok ? await response.json() as RuntimeConfig : null;
if (config?.gatewayHealthUrl) {
void fetch(config.gatewayHealthUrl)
void fetch(config.gatewayHealthUrl, { signal: providerStartupAbort.signal })
.then((healthResponse) => healthResponse.ok ? healthResponse.json() as Promise<MapGatewayHealth> : null)
.then((health) => { if (!cancelled) onGatewayHealth?.(health); })
.catch(() => { if (!cancelled) onGatewayHealth?.(null); });
@@ -997,9 +1003,11 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
proxy: resourceProxy,
});
};
const loadEndpoint = async (assetId: string) => {
const loadEndpoint = async (assetId: string, signal = providerStartupAbort.signal) => {
if (!config?.gatewayReady) throw new Error("map_gateway_not_ready");
const endpointResponse = await fetch(`${config.assetEndpointBase}/${assetId}/endpoint`);
const endpointResponse = await fetch(`${config.assetEndpointBase}/${assetId}/endpoint`, {
signal,
});
if (!endpointResponse.ok) throw new Error(`Map Gateway asset ${assetId}: ${endpointResponse.status}`);
return endpointResponse.json() as Promise<IonAssetEndpoint>;
};
@@ -1102,57 +1110,83 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
);
if (config?.gatewayReady) {
// Do not serialize provider startup. A failure in Bing imagery is
// recoverable and must not stop terrain, buildings, or the scene.
void loadEndpoint("2").then(async (endpoint) => {
if (endpoint.externalType !== "BING" || !endpoint.options?.url || endpoint.credentialMode !== "gateway") throw new Error("cesium_live_imagery_endpoint_invalid");
const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), {
// Cesium requires a key-shaped value to form its Bing URL. This
// public marker is stripped by Map Gateway before upstream use.
key: "nodedc-gateway",
mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle,
tileProtocol: "https",
});
if (cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(imageryProvider.errorEvent.addEventListener(() => latchProviderTileFailure("imagery")));
for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
imageryLayerRef.current = viewer.imageryLayers.addImageryProvider(imageryProvider);
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
reportProvider("imagery", "ready");
}).catch((error) => reportProvider("imagery", "error", error));
void loadEndpoint("1").then(async (terrainEndpoint) => {
if (!terrainEndpoint.url || terrainEndpoint.credentialMode !== "gateway") throw new Error("terrain_endpoint_invalid");
const terrainResource = buildResource(terrainEndpoint.url);
const world = await CesiumTerrainProvider.fromUrl(terrainResource, { requestVertexNormals: true, requestWaterMask: true });
if (cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(world.errorEvent.addEventListener(() => latchProviderTileFailure("terrain")));
terrain.world = world;
for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
onReadyChangeRef.current?.(!presentationRef.current.terrainEnabled || Boolean(terrain.world));
reportProvider("terrain", "ready");
}).catch((error) => reportProvider("terrain", "error", error));
void loadEndpoint("96188").then(async (buildingsEndpoint) => {
if (buildingsEndpoint.type !== "3DTILES" || !buildingsEndpoint.url || buildingsEndpoint.credentialMode !== "gateway") throw new Error("buildings_endpoint_invalid");
const buildingsResource = buildResource(buildingsEndpoint.url);
const buildings = await Cesium3DTileset.fromUrl(buildingsResource);
if (cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(buildings.tileFailed.addEventListener(() => latchProviderTileFailure("buildings")));
viewer.scene.primitives.add(buildings);
buildingsRef.current = buildings;
for (const attribution of buildingsEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayerRef.current, buildings, terrain, presentationRef.current);
reportProvider("buildings", "ready");
}).catch((error) => reportProvider("buildings", "error", error));
if (config.gaussianSplatsReady && config.gaussianAssetId) {
void loadEndpoint(config.gaussianAssetId).then(async (gaussianEndpoint) => {
// Give the visible base map first use of network and decoder
// capacity. Public Globe readiness events provide bounded gates;
// provider errors remain isolated and never block a later layer.
void runStagedMapProviders({
signal: providerStartupAbort.signal,
isCancelled: () => cancelled || !viewer || viewer.isDestroyed(),
loadImagery: async ({ signal }) => {
const endpoint = await loadEndpoint("2", signal);
if (endpoint.externalType !== "BING" || !endpoint.options?.url || endpoint.credentialMode !== "gateway") throw new Error("cesium_live_imagery_endpoint_invalid");
const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), {
// Cesium requires a key-shaped value to form its Bing URL.
// This public marker is stripped by Gateway before upstream.
key: "nodedc-gateway",
mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle,
tileProtocol: "https",
});
if (signal.aborted || cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(imageryProvider.errorEvent.addEventListener(() => latchProviderTileFailure("imagery")));
for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
imageryLayerRef.current = viewer.imageryLayers.addImageryProvider(imageryProvider);
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
reportProvider("imagery", "ready");
},
waitAfterImagery: async () => {
if (!viewer || viewer.isDestroyed() || !imageryLayerRef.current?.show) return;
await waitForGlobeViewportReady({
globe: viewer.scene.globe,
scene: viewer.scene,
signal: providerStartupAbort.signal,
timeoutMs: MAP_PROVIDER_STARTUP_POLICY.imageryViewportTimeoutMs,
});
},
loadTerrain: async ({ signal }) => {
const terrainEndpoint = await loadEndpoint("1", signal);
if (!terrainEndpoint.url || terrainEndpoint.credentialMode !== "gateway") throw new Error("terrain_endpoint_invalid");
const terrainResource = buildResource(terrainEndpoint.url);
const world = await CesiumTerrainProvider.fromUrl(terrainResource, { requestVertexNormals: true, requestWaterMask: true });
if (signal.aborted || cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(world.errorEvent.addEventListener(() => latchProviderTileFailure("terrain")));
terrain.world = world;
for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
onReadyChangeRef.current?.(!presentationRef.current.terrainEnabled || Boolean(terrain.world));
reportProvider("terrain", "ready");
},
waitAfterTerrain: async () => {
if (!viewer || viewer.isDestroyed() || !presentationRef.current.terrainEnabled || !terrain.world) return;
await waitForGlobeViewportReady({
globe: viewer.scene.globe,
scene: viewer.scene,
signal: providerStartupAbort.signal,
timeoutMs: MAP_PROVIDER_STARTUP_POLICY.terrainViewportTimeoutMs,
});
},
loadBuildings: async ({ signal }) => {
const buildingsEndpoint = await loadEndpoint("96188", signal);
if (buildingsEndpoint.type !== "3DTILES" || !buildingsEndpoint.url || buildingsEndpoint.credentialMode !== "gateway") throw new Error("buildings_endpoint_invalid");
const buildingsResource = buildResource(buildingsEndpoint.url);
const buildings = await Cesium3DTileset.fromUrl(buildingsResource);
if (signal.aborted || cancelled || !viewer || viewer.isDestroyed()) return;
removeProviderFailureListeners.push(buildings.tileFailed.addEventListener(() => latchProviderTileFailure("buildings")));
viewer.scene.primitives.add(buildings);
buildingsRef.current = buildings;
for (const attribution of buildingsEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayerRef.current, buildings, terrain, presentationRef.current);
reportProvider("buildings", "ready");
},
loadDeferred: config.gaussianSplatsReady && config.gaussianAssetId ? async () => {
const gaussianEndpoint = await loadEndpoint(config.gaussianAssetId!);
if (!gaussianEndpoint.url || gaussianEndpoint.credentialMode !== "gateway") throw new Error("gaussian_endpoint_invalid");
const gaussianResource = buildResource(gaussianEndpoint.url);
const gaussian = await Cesium3DTileset.fromUrl(gaussianResource);
if (cancelled || !viewer || viewer.isDestroyed()) return;
viewer.scene.primitives.add(gaussian);
}).catch(() => undefined);
}
} : undefined,
onProviderError: (provider, error) => reportProvider(provider, "error", error),
});
}
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
@@ -1250,6 +1284,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
void start();
return () => {
cancelled = true;
providerStartupAbort.abort();
stopSpiralAnimation("renderer_restarted");
onReadyChangeRef.current?.(false);
resizeObserver?.disconnect();
+44
View File
@@ -0,0 +1,44 @@
export type MapProviderStartupPolicy = Readonly<{
providerInitializationTimeoutMs: number;
imageryViewportTimeoutMs: number;
terrainViewportTimeoutMs: number;
minimumRenderFrames: number;
}>;
export type MapProviderStartupReason = "loaded" | "timeout" | "cancelled";
export type MapProviderStartupResult = "complete" | "cancelled";
export type MapProviderName = "imagery" | "terrain" | "buildings";
type CesiumEventLike<TListener extends (...args: never[]) => void> = {
addEventListener(listener: TListener): () => void;
};
export const MAP_PROVIDER_STARTUP_POLICY: MapProviderStartupPolicy;
export function waitForGlobeViewportReady(input: {
globe: {
readonly tilesLoaded: boolean;
tileLoadProgressEvent: CesiumEventLike<(pendingRequests: number) => void>;
};
scene: {
postRender: CesiumEventLike<() => void>;
requestRender(): void;
};
signal?: AbortSignal;
timeoutMs: number;
minimumRenderFrames?: number;
}): Promise<MapProviderStartupReason>;
export function runStagedMapProviders(input: {
signal?: AbortSignal;
isCancelled?: () => boolean;
providerInitializationTimeoutMs?: number;
loadImagery: (context: { signal: AbortSignal }) => void | Promise<void>;
waitAfterImagery?: () => void | Promise<void>;
loadTerrain: (context: { signal: AbortSignal }) => void | Promise<void>;
waitAfterTerrain?: () => void | Promise<void>;
loadBuildings: (context: { signal: AbortSignal }) => void | Promise<void>;
loadDeferred?: () => void | Promise<void>;
onProviderError?: (provider: MapProviderName, error: unknown) => void;
onDeferredError?: (error: unknown) => void;
}): Promise<MapProviderStartupResult>;
+141
View File
@@ -0,0 +1,141 @@
export const MAP_PROVIDER_STARTUP_POLICY = Object.freeze({
providerInitializationTimeoutMs: 8_000,
imageryViewportTimeoutMs: 6_000,
terrainViewportTimeoutMs: 5_000,
minimumRenderFrames: 2,
});
/**
* Wait until Cesium has rendered enough frames to discover the current
* viewport and the public Globe queue reports that its terrain and imagery
* are loaded. The deadline is deliberate: a slow or unavailable provider
* must never block the next independent layer.
*/
export function waitForGlobeViewportReady({
globe,
scene,
signal,
timeoutMs,
minimumRenderFrames = MAP_PROVIDER_STARTUP_POLICY.minimumRenderFrames,
}) {
if (signal?.aborted) return Promise.resolve("cancelled");
const frameTarget = Math.max(1, Math.trunc(minimumRenderFrames));
const deadlineMs = Math.max(0, Math.trunc(timeoutMs));
return new Promise((resolve) => {
let settled = false;
let renderFrames = 0;
let removeProgressListener;
let removePostRenderListener;
let timeout;
const cleanup = () => {
removeProgressListener?.();
removePostRenderListener?.();
if (timeout !== undefined) clearTimeout(timeout);
signal?.removeEventListener("abort", onAbort);
};
const finish = (reason) => {
if (settled) return;
settled = true;
cleanup();
resolve(reason);
};
const inspect = () => {
if (renderFrames >= frameTarget && globe.tilesLoaded) finish("loaded");
};
const onAbort = () => finish("cancelled");
const onTileLoadProgress = () => {
inspect();
if (!settled) scene.requestRender();
};
const onPostRender = () => {
renderFrames += 1;
inspect();
if (!settled && renderFrames < frameTarget) scene.requestRender();
};
removeProgressListener = globe.tileLoadProgressEvent.addEventListener(onTileLoadProgress);
removePostRenderListener = scene.postRender.addEventListener(onPostRender);
signal?.addEventListener("abort", onAbort, { once: true });
timeout = setTimeout(() => finish("timeout"), deadlineMs);
scene.requestRender();
});
}
/**
* Preserve independent failure domains while giving the base map first use
* of network and decode capacity. Each provider owns its error; viewport
* gates are scheduling hints and therefore never become provider failures.
*/
export async function runStagedMapProviders({
signal,
isCancelled,
providerInitializationTimeoutMs = MAP_PROVIDER_STARTUP_POLICY.providerInitializationTimeoutMs,
loadImagery,
waitAfterImagery,
loadTerrain,
waitAfterTerrain,
loadBuildings,
loadDeferred,
onProviderError,
onDeferredError,
}) {
const cancelled = () => Boolean(signal?.aborted || isCancelled?.());
const loadProvider = async (provider, load) => {
if (cancelled()) return false;
const stageAbort = new AbortController();
let timeout;
let rejectDeadline;
const abortStage = () => {
rejectDeadline?.(new Error(`${provider}_startup_cancelled`));
stageAbort.abort();
};
signal?.addEventListener("abort", abortStage, { once: true });
const deadline = new Promise((_, reject) => {
rejectDeadline = reject;
timeout = setTimeout(() => {
reject(new Error(`${provider}_startup_timeout`));
stageAbort.abort();
}, Math.max(0, Math.trunc(providerInitializationTimeoutMs)));
});
try {
await Promise.race([load({ signal: stageAbort.signal }), deadline]);
return !cancelled() && !stageAbort.signal.aborted;
} catch (error) {
if (!cancelled()) onProviderError?.(provider, error);
return false;
} finally {
if (timeout !== undefined) clearTimeout(timeout);
signal?.removeEventListener("abort", abortStage);
}
};
const waitForViewport = async (wait) => {
if (!wait || cancelled()) return;
try {
await wait();
} catch {
// A readiness gate controls ordering only. The provider's own error
// event remains the authority for availability and user diagnostics.
}
};
const imageryReady = await loadProvider("imagery", loadImagery);
if (imageryReady) await waitForViewport(waitAfterImagery);
const terrainReady = await loadProvider("terrain", loadTerrain);
if (terrainReady) await waitForViewport(waitAfterTerrain);
await loadProvider("buildings", loadBuildings);
if (loadDeferred && !cancelled()) {
try {
await loadDeferred();
} catch (error) {
if (!cancelled()) onDeferredError?.(error);
}
}
return cancelled() ? "cancelled" : "complete";
}
+2 -1
View File
@@ -11,7 +11,7 @@
"scripts": {
"build": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/map-cesium-react && npm run build --workspace @nodedc/ui-catalog",
"build:packages": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/map-cesium-react",
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:floating-position && npm run test:inspector-select && npm run test:range-control && npm run test:hgeozone-projection && npm run test:map-grid-lod && npm run test:map-filters && npm run test:map-workspace-model && npm run test:map-object-layers && npm run test:map-inspector-overlay-state && npm run test:map-reference-stations && npm run test:map-search && npm run test:map-subject-card && npm run test:map-subject-detail-profile",
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:floating-position && npm run test:inspector-select && npm run test:range-control && npm run test:hgeozone-projection && npm run test:map-provider-startup && npm run test:map-grid-lod && npm run test:map-filters && npm run test:map-workspace-model && npm run test:map-object-layers && npm run test:map-inspector-overlay-state && npm run test:map-reference-stations && npm run test:map-search && npm run test:map-subject-card && npm run test:map-subject-detail-profile",
"dev": "npm run build:packages && npm run dev --workspace @nodedc/ui-catalog",
"serve": "node server/catalog-server.mjs",
"validate:registry": "node scripts/validate-registry.mjs",
@@ -32,6 +32,7 @@
"test:map-subject-card": "node --test scripts/map-subject-card.test.mjs",
"test:map-subject-detail-profile": "node --test server/map-subject-detail-profile.test.mjs server/map-live-data-slot.test.mjs",
"test:map-cache-contract": "node --test scripts/map-cache-resource-contract.test.mjs",
"test:map-provider-startup": "node --test scripts/map-provider-startup.test.mjs",
"test:floating-position": "node --test scripts/floating-position-contract.test.mjs",
"test:inspector-select": "node --test scripts/inspector-select-contract.test.mjs",
"test:range-control": "node --test scripts/range-control-contract.test.mjs"
+163
View File
@@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import {
runStagedMapProviders,
waitForGlobeViewportReady,
} from "../apps/catalog/src/mapProviderStartup.mjs";
function eventHarness() {
const listeners = new Set();
return {
event: {
addEventListener(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
},
raise(...args) {
for (const listener of [...listeners]) listener(...args);
},
get size() {
return listeners.size;
},
};
}
test("providers start in base-map order", async () => {
const calls = [];
const result = await runStagedMapProviders({
loadImagery: () => calls.push("imagery"),
waitAfterImagery: () => calls.push("imagery-ready"),
loadTerrain: () => calls.push("terrain"),
waitAfterTerrain: () => calls.push("terrain-ready"),
loadBuildings: () => calls.push("buildings"),
loadDeferred: () => calls.push("deferred"),
});
assert.equal(result, "complete");
assert.deepEqual(calls, ["imagery", "imagery-ready", "terrain", "terrain-ready", "buildings", "deferred"]);
});
test("imagery failure is reported without blocking terrain or buildings", async () => {
const calls = [];
const errors = [];
await runStagedMapProviders({
loadImagery: () => { calls.push("imagery"); throw new Error("imagery failed"); },
waitAfterImagery: () => calls.push("imagery-ready"),
loadTerrain: () => calls.push("terrain"),
waitAfterTerrain: () => calls.push("terrain-ready"),
loadBuildings: () => calls.push("buildings"),
onProviderError: (provider, error) => errors.push([provider, error.message]),
});
assert.deepEqual(calls, ["imagery", "terrain", "terrain-ready", "buildings"]);
assert.deepEqual(errors, [["imagery", "imagery failed"]]);
});
test("terrain failure is reported without blocking buildings", async () => {
const calls = [];
const errors = [];
await runStagedMapProviders({
loadImagery: () => calls.push("imagery"),
waitAfterImagery: () => calls.push("imagery-ready"),
loadTerrain: () => { calls.push("terrain"); throw new Error("terrain failed"); },
waitAfterTerrain: () => calls.push("terrain-ready"),
loadBuildings: () => calls.push("buildings"),
onProviderError: (provider, error) => errors.push([provider, error.message]),
});
assert.deepEqual(calls, ["imagery", "imagery-ready", "terrain", "buildings"]);
assert.deepEqual(errors, [["terrain", "terrain failed"]]);
});
test("cancellation prevents all later stages", async () => {
const controller = new AbortController();
const calls = [];
const result = await runStagedMapProviders({
signal: controller.signal,
loadImagery: () => { calls.push("imagery"); controller.abort(); },
waitAfterImagery: () => calls.push("imagery-ready"),
loadTerrain: () => calls.push("terrain"),
loadBuildings: () => calls.push("buildings"),
});
assert.equal(result, "cancelled");
assert.deepEqual(calls, ["imagery"]);
});
test("a stalled provider times out without inserting late or blocking later stages", async () => {
const calls = [];
const errors = [];
let releaseImagery;
const stalledImagery = new Promise((resolve) => { releaseImagery = resolve; });
await runStagedMapProviders({
providerInitializationTimeoutMs: 1,
loadImagery: async ({ signal }) => {
calls.push("imagery-start");
await stalledImagery;
if (!signal.aborted) calls.push("imagery-insert");
},
loadTerrain: () => calls.push("terrain"),
loadBuildings: () => calls.push("buildings"),
onProviderError: (provider, error) => errors.push([provider, error.message]),
});
releaseImagery();
await Promise.resolve();
assert.deepEqual(calls, ["imagery-start", "terrain", "buildings"]);
assert.deepEqual(errors, [["imagery", "imagery_startup_timeout"]]);
});
test("viewport readiness waits for discovery frames and cleans listeners", async () => {
const progress = eventHarness();
const postRender = eventHarness();
const globe = { tilesLoaded: true, tileLoadProgressEvent: progress.event };
const scene = { postRender: postRender.event, requestRender() {} };
let settled = false;
const ready = waitForGlobeViewportReady({ globe, scene, timeoutMs: 100, minimumRenderFrames: 2 })
.then((reason) => { settled = true; return reason; });
postRender.raise();
await Promise.resolve();
assert.equal(settled, false);
postRender.raise();
assert.equal(await ready, "loaded");
assert.equal(progress.size, 0);
assert.equal(postRender.size, 0);
});
test("viewport readiness times out and aborts without leaking listeners", async () => {
const timeoutProgress = eventHarness();
const timeoutPostRender = eventHarness();
assert.equal(await waitForGlobeViewportReady({
globe: { tilesLoaded: false, tileLoadProgressEvent: timeoutProgress.event },
scene: { postRender: timeoutPostRender.event, requestRender() {} },
timeoutMs: 1,
}), "timeout");
assert.equal(timeoutProgress.size, 0);
assert.equal(timeoutPostRender.size, 0);
const abortProgress = eventHarness();
const abortPostRender = eventHarness();
const controller = new AbortController();
const pending = waitForGlobeViewportReady({
globe: { tilesLoaded: false, tileLoadProgressEvent: abortProgress.event },
scene: { postRender: abortPostRender.event, requestRender() {} },
signal: controller.signal,
timeoutMs: 100,
});
controller.abort();
assert.equal(await pending, "cancelled");
assert.equal(abortProgress.size, 0);
assert.equal(abortPostRender.size, 0);
});
test("the live renderer uses the staged provider coordinator", async () => {
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
assert.match(renderer, /runStagedMapProviders\(\{/);
assert.match(renderer, /waitForGlobeViewportReady\(\{/);
assert.match(renderer, /providerStartupAbort\.abort\(\)/);
assert.doesNotMatch(renderer, /Do not serialize provider startup/);
});