feat(companion): add data sources package
This commit is contained in:
165
aris/packages/data-sources/src/tfl/tfl.ts
Normal file
165
aris/packages/data-sources/src/tfl/tfl.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
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";
|
||||
};
|
||||
Reference in New Issue
Block a user