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.
This commit is contained in:
Sameh Khamis committed 2026-10-02 18:29:42 -07:00
1 parent d4311a9d54
commit 9e5ef78f91
3 files changed
+118 -1

No files matched your search

+63
View File
@@ -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`;
}
+11 -1
View File
@@ -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}`);
+44
View File
@@ -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;
}
});