From 9e5ef78f914ca790e6a794b67c57fc45dda92c16 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 2 Oct 2026 17:43:10 -0700 Subject: [PATCH] Embed the files Cesium's workers load in the panel's workers script The ground-polyline worker loads approximateTerrainHeights.json itself, and a worker's requests go to the panel page's own site, where they failed with 404 and stopped rendering once ground-clamped lines such as military installations appeared. The panel build now embeds that file in the workers script, which answers the worker's request from memory. --- build/panel.js | 63 +++++++++++++++++++++++++++++++++ scripts/build-panel.mjs | 12 ++++++- src/tooling/panelBuild.test.mjs | 44 +++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/build/panel.js b/build/panel.js index b68df95ea..af5b3e709 100644 --- a/build/panel.js +++ b/build/panel.js @@ -101,3 +101,66 @@ export function panelBuildPlugin({ outDir = PANEL_OUT_DIR } = {}) { }, }; } + +/** + * Cesium files its workers load themselves, as paths under Cesium's base. + * Workers request them from the panel page's own site, which has none of + * the app's files, so the panel build embeds them in the workers script. + */ +export const PANEL_WORKER_FILES = Object.freeze([ + 'Assets/approximateTerrainHeights.json', +]); + +/** + * A script, run in each worker before Cesium's, that answers the worker's + * requests for `files` (Cesium-relative path → text) from memory and lets + * every other request through. + */ +export function workerFilesPrelude(files) { + const answer = (FILES) => { + const find = (url) => { + const path = String(url).split(/[?#]/)[0]; + for (const name of Object.keys(FILES)) + if (path.endsWith(`/${name}`)) return FILES[name]; + return null; + }; + const xhr = XMLHttpRequest.prototype; + const { open, send, setRequestHeader } = xhr; + xhr.open = function (method, url, ...rest) { + this.__gevFile = find(url); + if (this.__gevFile === null) return open.call(this, method, url, ...rest); + }; + xhr.setRequestHeader = function (...args) { + if (this.__gevFile === null) return setRequestHeader.apply(this, args); + }; + xhr.send = function (body) { + const text = this.__gevFile; + if (text === null || text === undefined) return send.call(this, body); + const type = this.responseType; + const response = + type === 'json' + ? JSON.parse(text) + : type === 'arraybuffer' + ? new TextEncoder().encode(text).buffer + : text; + const state = { + readyState: 4, + status: 200, + statusText: 'OK', + response, + responseText: typeof response === 'string' ? response : '', + getAllResponseHeaders: () => 'content-type: application/json\r\n', + getResponseHeader: (name) => + name.toLowerCase() === 'content-type' ? 'application/json' : null, + }; + for (const [name, value] of Object.entries(state)) + Object.defineProperty(this, name, { configurable: true, value }); + setTimeout(() => { + this.dispatchEvent(new Event('readystatechange')); + this.dispatchEvent(new ProgressEvent('load')); + this.dispatchEvent(new ProgressEvent('loadend')); + }); + }; + }; + return `(${answer})(${JSON.stringify(files)});\n`; +} diff --git a/scripts/build-panel.mjs b/scripts/build-panel.mjs index ad4cc8bd2..82d4c9043 100644 --- a/scripts/build-panel.mjs +++ b/scripts/build-panel.mjs @@ -4,7 +4,7 @@ * See build/panel.js. */ -import { readdir, rename, rm, writeFile } from 'node:fs/promises'; +import { readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { build as esbuild } from 'esbuild'; @@ -13,8 +13,10 @@ import standaloneConfig from '../server/standalone/vite.config.js'; import { PANEL_BASE, PANEL_OUT_DIR, + PANEL_WORKER_FILES, PANEL_WORKERS_PATH, panelBuildConfig, + workerFilesPrelude, workersEntrySource, } from '../build/panel.js'; @@ -49,4 +51,12 @@ await esbuild({ logLevel: 'warning', }); await rm(entry); +const files = {}; +for (const name of PANEL_WORKER_FILES) + files[name] = await readFile(join(outDir, 'cesium', name), 'utf8'); +const workers = join(outDir, PANEL_WORKERS_PATH); +await writeFile( + workers, + workerFilesPrelude(files) + (await readFile(workers, 'utf8')), +); console.log(`Panel build written to ${PANEL_OUT_DIR}`); diff --git a/src/tooling/panelBuild.test.mjs b/src/tooling/panelBuild.test.mjs index 830be7e95..65bf35bb9 100644 --- a/src/tooling/panelBuild.test.mjs +++ b/src/tooling/panelBuild.test.mjs @@ -9,6 +9,7 @@ import { PANEL_WORKERS_PATH, panelBuildConfig, panelBuildPlugin, + workerFilesPrelude, workersEntrySource, } from '../../build/panel.js'; @@ -81,3 +82,46 @@ test('the dev server serves the panel build and nothing outside it', async () => await rm(root, { recursive: true, force: true }); } }); + +test("Cesium's workers get embedded files from memory and other requests from the network", async () => { + const sent = []; + class FakeRequest extends EventTarget { + open(method, url) { + this.url = url; + } + setRequestHeader() {} + send() { + sent.push(this.url); + } + } + const saved = { + XMLHttpRequest: globalThis.XMLHttpRequest, + ProgressEvent: globalThis.ProgressEvent, + }; + globalThis.XMLHttpRequest = FakeRequest; + globalThis.ProgressEvent ??= class extends Event {}; + try { + new Function(workerFilesPrelude({ 'Assets/heights.json': '{"a":1}' }))(); + const embedded = new XMLHttpRequest(); + embedded.open( + 'GET', + 'https://panel.example/panel/cesium/Assets/heights.json?v=1', + ); + embedded.setRequestHeader('Accept', 'application/json'); + embedded.responseType = 'text'; + const loaded = new Promise((resolve) => + embedded.addEventListener('load', resolve), + ); + embedded.send(); + await loaded; + assert.equal(embedded.status, 200); + assert.equal(embedded.response, '{"a":1}'); + const other = new XMLHttpRequest(); + other.open('GET', 'https://tiles.example/1.png'); + other.send(); + assert.deepEqual(sent, ['https://tiles.example/1.png']); + } finally { + globalThis.XMLHttpRequest = saved.XMLHttpRequest; + globalThis.ProgressEvent = saved.ProgressEvent; + } +});