Files
NODEDC_DESIGN_GUIDELINE/packages/map-cesium-react/src/runtime.ts
T

212 lines
5.9 KiB
TypeScript

import type {
MapCacheIntent,
MapGatewayError,
MapProviderId,
MapProviderState,
MapRuntimeConfiguration,
MapRuntimeState,
} from "./contracts.js";
import {
CESIUM_RENDERER_VERSION,
MAP_PAGE_VERSION,
MAP_RUNTIME_SCHEMA_VERSION,
} from "./contracts.js";
const SAFE_ERROR_CODE = /^[a-z][a-z0-9_]{0,95}$/;
const PROVIDER_IDS: readonly MapProviderId[] = ["imagery", "terrain", "buildings"];
const CREDENTIAL_QUERY_KEYS = new Set([
"access_token",
"access-token",
"authorization",
"credential",
"key",
"signature",
"sig",
"token",
]);
export class MapRuntimeError extends Error {
readonly code: string;
constructor(code: string) {
super(code);
this.name = "MapRuntimeError";
this.code = SAFE_ERROR_CODE.test(code) ? code : "map_runtime_error";
}
}
export function initialMapRuntimeState(): MapRuntimeState {
return {
phase: "loading",
providers: {
imagery: { phase: "idle" },
terrain: { phase: "idle" },
buildings: { phase: "idle" },
},
};
}
export function withProviderState(
current: MapRuntimeState,
providerId: MapProviderId,
providerState: MapProviderState,
): MapRuntimeState {
const providers = {
...current.providers,
[providerId]: providerState,
};
const enabled = PROVIDER_IDS.map((id) => providers[id]).filter(
(state) => state.phase !== "disabled",
);
const readyCount = enabled.filter((state) => state.phase === "ready").length;
const errorCount = enabled.filter((state) => state.phase === "error").length;
const loadingCount = enabled.filter((state) =>
["idle", "loading"].includes(state.phase),
).length;
const phase =
loadingCount > 0
? "loading"
: errorCount === 0
? "ready"
: readyCount > 0
? "degraded"
: "gateway-unavailable";
return {
phase,
providers,
...(errorCount > 0
? {
code:
enabled.find((state) => state.phase === "error")?.code ??
"map_provider_unavailable",
}
: {}),
};
}
export async function fetchMapRuntimeConfiguration(
path: string,
signal: AbortSignal,
): Promise<MapRuntimeConfiguration> {
const document = await fetchSameOriginJson(path, signal);
if (
document.schema_version !== MAP_RUNTIME_SCHEMA_VERSION ||
document.map_page_version !== MAP_PAGE_VERSION ||
!isRecord(document.renderer) ||
document.renderer.id !== "cesium" ||
document.renderer.version !== CESIUM_RENDERER_VERSION ||
!isRecord(document.gateway) ||
document.gateway.configured !== true ||
!isSafeSameOriginPath(document.gateway.health_url) ||
!isSafeSameOriginPath(document.gateway.asset_endpoint_template) ||
!isSafeSameOriginPath(document.gateway.cache_proxy_prefix) ||
!isRecord(document.assets) ||
document.assets.imagery !== 2 ||
document.assets.terrain !== 1 ||
document.assets.buildings !== 96188 ||
(document.sandbox !== undefined &&
(!isRecord(document.sandbox) ||
typeof document.sandbox.hide_credit_overlay !== "boolean"))
) {
throw new MapRuntimeError("map_runtime_contract_mismatch");
}
return document as MapRuntimeConfiguration;
}
export async function fetchSameOriginJson(
path: string,
signal: AbortSignal,
): Promise<Record<string, unknown>> {
if (!isSafeSameOriginPath(path)) {
throw new MapRuntimeError("map_runtime_cross_origin_forbidden");
}
const response = await fetch(path, {
method: "GET",
credentials: "same-origin",
cache: "no-store",
signal,
headers: { Accept: "application/json" },
});
const document = await response.json().catch(() => null);
if (!response.ok) {
throw new MapRuntimeError(readSafeErrorCode(document));
}
if (!isRecord(document)) {
throw new MapRuntimeError("map_runtime_invalid_response");
}
return document;
}
export function assetEndpointPath(
configuration: MapRuntimeConfiguration,
providerId: MapProviderId,
): string {
const assetId = configuration.assets[providerId];
return configuration.gateway.asset_endpoint_template.replace(
"{asset_id}",
String(assetId),
);
}
export function applyCacheIntent(
rawUrl: string,
intent: MapCacheIntent,
refresh: boolean,
): string {
const url = new URL(rawUrl);
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
throw new MapRuntimeError("map_provider_url_rejected");
}
for (const key of url.searchParams.keys()) {
if (CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())) {
throw new MapRuntimeError("map_provider_credential_rejected");
}
}
if (!intent.enabled) {
url.searchParams.set("nodedc_cache_mode", "passthrough");
} else {
url.searchParams.set("nodedc_cache_profile", "live");
if (!intent.no_overwrite || refresh) {
url.searchParams.set("nodedc_cache_refresh", "1");
}
}
return url.toString();
}
export function isSafeSameOriginPath(value: unknown): value is string {
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) {
return false;
}
try {
const resolved = new URL(value, window.location.origin);
return resolved.origin === window.location.origin;
} catch {
return false;
}
}
export function readSafeErrorCode(value: unknown): string {
if (isRecord(value)) {
const direct = value.code;
if (typeof direct === "string" && SAFE_ERROR_CODE.test(direct)) {
return direct;
}
const error = value.error;
if (isRecord(error) && typeof error.code === "string" && SAFE_ERROR_CODE.test(error.code)) {
return error.code;
}
if (typeof error === "string" && SAFE_ERROR_CODE.test(error)) {
return error;
}
}
return "map_gateway_request_failed";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function asMapGatewayError(value: unknown): MapGatewayError | null {
return isRecord(value) ? (value as MapGatewayError) : null;
}