166 lines
4.4 KiB
TypeScript
166 lines
4.4 KiB
TypeScript
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";
|
|
};
|