mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 05:01:17 +00:00
refactor: migrate aris-data-source-tfl to aris-source-tfl
Migrates TFL package from old DataSource interface to new FeedSource interface for use with FeedEngine. Changes: - Rename package from @aris/data-source-tfl to @aris/source-tfl - Replace TflDataSource class with TflSource implementing FeedSource - Add dependency on @aris/source-location for LocationKey - Use normalized priority values (0-1) instead of arbitrary numbers - Update tests for FeedSource interface - Update README.md with new package name Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
172
packages/aris-source-tfl/src/tfl-api.ts
Normal file
172
packages/aris-source-tfl/src/tfl-api.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { type } from "arktype"
|
||||
|
||||
import type { StationLocation, TflAlertSeverity, TflLineStatus } from "./types.ts"
|
||||
|
||||
const TFL_API_BASE = "https://api.tfl.gov.uk"
|
||||
|
||||
const ALL_LINE_IDS: TflLineId[] = [
|
||||
"bakerloo",
|
||||
"central",
|
||||
"circle",
|
||||
"district",
|
||||
"hammersmith-city",
|
||||
"jubilee",
|
||||
"metropolitan",
|
||||
"northern",
|
||||
"piccadilly",
|
||||
"victoria",
|
||||
"waterloo-city",
|
||||
"lioness",
|
||||
"mildmay",
|
||||
"windrush",
|
||||
"weaver",
|
||||
"suffragette",
|
||||
"liberty",
|
||||
"elizabeth",
|
||||
]
|
||||
|
||||
// TfL severity codes: https://api.tfl.gov.uk/Line/Meta/Severity
|
||||
// 0 = Special Service, 1 = Closed, 6 = Severe Delays, 9 = Minor Delays, 10 = Good Service
|
||||
const SEVERITY_MAP: Record<number, TflAlertSeverity | null> = {
|
||||
1: "closure",
|
||||
2: "closure", // Suspended
|
||||
3: "closure", // Part Suspended
|
||||
4: "closure", // Planned Closure
|
||||
5: "closure", // Part Closure
|
||||
6: "major-delays", // Severe Delays
|
||||
7: "major-delays", // Reduced Service
|
||||
8: "major-delays", // Bus Service
|
||||
9: "minor-delays", // Minor Delays
|
||||
10: null, // Good Service
|
||||
11: null, // Part Closed
|
||||
12: null, // Exit Only
|
||||
13: null, // No Step Free Access
|
||||
14: null, // Change of frequency
|
||||
15: null, // Diverted
|
||||
16: null, // Not Running
|
||||
17: null, // Issues Reported
|
||||
18: null, // No Issues
|
||||
19: null, // Information
|
||||
20: null, // Service Closed
|
||||
}
|
||||
|
||||
export class TflApi {
|
||||
private apiKey: string
|
||||
private stationsCache: StationLocation[] | null = null
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
private async fetch<T>(path: string): Promise<T> {
|
||||
const url = new URL(path, TFL_API_BASE)
|
||||
url.searchParams.set("app_key", this.apiKey)
|
||||
const response = await fetch(url.toString())
|
||||
if (!response.ok) {
|
||||
throw new Error(`TfL API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
async fetchLineStatuses(lines?: TflLineId[]): Promise<TflLineStatus[]> {
|
||||
const lineIds = lines ?? ALL_LINE_IDS
|
||||
const data = await this.fetch<unknown>(`/Line/${lineIds.join(",")}/Status`)
|
||||
|
||||
const parsed = lineResponseArray(data)
|
||||
if (parsed instanceof type.errors) {
|
||||
throw new Error(`Invalid TfL API response: ${parsed.summary}`)
|
||||
}
|
||||
|
||||
const statuses: TflLineStatus[] = []
|
||||
|
||||
for (const line of parsed) {
|
||||
for (const status of line.lineStatuses) {
|
||||
const severity = SEVERITY_MAP[status.statusSeverity]
|
||||
if (severity) {
|
||||
statuses.push({
|
||||
lineId: line.id,
|
||||
lineName: line.name,
|
||||
severity,
|
||||
description: status.reason ?? status.statusSeverityDescription,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
|
||||
async fetchStations(): Promise<StationLocation[]> {
|
||||
if (this.stationsCache) {
|
||||
return this.stationsCache
|
||||
}
|
||||
|
||||
// Fetch stations for all lines in parallel
|
||||
const responses = await Promise.all(
|
||||
ALL_LINE_IDS.map(async (id) => {
|
||||
const data = await this.fetch<unknown>(`/Line/${id}/StopPoints`)
|
||||
const parsed = lineStopPointsArray(data)
|
||||
if (parsed instanceof type.errors) {
|
||||
throw new Error(`Invalid TfL API response for line ${id}: ${parsed.summary}`)
|
||||
}
|
||||
return { lineId: id, stops: parsed }
|
||||
}),
|
||||
)
|
||||
|
||||
// Merge stations, combining lines for shared stations
|
||||
const stationMap = new Map<string, StationLocation>()
|
||||
|
||||
for (const { lineId: currentLineId, stops } of responses) {
|
||||
for (const stop of stops) {
|
||||
const existing = stationMap.get(stop.naptanId)
|
||||
if (existing) {
|
||||
if (!existing.lines.includes(currentLineId)) {
|
||||
existing.lines.push(currentLineId)
|
||||
}
|
||||
} else {
|
||||
stationMap.set(stop.naptanId, {
|
||||
id: stop.naptanId,
|
||||
name: stop.commonName,
|
||||
lat: stop.lat,
|
||||
lng: stop.lon,
|
||||
lines: [currentLineId],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.stationsCache = Array.from(stationMap.values())
|
||||
return this.stationsCache
|
||||
}
|
||||
}
|
||||
|
||||
// Schemas
|
||||
|
||||
const lineId = type(
|
||||
"'bakerloo' | 'central' | 'circle' | 'district' | 'hammersmith-city' | 'jubilee' | 'metropolitan' | 'northern' | 'piccadilly' | 'victoria' | 'waterloo-city' | 'lioness' | 'mildmay' | 'windrush' | 'weaver' | 'suffragette' | 'liberty' | 'elizabeth'",
|
||||
)
|
||||
|
||||
export type TflLineId = typeof lineId.infer
|
||||
|
||||
const lineStatus = type({
|
||||
statusSeverity: "number",
|
||||
statusSeverityDescription: "string",
|
||||
"reason?": "string",
|
||||
})
|
||||
|
||||
const lineResponse = type({
|
||||
id: lineId,
|
||||
name: "string",
|
||||
lineStatuses: lineStatus.array(),
|
||||
})
|
||||
|
||||
const lineResponseArray = lineResponse.array()
|
||||
|
||||
const lineStopPoint = type({
|
||||
naptanId: "string",
|
||||
commonName: "string",
|
||||
lat: "number",
|
||||
lon: "number",
|
||||
})
|
||||
|
||||
const lineStopPointsArray = lineStopPoint.array()
|
||||
Reference in New Issue
Block a user