Skip to main content

Poll topology

A fleet poller for GET /topology built on the stepped as_of_utc pattern: walk the evaluation instant across a time range at a fixed step, one request per step, and each response is the latest-per-entity fleet state as of that instant. Setting the step equal to freshness_seconds gives clean, non-smearing frames.

The same loop serves two jobs:

  • Replay: step from a start time to an end time to reconstruct history.
  • Live polling: keep stepping at the wall clock (or just omit as_of_utc) to follow the present.

Each step sleeps one second between requests to stay under the per-tenant budget of 60 requests/minute (rate limits). Set CONSTELLATION_API_BASE and CONSTELLATION_API_TOKEN first (common setup).

const BASE = process.env.CONSTELLATION_API_BASE ?? 'https://api.constellation.space'
const TOKEN = process.env.CONSTELLATION_API_TOKEN
if (!TOKEN) throw new Error('CONSTELLATION_API_TOKEN is not set')

const STEP_SECONDS = 300
const START = new Date('2026-07-09T12:00:00Z')
const END = new Date('2026-07-09T14:00:00Z')

type Entity = { entity_id: string; observed_at: string; tags?: Record<string, string> }
type Topology = { as_of_utc: string; tenant_key: string; entities: Entity[] }

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

async function snapshot(asOf: Date): Promise<Topology> {
const url = new URL(`${BASE}/topology`)
url.searchParams.set('measurement', 'satellite')
url.searchParams.set('freshness_seconds', String(STEP_SECONDS))
url.searchParams.set('as_of_utc', asOf.toISOString())
for (;;) {
const res = await fetch(url, { headers: { 'x-api-key': TOKEN! } })
if (res.status === 429 || res.status === 503) {
await sleep(Number(res.headers.get('retry-after') ?? '5') * 1000)
continue
}
if (!res.ok) throw new Error(`topology read failed: ${res.status} ${await res.text()}`)
return (await res.json()) as Topology
}
}

for (let t = START.getTime(); t <= END.getTime(); t += STEP_SECONDS * 1000) {
const topo = await snapshot(new Date(t))
const nodes = new Set(topo.entities.map((e) => e.tags?.node_id ?? e.entity_id))
console.log(`${topo.as_of_utc} ${nodes.size} nodes`)
await sleep(1000) // pace requests under the tenant rate budget
}

Expected output:

2026-07-09T12:00:00Z 24 nodes
2026-07-09T12:05:00Z 24 nodes
2026-07-09T12:10:00Z 23 nodes

A node count dropping between frames means an entity went silent for longer than the freshness window; that reads as "stale", not "deleted" (topology semantics).