refactor: split satellite and launch provider modules

This commit is contained in:
Sameh Khamis committed 2026-09-11 21:22:15 -07:00
1 parent bbabba89bf
commit 9a4b98700e
12 files changed
+417 -248

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
/** Fixed upstream request URLs; callers own validation, credentials and transport. */
export function celestrakTleUrl(group) {
const url = new URL('https://celestrak.org/NORAD/elements/gp.php');
url.searchParams.set('GROUP', group);
url.searchParams.set('FORMAT', 'tle');
return url;
}
export function launchLibraryRecentUrl(end) {
const start = new Date(end.getTime() - 30 * 86400000);
const url = new URL('https://ll.thespacedevs.com/2.3.0/launches/');
url.searchParams.set('net__gte', start.toISOString());
url.searchParams.set('net__lte', end.toISOString());
url.searchParams.set('limit', '100');
url.searchParams.set('mode', 'detailed');
return url;
}
+2 -2
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js']
const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js', 'space/celestrak.js', 'space/launch-library.js', '../../src/data/spaceProviderRequests.js']
.map(file => readFileSync(new URL(`../server/providers/${file}`, import.meta.url), 'utf8'))
.join('\n');
const detail = 'fixture-secret-token /internal/example <html>';
@@ -34,7 +34,7 @@ function fixture(name, overrides = {}, preview = false) {
resolveTerrainHeightRequest: async () => { throw new Error(detail); },
...overrides,
};
const helpers = ['readResponseTextCapped', 'coalesceProxyRequest', 'launchLibraryRequestHeaders'].map(extract).join('\n');
const helpers = ['readResponseTextCapped', 'coalesceProxyRequest', 'launchLibraryRequestHeaders', 'celestrakTleUrl', 'launchLibraryRecentUrl'].map(extract).join('\n');
const plugin = new Function(...Object.keys(deps), `${helpers}\n${extract(name)}\nreturn ${name}();`)(...Object.values(deps));
let middleware;
plugin[preview ? 'configurePreviewServer' : 'configureServer']({ middlewares: { use(_route, handler) { middleware = handler; } } });
+99
View File
@@ -0,0 +1,99 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { promises as fsp } from 'node:fs';
import { 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';
function install(plugin, preview = false) {
const routes = new Map();
plugin[preview ? 'configurePreviewServer' : 'configureServer']({ middlewares: { use(route, handler) { routes.set(route, handler); } } });
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; } };
await routes.get(route)({ url, method }, res);
return res;
};
}
function isolateDisk(t) {
t.mock.method(fsp, 'readFile', async () => { throw Error('no cache'); });
t.mock.method(fsp, 'stat', async () => { throw Error('no cache'); });
t.mock.method(fsp, 'mkdir', async () => {});
t.mock.method(fsp, 'writeFile', async () => {});
}
test('portable requests keep fixed origins, encode group data and preserve the 30-day UTC window', () => {
const tle = celestrakTleUrl('stations&FORMAT=json');
assert.equal(tle.origin, 'https://celestrak.org');
assert.equal(tle.pathname, '/NORAD/elements/gp.php');
assert.equal(tle.searchParams.get('GROUP'), 'stations&FORMAT=json');
assert.equal(tle.searchParams.get('FORMAT'), 'tle');
const end = new Date('2026-03-01T12:34:56.000Z');
const url = launchLibraryRecentUrl(end);
assert.equal(url.origin, 'https://ll.thespacedevs.com');
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.equal(end.toISOString(), '2026-03-01T12:34:56.000Z');
});
test('compatibility exports retain the same LL2 header helper and TTL', () => {
assert.equal(compatibility.launchLibraryRequestHeaders, launchLibraryRequestHeaders);
assert.equal(compatibility.LL2_CACHE_TTL_MS, LL2_CACHE_TTL_MS);
});
test('exported CelesTrak plugin coalesces refreshes, retains stale TLEs, and reads disk in a new instance', async (t) => {
isolateDisk(t);
let now = Date.now();
t.mock.method(Date, 'now', () => now);
t.mock.method(console, 'warn', () => {});
const tle = 'ISS\n1 25544U fixture\n2 25544 fixture';
let calls = 0, release;
const gate = new Promise(resolve => { release = resolve; });
t.mock.method(globalThis, 'fetch', async (url) => {
calls++;
assert.equal(new URL(url).searchParams.get('GROUP'), 'stations');
await gate;
return new Response(tle);
});
const request = install(celestrakProxy());
assert.equal((await request('/api/celestrak', '/../bad')).status, 400);
assert.equal(calls, 0);
const first = request('/api/celestrak', '/stations');
const second = request('/api/celestrak', '/stations');
release();
for(const res of await Promise.all([first, second])) assert.equal(res.body, tle);
assert.equal(calls, 1);
assert.equal((await request('/api/celestrak', '/stations')).headers['x-tle-cache'], 'HIT');
now += 6 * 3600_000;
t.mock.method(globalThis, 'fetch', async () => new Response('not a TLE'));
const stale = await request('/api/celestrak', '/stations');
assert.equal(stale.body, tle);
assert.equal(stale.headers['x-tle-cache'], 'STALE-ERROR');
t.mock.method(fsp, 'readFile', async () => 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');
assert.equal(disk.headers['x-tle-cache'], 'HIT');
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) => {
isolateDisk(t);
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; });
process.env.LL2_API_TOKEN = ' fixture-token ';
let calls = 0;
t.mock.method(globalThis, 'fetch', async (url, options) => {
calls++;
assert.equal(url.searchParams.get('limit'), '100');
assert.equal(options.headers.Authorization, 'Token fixture-token');
return Response.json({ results: [{ id: 'launch-fixture' }] });
});
const request = install(rocketLaunchesProxy(), preview);
assert.equal((await request('/api/launches', '/', 'POST')).status, 405);
const first = await request('/api/launches');
assert.equal(first.headers['X-GEV-Cache'], 'MISS');
assert.doesNotMatch(first.body, /fixture-token/);
const hit = await request('/api/launches');
assert.equal(hit.headers['X-GEV-Cache'], 'HIT');
assert.equal(hit.body, first.body);
assert.equal(calls, 1);
});