feat(foundry): productionize Cesium Map Page and platform runtime
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { createServer as createHttpServer, request as httpRequest } from "node:http";
|
||||
import { createServer as createNetServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const foundryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
test("Map Gateway BFF caches only confirmed hits, honors validators and aborts abandoned streams", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "nodedc-foundry-map-proxy-"));
|
||||
const gatewayPort = await freePort();
|
||||
const foundryPort = await freePort();
|
||||
let conditionalHeader = "";
|
||||
let resolveSlowClosed;
|
||||
const slowClosed = new Promise((resolveClosed) => { resolveSlowClosed = resolveClosed; });
|
||||
let resolveIdleClosed;
|
||||
const idleClosed = new Promise((resolveClosed) => { resolveIdleClosed = resolveClosed; });
|
||||
const gateway = createHttpServer((request, response) => {
|
||||
const url = new URL(request.url || "/", "http://gateway.local");
|
||||
if (url.pathname === "/healthz") {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ cache: { persistent: true, entries: 1, bytes: 4 } }));
|
||||
return;
|
||||
}
|
||||
if (url.pathname.startsWith("/api/map/ion/assets/")) {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ ok: true, credentialMode: "gateway" }));
|
||||
return;
|
||||
}
|
||||
if (url.pathname !== "/api/map/cache") {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
const target = String(url.searchParams.get("url") || "");
|
||||
conditionalHeader = String(request.headers["if-none-match"] || conditionalHeader);
|
||||
if (target.includes("/provider-error")) {
|
||||
response.writeHead(401, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "provider_unauthorized" }));
|
||||
return;
|
||||
}
|
||||
const headers = {
|
||||
"content-type": "application/octet-stream",
|
||||
etag: '"tile-v1"',
|
||||
"x-nodedc-map-cache": target.includes("nodedc_cache_refresh=1") ? "live-stale-upstream-error" : "live-cache-hit",
|
||||
};
|
||||
if (target.includes("/headers-slow")) {
|
||||
const timer = setTimeout(() => {
|
||||
if (response.destroyed) return;
|
||||
response.writeHead(200, { ...headers, "content-length": "4" });
|
||||
response.end("tile");
|
||||
}, 180);
|
||||
response.once("close", () => clearTimeout(timer));
|
||||
return;
|
||||
}
|
||||
if (target.includes("/body-idle")) {
|
||||
response.writeHead(200, headers);
|
||||
response.write("start");
|
||||
const timer = setTimeout(() => response.end("late"), 500);
|
||||
response.once("close", () => {
|
||||
clearTimeout(timer);
|
||||
resolveIdleClosed();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (target.includes("/progress")) {
|
||||
response.writeHead(200, { ...headers, "content-length": String(8 * 256) });
|
||||
let chunks = 0;
|
||||
const interval = setInterval(() => {
|
||||
chunks += 1;
|
||||
response.write(Buffer.alloc(256, chunks));
|
||||
if (chunks === 8) {
|
||||
clearInterval(interval);
|
||||
response.end();
|
||||
}
|
||||
}, 30);
|
||||
response.once("close", () => clearInterval(interval));
|
||||
return;
|
||||
}
|
||||
if (target.includes("/slow")) {
|
||||
response.writeHead(200, headers);
|
||||
response.write(Buffer.alloc(1024, 1));
|
||||
const interval = setInterval(() => response.write(Buffer.alloc(1024, 2)), 20);
|
||||
response.once("close", () => {
|
||||
clearInterval(interval);
|
||||
resolveSlowClosed();
|
||||
});
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, { ...headers, "content-length": "4" });
|
||||
response.end("tile");
|
||||
});
|
||||
let foundry;
|
||||
|
||||
try {
|
||||
gateway.listen(gatewayPort, "127.0.0.1");
|
||||
await once(gateway, "listening");
|
||||
foundry = spawn(process.execPath, ["server/catalog-server.mjs"], {
|
||||
cwd: foundryRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "development",
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(foundryPort),
|
||||
FOUNDRY_RUNTIME_DIR: root,
|
||||
NODEDC_FOUNDRY_AUTH_REQUIRED: "false",
|
||||
NODEDC_MAP_GATEWAY_INTERNAL_URL: `http://127.0.0.1:${gatewayPort}`,
|
||||
NODEDC_MAP_GATEWAY_HEADERS_TIMEOUT_MS: "100",
|
||||
NODEDC_MAP_GATEWAY_BODY_IDLE_TIMEOUT_MS: "80",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
await waitForService(foundryPort, foundry);
|
||||
const base = `http://127.0.0.1:${foundryPort}`;
|
||||
const proxied = (target) => `${base}/api/map-gateway/api/map/cache?url=${encodeURIComponent(target)}`;
|
||||
|
||||
const hit = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin"));
|
||||
assert.equal(hit.status, 200);
|
||||
assert.equal(await hit.text(), "tile");
|
||||
assert.equal(hit.headers.get("cache-control"), "private, max-age=300, stale-while-revalidate=60");
|
||||
assert.equal(hit.headers.get("vary"), "Cookie");
|
||||
assert.equal(hit.headers.get("etag"), '"tile-v1"');
|
||||
|
||||
const conditional = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin"), {
|
||||
headers: { "if-none-match": 'W/"tile-v1"' },
|
||||
});
|
||||
assert.equal(conditional.status, 304);
|
||||
assert.equal(conditionalHeader, 'W/"tile-v1"');
|
||||
assert.equal(conditional.headers.get("cache-control"), "private, max-age=300, stale-while-revalidate=60");
|
||||
|
||||
const refresh = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin?nodedc_cache_refresh=1"));
|
||||
assert.equal(refresh.status, 200);
|
||||
assert.equal(refresh.headers.get("cache-control"), "no-store");
|
||||
|
||||
const providerError = await fetch(proxied("https://assets.ion.cesium.com/1/provider-error"));
|
||||
assert.equal(providerError.status, 401);
|
||||
assert.equal(providerError.headers.get("cache-control"), "no-store");
|
||||
|
||||
const endpoint = await fetch(`${base}/api/map-gateway/api/map/ion/assets/1/endpoint`);
|
||||
assert.equal(endpoint.status, 200);
|
||||
assert.equal(endpoint.headers.get("cache-control"), "no-store");
|
||||
const health = await fetch(`${base}/api/map-gateway/healthz`);
|
||||
assert.equal(health.status, 200);
|
||||
assert.equal(health.headers.get("cache-control"), "no-store");
|
||||
|
||||
const progressing = await fetch(proxied("https://assets.ion.cesium.com/1/progress"));
|
||||
assert.equal(progressing.status, 200);
|
||||
assert.equal((await progressing.arrayBuffer()).byteLength, 8 * 256);
|
||||
|
||||
const headersTimeout = await fetch(proxied("https://assets.ion.cesium.com/1/headers-slow"));
|
||||
assert.equal(headersTimeout.status, 504);
|
||||
assert.equal((await headersTimeout.json()).error, "map_gateway_headers_timeout");
|
||||
|
||||
const idle = await fetch(proxied("https://assets.ion.cesium.com/1/body-idle"));
|
||||
assert.equal(idle.status, 200);
|
||||
await assert.rejects(idle.arrayBuffer());
|
||||
await Promise.race([
|
||||
idleClosed,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("upstream_body_idle_abort_timeout")), 2_000)),
|
||||
]);
|
||||
|
||||
await abortAfterFirstChunk(proxied("https://assets.ion.cesium.com/1/slow"));
|
||||
await Promise.race([
|
||||
slowClosed,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("upstream_abort_timeout")), 2_000)),
|
||||
]);
|
||||
const stillHealthy = await fetch(`${base}/healthz`);
|
||||
assert.equal(stillHealthy.status, 200);
|
||||
} finally {
|
||||
await stop(foundry);
|
||||
if (gateway.listening) {
|
||||
gateway.close();
|
||||
await once(gateway, "close");
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function abortAfterFirstChunk(url) {
|
||||
return new Promise((resolveAbort, rejectAbort) => {
|
||||
const request = httpRequest(url, (response) => {
|
||||
response.once("data", () => {
|
||||
response.destroy();
|
||||
request.destroy();
|
||||
resolveAbort();
|
||||
});
|
||||
});
|
||||
request.once("error", (error) => {
|
||||
if (error?.code === "ECONNRESET") resolveAbort();
|
||||
else rejectAbort(error);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function freePort() {
|
||||
const server = createNetServer();
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
assert(address && typeof address === "object");
|
||||
const port = address.port;
|
||||
server.close();
|
||||
await once(server, "close");
|
||||
return port;
|
||||
}
|
||||
|
||||
async function waitForService(port, child) {
|
||||
let output = "";
|
||||
child.stderr.on("data", (chunk) => { output += String(chunk); });
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
if (child.exitCode !== null) throw new Error(`foundry_exited:${child.exitCode}:${output}`);
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/healthz`);
|
||||
if (response.ok) return;
|
||||
} catch { /* service is still starting */ }
|
||||
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
|
||||
}
|
||||
throw new Error(`foundry_start_timeout:${output}`);
|
||||
}
|
||||
|
||||
async function stop(child) {
|
||||
if (!child || child.exitCode !== null) return;
|
||||
child.kill("SIGTERM");
|
||||
await once(child, "exit").catch(() => undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user