(source: DataSource, now: number)
| 214 | } |
| 215 | |
| 216 | private async fetchList(source: DataSource, now: number): Promise<Aircraft[] | null> { |
| 217 | const url = source === "radio" ? this.o.getConfig().radioUrl : this.buildApiUrl(); |
| 218 | try { |
| 219 | const json = await fetchJson(url); |
| 220 | const rawList: RawAircraft[] = json.aircraft ?? json.ac ?? []; |
| 221 | const list: Aircraft[] = []; |
| 222 | for (const raw of rawList) { |
| 223 | const ac = normalize(raw, now); |
| 224 | if (ac) list.push(ac); |
| 225 | } |
| 226 | // Area APIs sometimes return a far wider box than the radius we asked |
| 227 | // for, so a 3-mile setup ends up showing flights across the country. |
| 228 | // Trim to the configured radius ourselves (matching the renderer's |
| 229 | // radiusMiles * 1.08 cutoff). The radio feed is already local-only. |
| 230 | return source === "api" ? this.withinRadius(list) : list; |
| 231 | } catch (e) { |
| 232 | const reason = describeFetchError(e); |
| 233 | if (source === "api" && reason === "HTTP 429") { |
| 234 | this.apiBackoffUntil = now + RATE_LIMIT_BACKOFF_MS; |
| 235 | } |
| 236 | let host = url; |
| 237 | try { |
| 238 | host = new URL(url).host; |
| 239 | } catch { |
| 240 | // keep the raw url — a malformed one is itself the diagnosis |
| 241 | } |
| 242 | this.lastError = `${source} fetch failed: ${reason} (${host})`; |
| 243 | if (now - this.lastErrorLogAt > 30_000) { |
| 244 | this.lastErrorLogAt = now; |
| 245 | console.error(`[poller] ${source} fetch failed: ${reason} — ${url}`); |
| 246 | } |
| 247 | return null; |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | private async refreshApi(): Promise<void> { |
| 252 | if (Date.now() < this.apiBackoffUntil) return; |
no test coverage detected