Files
aris-old/aris/packages/data-sources/src/tfl/tfl.ts

166 lines
4.4 KiB
TypeScript
Raw Normal View History

import { DataSourceError } from "../common/errors";
import type { Diagnostics } from "../common/types";
import type {
TflData,
TflDataSource,
TflDataSourceConfig,
TflDisruption,
TflRequest,
TflStatusSeverity,
} from "./types";
import { TFL_STATUS_SEVERITY } from "./types";
type CacheEntry = {
timestamp: number;
data: TflData;
};
const defaultConfig: Required<TflDataSourceConfig> = {
cacheValiditySec: 120,
ttlSec: 300,
maxDisruptions: 3,
};
const ignoredSeverities = new Set<TflStatusSeverity>([
TFL_STATUS_SEVERITY.PlannedClosure,
TFL_STATUS_SEVERITY.PartClosure,
TFL_STATUS_SEVERITY.GoodService,
]);
const majorSeverities = new Set<TflStatusSeverity>([
TFL_STATUS_SEVERITY.Closed,
TFL_STATUS_SEVERITY.Suspended,
TFL_STATUS_SEVERITY.PartSuspended,
TFL_STATUS_SEVERITY.SevereDelays,
]);
let cache: CacheEntry | null = null;
const resolveConfig = (
base: TflDataSourceConfig,
override?: TflDataSourceConfig,
): Required<TflDataSourceConfig> => ({
...defaultConfig,
...base,
...(override ?? {}),
});
const fetchTflData = async (
request: TflRequest,
config: TflDataSourceConfig,
): Promise<{ data: TflData; diagnostics: Diagnostics }> => {
const resolvedConfig = resolveConfig(config, request.config);
const diagnostics: Diagnostics = {
now: String(request.now),
cache_validity_sec: String(resolvedConfig.cacheValiditySec),
};
if (cache && request.now - cache.timestamp < resolvedConfig.cacheValiditySec) {
diagnostics.source = "cache";
diagnostics.cache_age_sec = String(request.now - cache.timestamp);
return { data: cache.data, diagnostics };
}
const url = "https://api.tfl.gov.uk/Line/Mode/tube,elizabeth-line/Status";
const response = await fetch(url, {
headers: {
Accept: "application/json",
},
});
diagnostics.http_status = String(response.status);
if (!response.ok) {
throw new DataSourceError(
"network_failed",
`HTTP ${response.status}`,
diagnostics,
);
}
const lines = (await response.json()) as Array<{
id: string;
name: string;
lineStatuses?: Array<{
statusSeverity: number;
statusSeverityDescription: string;
reason?: string | null;
disruption?: { description?: string | null } | null;
}>;
}>;
diagnostics.source = "network";
diagnostics.lines_returned = String(lines.length);
const disruptions: TflDisruption[] = [];
const seenLines = new Set<string>();
for (const line of lines) {
if (seenLines.has(line.id)) {
continue;
}
const statuses = line.lineStatuses ?? [];
for (const status of statuses) {
if (ignoredSeverities.has(status.statusSeverity)) {
continue;
}
seenLines.add(line.id);
const isMajor = majorSeverities.has(status.statusSeverity);
disruptions.push({
id: `${line.id}:${status.statusSeverity}`,
lineName: line.name,
lineId: line.id,
severity: status.statusSeverity,
severityDescription: status.statusSeverityDescription,
reason: status.reason ?? status.disruption?.description ?? null,
isMajor,
});
break;
}
}
disruptions.sort((a, b) => a.severity - b.severity);
const limited = disruptions.slice(0, resolvedConfig.maxDisruptions);
diagnostics.disruptions_found = String(disruptions.length);
diagnostics.disruptions_returned = String(limited.length);
const data = { disruptions: limited };
cache = { timestamp: request.now, data };
return { data, diagnostics };
};
export const createTflDataSource = (
config: TflDataSourceConfig = {},
): TflDataSource => ({
dataWithDiagnostics: (request) => fetchTflData(request, config),
});
export const formatTflDisruptionTitle = (disruption: TflDisruption) => {
let name = disruption.lineName;
name = name.replace(" & City", "");
name = name.replace("Hammersmith", "H'smith");
name = name.replace("Metropolitan", "Met");
name = name.replace("Waterloo", "W'loo");
name = name.replace("Elizabeth line", "Eliz.");
let severity = disruption.severityDescription;
severity = severity.replace("Minor Delays", "Delays");
severity = severity.replace("Severe Delays", "Severe");
severity = severity.replace("Part Closure", "Part Closed");
severity = severity.replace("Part Suspended", "Part Susp.");
return `${name}: ${severity}`;
};
export const formatTflDisruptionSubtitle = (disruption: TflDisruption) => {
if (!disruption.reason) {
return "Check TFL for details";
}
const first = disruption.reason
.split(".")
.map((part) => part.trim())
.find((part) => part.length > 0);
return first && first.length > 0 ? first : "Check TFL for details";
};