style: format satellite and launch provider modules

This commit is contained in:
Sameh Khamis committed 2026-09-11 21:22:41 -07:00
1 parent 9a4b98700e
commit 290afd808d
5 files changed
+129 -35

No files matched your search

+6 -1
View File
@@ -52,5 +52,10 @@
"server/providers/places/google.js", "server/providers/places/google.js",
"server/providers/places/routes.js", "server/providers/places/routes.js",
"src/data/placeProviderPayloads.js", "src/data/placeProviderPayloads.js",
"src/tooling/placeProviders.test.mjs" "src/tooling/placeProviders.test.mjs",
"server/providers/space.js",
"server/providers/space/celestrak.js",
"server/providers/space/launch-library.js",
"src/data/spaceProviderRequests.js",
"src/tooling/spaceProviders.test.mjs"
] ]
+5 -1
View File
@@ -1,3 +1,7 @@
/** Satellite and launch-feed middleware for Node development servers. */ /** Satellite and launch-feed middleware for Node development servers. */
export { celestrakProxy } from './space/celestrak.js'; export { celestrakProxy } from './space/celestrak.js';
export { rocketLaunchesProxy, LL2_CACHE_TTL_MS, launchLibraryRequestHeaders } from './space/launch-library.js'; export {
rocketLaunchesProxy,
LL2_CACHE_TTL_MS,
launchLibraryRequestHeaders,
} from './space/launch-library.js';
+24 -8
View File
@@ -31,8 +31,11 @@ export function celestrakProxy() {
async function readDisk(group) { async function readDisk(group) {
try { try {
const parsed = JSON.parse(await fsp.readFile(diskPath(group), 'utf8')); const parsed = JSON.parse(await fsp.readFile(diskPath(group), 'utf8'));
if (typeof parsed?.body === 'string' && Number.isFinite(parsed?.at)) return parsed; if (typeof parsed?.body === 'string' && Number.isFinite(parsed?.at))
} catch { /* no disk cache yet */ } return parsed;
} catch {
/* no disk cache yet */
}
return null; return null;
} }
@@ -51,7 +54,10 @@ export function celestrakProxy() {
signal: AbortSignal.timeout(20000), signal: AbortSignal.timeout(20000),
// CelesTrak 403s bulk groups (e.g. `active`) unless the request carries a // CelesTrak 403s bulk groups (e.g. `active`) unless the request carries a
// descriptive User-Agent with a contact point. // descriptive User-Agent with a contact point.
headers: { 'User-Agent': 'gods-eye-view-celestrak-proxy/1.0 (+https://github.com/bilawalsidhu/gods-eye-view)' }, headers: {
'User-Agent':
'gods-eye-view-celestrak-proxy/1.0 (+https://github.com/bilawalsidhu/gods-eye-view)',
},
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.text(); const body = await res.text();
@@ -64,7 +70,9 @@ export function celestrakProxy() {
name: 'celestrak-proxy', name: 'celestrak-proxy',
configureServer(server) { configureServer(server) {
server.middlewares.use('/api/celestrak', async (req, res) => { server.middlewares.use('/api/celestrak', async (req, res) => {
const group = String(req.url || '').replace(/^\//, '').split('?')[0]; const group = String(req.url || '')
.replace(/^\//, '')
.split('?')[0];
if (!/^[a-z0-9-]+$/i.test(group)) { if (!/^[a-z0-9-]+$/i.test(group)) {
res.writeHead(400, { 'Content-Type': 'text/plain' }); res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('invalid group'); res.end('invalid group');
@@ -75,7 +83,10 @@ export function celestrakProxy() {
// went out routing into the catch's send): writeHead after headersSent // went out routing into the catch's send): writeHead after headersSent
// throws "Cannot set headers after they are sent". // throws "Cannot set headers after they are sent".
if (res.headersSent) return; if (res.headersSent) return;
res.writeHead(status, { 'Content-Type': 'text/plain', 'x-tle-cache': cacheStatus }); res.writeHead(status, {
'Content-Type': 'text/plain',
'x-tle-cache': cacheStatus,
});
res.end(body); res.end(body);
}; };
try { try {
@@ -91,17 +102,22 @@ export function celestrakProxy() {
} }
// Stale or missing → refresh, single-flight per group. // Stale or missing → refresh, single-flight per group.
if (!inflight.has(group)) { if (!inflight.has(group)) {
inflight.set(group, fetchUpstream(group) inflight.set(
group,
fetchUpstream(group)
.then(async (fresh) => { .then(async (fresh) => {
mem.set(group, fresh); mem.set(group, fresh);
await writeDisk(group, fresh); await writeDisk(group, fresh);
return fresh; return fresh;
}) })
.catch((err) => { .catch((err) => {
console.warn('[celestrak-proxy] refresh failed — serving cache if any'); console.warn(
'[celestrak-proxy] refresh failed — serving cache if any',
);
return null; return null;
}) })
.finally(() => inflight.delete(group))); .finally(() => inflight.delete(group)),
);
} }
const fresh = await inflight.get(group); const fresh = await inflight.get(group);
if (fresh) { if (fresh) {
+28 -8
View File
@@ -1,6 +1,9 @@
import path from 'node:path'; import path from 'node:path';
import { promises as fsp } from 'node:fs'; import { promises as fsp } from 'node:fs';
import { readResponseTextCapped, coalesceProxyRequest } from '../common/http.js'; import {
readResponseTextCapped,
coalesceProxyRequest,
} from '../common/http.js';
import { launchLibraryRecentUrl } from '../../../src/data/spaceProviderRequests.js'; import { launchLibraryRecentUrl } from '../../../src/data/spaceProviderRequests.js';
export const LL2_CACHE_TTL_MS = 15 * 60_000; export const LL2_CACHE_TTL_MS = 15 * 60_000;
@@ -19,7 +22,11 @@ export function rocketLaunchesProxy() {
const ttlMs = LL2_CACHE_TTL_MS; const ttlMs = LL2_CACHE_TTL_MS;
const maxResponseBytes = 12 * 1024 * 1024; const maxResponseBytes = 12 * 1024 * 1024;
const maxDiskCacheBytes = 24 * 1024 * 1024; const maxDiskCacheBytes = 24 * 1024 * 1024;
const cachePath = path.join(process.cwd(), '.gev-cache', 'launch-library-2-v2.3.json'); const cachePath = path.join(
process.cwd(),
'.gev-cache',
'launch-library-2-v2.3.json',
);
let cache = null; let cache = null;
let diskLoaded = false; let diskLoaded = false;
const inFlight = new Map(); const inFlight = new Map();
@@ -29,13 +36,16 @@ export function rocketLaunchesProxy() {
diskLoaded = true; diskLoaded = true;
try { try {
const stat = await fsp.stat(cachePath); const stat = await fsp.stat(cachePath);
if (stat.size > maxDiskCacheBytes) throw new Error('cache file too large'); if (stat.size > maxDiskCacheBytes)
throw new Error('cache file too large');
const parsed = JSON.parse(await fsp.readFile(cachePath, 'utf8')); const parsed = JSON.parse(await fsp.readFile(cachePath, 'utf8'));
if (Number.isFinite(parsed?.at) && typeof parsed?.body === 'string') { if (Number.isFinite(parsed?.at) && typeof parsed?.body === 'string') {
const body = JSON.parse(parsed.body); const body = JSON.parse(parsed.body);
if (Array.isArray(body?.results)) cache = parsed; if (Array.isArray(body?.results)) cache = parsed;
} }
} catch { /* first run or invalid cache */ } } catch {
/* first run or invalid cache */
}
} }
async function saveDiskCache(entry) { async function saveDiskCache(entry) {
@@ -70,7 +80,8 @@ export function rocketLaunchesProxy() {
throw error; throw error;
} }
const parsed = JSON.parse(body); const parsed = JSON.parse(body);
if (!Array.isArray(parsed?.results)) throw new Error('malformed upstream response'); if (!Array.isArray(parsed?.results))
throw new Error('malformed upstream response');
const fresh = { at: Date.now(), body }; const fresh = { at: Date.now(), body };
cache = fresh; cache = fresh;
void saveDiskCache(fresh); void saveDiskCache(fresh);
@@ -90,14 +101,23 @@ export function rocketLaunchesProxy() {
return; return;
} }
const stale = cache; const stale = cache;
const request = coalesceProxyRequest(inFlight, 'recent-launches', refreshUpstream); const request = coalesceProxyRequest(
inFlight,
'recent-launches',
refreshUpstream,
);
try { try {
const fresh = await request.promise; const fresh = await request.promise;
send(res, 200, fresh.body, request.shared ? 'INFLIGHT' : 'MISS'); send(res, 200, fresh.body, request.shared ? 'INFLIGHT' : 'MISS');
} catch (error) { } catch (error) {
// Log only a bounded status, never upstream bodies, URLs, or credentials. // Log only a bounded status, never upstream bodies, URLs, or credentials.
const status = Number.isInteger(error?.upstreamStatus) ? error.upstreamStatus : 502; const status = Number.isInteger(error?.upstreamStatus)
if (!request.shared) console.warn(`[launch-library-proxy] refresh failed (HTTP ${status})${stale ? ' — serving stale cache' : ''}`); ? error.upstreamStatus
: 502;
if (!request.shared)
console.warn(
`[launch-library-proxy] refresh failed (HTTP ${status})${stale ? ' — serving stale cache' : ''}`,
);
if (stale) { if (stale) {
send(res, 200, stale.body, 'STALE-ERROR'); send(res, 200, stale.body, 'STALE-ERROR');
return; return;
+66 -17
View File
@@ -1,22 +1,48 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { promises as fsp } from 'node:fs'; import { promises as fsp } from 'node:fs';
import { celestrakProxy, rocketLaunchesProxy, launchLibraryRequestHeaders, LL2_CACHE_TTL_MS } from 'gods-eye-view/server/providers/space'; import {
import { celestrakTleUrl, launchLibraryRecentUrl } from 'gods-eye-view/sources/space'; celestrakProxy,
rocketLaunchesProxy,
launchLibraryRequestHeaders,
LL2_CACHE_TTL_MS,
} from 'gods-eye-view/server/providers/space';
import {
celestrakTleUrl,
launchLibraryRecentUrl,
} from 'gods-eye-view/sources/space';
import * as compatibility from '../../server/providers/local.js'; import * as compatibility from '../../server/providers/local.js';
function install(plugin, preview = false) { function install(plugin, preview = false) {
const routes = new Map(); const routes = new Map();
plugin[preview ? 'configurePreviewServer' : 'configureServer']({ middlewares: { use(route, handler) { routes.set(route, handler); } } }); plugin[preview ? 'configurePreviewServer' : 'configureServer']({
middlewares: {
use(route, handler) {
routes.set(route, handler);
},
},
});
return async (route, url = '/', method = 'GET') => { return async (route, url = '/', method = 'GET') => {
const res = { headersSent: false, writeHead(status, headers) { Object.assign(this, { status, headers, headersSent: true }); }, end(body) { this.body = body; } }; const res = {
headersSent: false,
writeHead(status, headers) {
Object.assign(this, { status, headers, headersSent: true });
},
end(body) {
this.body = body;
},
};
await routes.get(route)({ url, method }, res); await routes.get(route)({ url, method }, res);
return res; return res;
}; };
} }
function isolateDisk(t) { function isolateDisk(t) {
t.mock.method(fsp, 'readFile', async () => { throw Error('no cache'); }); t.mock.method(fsp, 'readFile', async () => {
t.mock.method(fsp, 'stat', async () => { throw Error('no cache'); }); throw Error('no cache');
});
t.mock.method(fsp, 'stat', async () => {
throw Error('no cache');
});
t.mock.method(fsp, 'mkdir', async () => {}); t.mock.method(fsp, 'mkdir', async () => {});
t.mock.method(fsp, 'writeFile', async () => {}); t.mock.method(fsp, 'writeFile', async () => {});
} }
@@ -31,12 +57,20 @@ test('portable requests keep fixed origins, encode group data and preserve the 3
const url = launchLibraryRecentUrl(end); const url = launchLibraryRecentUrl(end);
assert.equal(url.origin, 'https://ll.thespacedevs.com'); assert.equal(url.origin, 'https://ll.thespacedevs.com');
assert.equal(url.pathname, '/2.3.0/launches/'); assert.equal(url.pathname, '/2.3.0/launches/');
assert.deepEqual(Object.fromEntries(url.searchParams), { net__gte: '2026-01-30T12:34:56.000Z', net__lte: '2026-03-01T12:34:56.000Z', limit: '100', mode: 'detailed' }); assert.deepEqual(Object.fromEntries(url.searchParams), {
net__gte: '2026-01-30T12:34:56.000Z',
net__lte: '2026-03-01T12:34:56.000Z',
limit: '100',
mode: 'detailed',
});
assert.equal(end.toISOString(), '2026-03-01T12:34:56.000Z'); assert.equal(end.toISOString(), '2026-03-01T12:34:56.000Z');
}); });
test('compatibility exports retain the same LL2 header helper and TTL', () => { test('compatibility exports retain the same LL2 header helper and TTL', () => {
assert.equal(compatibility.launchLibraryRequestHeaders, launchLibraryRequestHeaders); assert.equal(
compatibility.launchLibraryRequestHeaders,
launchLibraryRequestHeaders,
);
assert.equal(compatibility.LL2_CACHE_TTL_MS, LL2_CACHE_TTL_MS); assert.equal(compatibility.LL2_CACHE_TTL_MS, LL2_CACHE_TTL_MS);
}); });
@@ -46,8 +80,11 @@ test('exported CelesTrak plugin coalesces refreshes, retains stale TLEs, and rea
t.mock.method(Date, 'now', () => now); t.mock.method(Date, 'now', () => now);
t.mock.method(console, 'warn', () => {}); t.mock.method(console, 'warn', () => {});
const tle = 'ISS\n1 25544U fixture\n2 25544 fixture'; const tle = 'ISS\n1 25544U fixture\n2 25544 fixture';
let calls = 0, release; let calls = 0,
const gate = new Promise(resolve => { release = resolve; }); release;
const gate = new Promise((resolve) => {
release = resolve;
});
t.mock.method(globalThis, 'fetch', async (url) => { t.mock.method(globalThis, 'fetch', async (url) => {
calls++; calls++;
assert.equal(new URL(url).searchParams.get('GROUP'), 'stations'); assert.equal(new URL(url).searchParams.get('GROUP'), 'stations');
@@ -60,25 +97,37 @@ test('exported CelesTrak plugin coalesces refreshes, retains stale TLEs, and rea
const first = request('/api/celestrak', '/stations'); const first = request('/api/celestrak', '/stations');
const second = request('/api/celestrak', '/stations'); const second = request('/api/celestrak', '/stations');
release(); release();
for(const res of await Promise.all([first, second])) assert.equal(res.body, tle); for (const res of await Promise.all([first, second]))
assert.equal(res.body, tle);
assert.equal(calls, 1); assert.equal(calls, 1);
assert.equal((await request('/api/celestrak', '/stations')).headers['x-tle-cache'], 'HIT'); assert.equal(
(await request('/api/celestrak', '/stations')).headers['x-tle-cache'],
'HIT',
);
now += 6 * 3600_000; now += 6 * 3600_000;
t.mock.method(globalThis, 'fetch', async () => new Response('not a TLE')); t.mock.method(globalThis, 'fetch', async () => new Response('not a TLE'));
const stale = await request('/api/celestrak', '/stations'); const stale = await request('/api/celestrak', '/stations');
assert.equal(stale.body, tle); assert.equal(stale.body, tle);
assert.equal(stale.headers['x-tle-cache'], 'STALE-ERROR'); assert.equal(stale.headers['x-tle-cache'], 'STALE-ERROR');
t.mock.method(fsp, 'readFile', async () => JSON.stringify({ at: now, body: tle })); t.mock.method(fsp, 'readFile', async () =>
t.mock.method(globalThis, 'fetch', async () => { throw Error('fresh disk must prevent fetch'); }); JSON.stringify({ at: now, body: tle }),
);
t.mock.method(globalThis, 'fetch', async () => {
throw Error('fresh disk must prevent fetch');
});
const disk = await install(celestrakProxy())('/api/celestrak', '/stations'); const disk = await install(celestrakProxy())('/api/celestrak', '/stations');
assert.equal(disk.headers['x-tle-cache'], 'HIT'); assert.equal(disk.headers['x-tle-cache'], 'HIT');
assert.equal(disk.body, tle); assert.equal(disk.body, tle);
}); });
for(const preview of [false, true]) test(`exported launch plugin preserves optional server auth and cache in ${preview ? 'preview' : 'development'}`, async (t) => { for (const preview of [false, true])
test(`exported launch plugin preserves optional server auth and cache in ${preview ? 'preview' : 'development'}`, async (t) => {
isolateDisk(t); isolateDisk(t);
const prior = process.env.LL2_API_TOKEN; const prior = process.env.LL2_API_TOKEN;
t.after(() => { if (prior === undefined) delete process.env.LL2_API_TOKEN; else process.env.LL2_API_TOKEN = prior; }); t.after(() => {
if (prior === undefined) delete process.env.LL2_API_TOKEN;
else process.env.LL2_API_TOKEN = prior;
});
process.env.LL2_API_TOKEN = ' fixture-token '; process.env.LL2_API_TOKEN = ' fixture-token ';
let calls = 0; let calls = 0;
t.mock.method(globalThis, 'fetch', async (url, options) => { t.mock.method(globalThis, 'fetch', async (url, options) => {
@@ -96,4 +145,4 @@ for(const preview of [false, true]) test(`exported launch plugin preserves optio
assert.equal(hit.headers['X-GEV-Cache'], 'HIT'); assert.equal(hit.headers['X-GEV-Cache'], 'HIT');
assert.equal(hit.body, first.body); assert.equal(hit.body, first.body);
assert.equal(calls, 1); assert.equal(calls, 1);
}); });