fix(satellites): a current SATCAT read is kept six hours instead of re-reading eleven groups every update; the tracking regression serves SATCAT locally

This commit is contained in:
Sameh Khamis committed 2026-11-08 15:16:01 -08:00
1 parent 3bb742ae7e
commit cc24da165b
4 files changed
+71 -2

No files matched your search

+9
View File
@@ -409,6 +409,15 @@ async function main() {
status: 'ready',
}));
}
// CelesTrak SATCAT, which the satellite classes read beside the TLEs.
// An empty CSV (header only) keeps the run hermetic: the classes fall
// back to names and groups, as they do when SATCAT is unavailable.
if (isAppRequest && url.pathname.startsWith('/api/celestrak-satcat/')) {
return Promise.resolve(new Response(
'OBJECT_NAME,OBJECT_ID,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE,LAUNCH_DATE\n',
{ status: 200, headers: { 'Content-Type': 'text/csv' } },
));
}
// CelesTrak TLE groups. Served from a fixed two-satellite catalog so
// the satellite assertions are deterministic and the run never depends
// on an upstream that rate-limits (it was answering 403 the night this
+14
View File
@@ -12,6 +12,13 @@ import { classifySatellite, parseSatcatCsv } from './classes.js';
*/
export const SATCAT_WAIT_MS = 3000;
/**
* How long a complete, current SATCAT read is kept before the layer asks
* again, ms. SATCAT changes daily and the proxy caches it a day, so the
* five-minute TLE update does not read all eleven groups each time.
*/
export const SATCAT_KEEP_MS = 6 * 60 * 60 * 1000;
export function createIngestion({
state: layerState,
services,
@@ -42,6 +49,12 @@ export function createIngestion({
*/
function _readSatcat(signal) {
if (typeof source.readSatcat !== 'function') return Promise.resolve([]);
// A complete, current read is kept; a stale or failed one is asked again.
if (
layerState._satcatState === 'current' &&
Date.now() - layerState._satcatReadAtMs < SATCAT_KEEP_MS
)
return Promise.resolve([]);
return Promise.all(
CATALOG_GROUPS.map((groupDef) =>
source
@@ -86,6 +99,7 @@ export function createIngestion({
if (!groups.length) return;
const changed = layerState._satcatState !== satcatState;
layerState._satcatState = satcatState;
layerState._satcatReadAtMs = Date.now();
if (satcatState !== 'current')
console.warn(`[Data:Satellites] SATCAT ${satcatState}: classes may lag`);
// The row's info line says so (getRowControls).
+45 -2
View File
@@ -1,7 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseTleText, tleLineLaunchYear } from '../../sources/tle.js';
import { SATCAT_WAIT_MS, createIngestion } from './ingestion.js';
import {
SATCAT_KEEP_MS,
SATCAT_WAIT_MS,
createIngestion,
} from './ingestion.js';
import { createOrbits } from './orbits.js';
import { createControls } from './controls.js';
@@ -178,7 +182,9 @@ test('a satellite in visual and resource is imaging, whichever group shows it',
t.mock.method(console, 'warn', () => {});
// A name no pattern knows, no UCS record and no SATCAT: only its groups.
const { state, update } = ingestionFor((path) =>
path === 'visual' || path === 'resource' ? tleSet('ZZ TESTSAT', '50001') : '',
path === 'visual' || path === 'resource'
? tleSet('ZZ TESTSAT', '50001')
: '',
);
await update();
const sat = state._catalog.get(50001);
@@ -247,3 +253,40 @@ test('a failed SATCAT read is told on the layer row, not only the console', asyn
});
assert.equal(current.methods.getRowControls().info, '');
});
test('a current SATCAT read is kept between updates; a failed one is asked again', async (t) => {
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'warn', () => {});
const header =
'OBJECT_NAME,OBJECT_ID,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE\n';
let reads = 0;
let ok = true;
const { state, update } = ingestionFor(tleSet('ZZ TESTSAT', '50003'), {
source: {
readSatcat: async () => {
reads++;
return ok
? { ok: true, text: `${header}ZZ TESTSAT,2015-001A,50003,PAY,-` }
: { ok: false, text: '' };
},
},
});
await update();
const perUpdate = reads;
assert.ok(perUpdate > 0);
await update();
assert.equal(reads, perUpdate, 'a fresh read is not asked again');
assert.equal(
state._catalog.get(50003).satelliteClass,
'debris',
'the held records still class the rebuilt catalog',
);
state._satcatReadAtMs -= SATCAT_KEEP_MS;
ok = false;
await update();
assert.equal(reads, 2 * perUpdate, 'an old read is asked again');
assert.equal(state._satcatState, 'unavailable');
await update();
assert.equal(reads, 3 * perUpdate, 'a failed read is asked again');
});
+3
View File
@@ -48,6 +48,9 @@ export function createState({ services }) {
// How current the SATCAT records behind the classes are: null before any
// read, then 'current' | 'stale' | 'unavailable' (see ingestion.js).
state._satcatState = null;
// When _satcatState was last set (Date.now()); a current read is kept
// SATCAT_KEEP_MS (src/layers/satellites/ingestion.js).
state._satcatReadAtMs = Number.NEGATIVE_INFINITY;
state._denseStale = false;
state._activeUpdateControllers = new Set();