Compare commits

..

2 Commits

Author SHA1 Message Date
4097470656 feat: switch default LLM to glm-4.7-flash (#108)
Co-authored-by: Ona <no-reply@ona.com>
2026-03-30 00:00:53 +01:00
f549859a44 feat: combine TFL alerts into single feed item (#107)
TflSource.fetchItems() now returns one TflStatusFeedItem with an
alerts array instead of separate items per line disruption. Signals
use the highest severity. Alerts sorted by station distance.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-30 00:00:41 +01:00
6 changed files with 322 additions and 167 deletions

View File

@@ -10,5 +10,7 @@ export {
type TflAlertSeverity, type TflAlertSeverity,
type TflLineStatus, type TflLineStatus,
type TflSourceOptions, type TflSourceOptions,
type TflStatusData,
type TflStatusFeedItem,
} from "./types.ts" } from "./types.ts"
export { renderTflAlert } from "./renderer.tsx" export { renderTflStatus } from "./renderer.tsx"

View File

@@ -2,102 +2,140 @@
import { render } from "@nym.sh/jrx" import { render } from "@nym.sh/jrx"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { TflAlertFeedItem } from "./types.ts" import type { TflAlertData, TflStatusFeedItem } from "./types.ts"
import { renderTflAlert } from "./renderer.tsx" import { renderTflStatus } from "./renderer.tsx"
function makeItem(overrides: Partial<TflAlertFeedItem["data"]> = {}): TflAlertFeedItem { function makeAlert(overrides: Partial<TflAlertData> = {}): TflAlertData {
return { return {
id: "tfl-alert-northern-minor-delays", line: "northern",
type: "tfl-alert", lineName: "Northern",
timestamp: new Date("2026-01-15T12:00:00Z"), severity: "minor-delays",
data: { description: "Minor delays due to signal failure",
line: "northern", closestStationDistance: null,
lineName: "Northern", ...overrides,
severity: "minor-delays",
description: "Minor delays due to signal failure",
closestStationDistance: null,
...overrides,
},
} }
} }
describe("renderTflAlert", () => { function makeItem(alerts: TflAlertData[]): TflStatusFeedItem {
test("renders a FeedCard with title and description", () => { return {
const node = renderTflAlert(makeItem()) id: "tfl-status",
sourceId: "aelis.tfl",
type: "tfl-status",
timestamp: new Date("2026-01-15T12:00:00Z"),
data: { alerts },
}
}
/** Collect all SansSerifText elements from a rendered spec, filtering out Fragments. */
function collectTextElements(spec: ReturnType<typeof render>) {
return Object.values(spec.elements).filter((el) => el.type === "SansSerifText")
}
describe("renderTflStatus", () => {
test("renders a single FeedCard", () => {
const node = renderTflStatus(makeItem([makeAlert()]))
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const root = spec.elements[spec.root]!
expect(root.type).toBe("FeedCard") expect(root.type).toBe("FeedCard")
expect(root.children!.length).toBeGreaterThanOrEqual(2) })
const title = spec.elements[root.children![0]!]! test("renders one alert with title and description", () => {
expect(title.type).toBe("SansSerifText") const node = renderTflStatus(makeItem([makeAlert()]))
expect(title.props.content).toBe("Northern · Minor delays") const spec = render(node)
const body = spec.elements[root.children![1]!]! const texts = collectTextElements(spec)
expect(body.type).toBe("SansSerifText") const titleText = texts.find((el) => el.props.content === "Northern · Minor delays")
expect(body.props.content).toBe("Minor delays due to signal failure") const bodyText = texts.find((el) => el.props.content === "Minor delays due to signal failure")
expect(titleText).toBeDefined()
expect(bodyText).toBeDefined()
})
test("renders multiple alerts stacked in one card", () => {
const alerts = [
makeAlert({ line: "northern", lineName: "Northern", severity: "minor-delays" }),
makeAlert({
line: "central",
lineName: "Central",
severity: "closure",
description: "Closed due to strike",
}),
]
const node = renderTflStatus(makeItem(alerts))
const spec = render(node)
const root = spec.elements[spec.root]!
expect(root.type).toBe("FeedCard")
const texts = collectTextElements(spec)
const northernTitle = texts.find((el) => el.props.content === "Northern · Minor delays")
const centralTitle = texts.find((el) => el.props.content === "Central · Closed")
const centralBody = texts.find((el) => el.props.content === "Closed due to strike")
expect(northernTitle).toBeDefined()
expect(centralTitle).toBeDefined()
expect(centralBody).toBeDefined()
}) })
test("shows nearest station distance when available", () => { test("shows nearest station distance when available", () => {
const node = renderTflAlert(makeItem({ closestStationDistance: 0.35 })) const node = renderTflStatus(makeItem([makeAlert({ closestStationDistance: 0.35 })]))
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
expect(root.children).toHaveLength(3) const caption = texts.find((el) => el.props.content === "Nearest station: 350m away")
expect(caption).toBeDefined()
const caption = spec.elements[root.children![2]!]!
expect(caption.type).toBe("SansSerifText")
expect(caption.props.content).toBe("Nearest station: 350m away")
}) })
test("formats distance in km when >= 1km", () => { test("formats distance in km when >= 1km", () => {
const node = renderTflAlert(makeItem({ closestStationDistance: 2.456 })) const node = renderTflStatus(makeItem([makeAlert({ closestStationDistance: 2.456 })]))
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
const caption = spec.elements[root.children![2]!]! const caption = texts.find((el) => el.props.content === "Nearest station: 2.5km away")
expect(caption.props.content).toBe("Nearest station: 2.5km away") expect(caption).toBeDefined()
}) })
test("formats near-1km boundary as km not meters", () => { test("formats near-1km boundary as km not meters", () => {
const node = renderTflAlert(makeItem({ closestStationDistance: 0.9999 })) const node = renderTflStatus(makeItem([makeAlert({ closestStationDistance: 0.9999 })]))
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
const caption = spec.elements[root.children![2]!]! const caption = texts.find((el) => el.props.content === "Nearest station: 1.0km away")
expect(caption.props.content).toBe("Nearest station: 1.0km away") expect(caption).toBeDefined()
}) })
test("omits station distance when null", () => { test("omits station distance when null", () => {
const node = renderTflAlert(makeItem({ closestStationDistance: null })) const node = renderTflStatus(makeItem([makeAlert({ closestStationDistance: null })]))
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
// Title + body only, no caption (empty fragment doesn't produce a child) const distanceTexts = texts.filter((el) =>
const children = root.children!.filter((key) => { (el.props.content as string).startsWith("Nearest station:"),
const el = spec.elements[key] )
return el && el.type !== "Fragment" expect(distanceTexts).toHaveLength(0)
})
expect(children).toHaveLength(2)
}) })
test("renders closure severity label", () => { test("renders closure severity label", () => {
const node = renderTflAlert(makeItem({ severity: "closure", lineName: "Central" })) const node = renderTflStatus(
makeItem([makeAlert({ severity: "closure", lineName: "Central" })]),
)
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
const title = spec.elements[root.children![0]!]! const title = texts.find((el) => el.props.content === "Central · Closed")
expect(title.props.content).toBe("Central · Closed") expect(title).toBeDefined()
}) })
test("renders major delays severity label", () => { test("renders major delays severity label", () => {
const node = renderTflAlert(makeItem({ severity: "major-delays", lineName: "Jubilee" })) const node = renderTflStatus(
makeItem([makeAlert({ severity: "major-delays", lineName: "Jubilee" })]),
)
const spec = render(node) const spec = render(node)
const root = spec.elements[spec.root]! const texts = collectTextElements(spec)
const title = spec.elements[root.children![0]!]! const title = texts.find((el) => el.props.content === "Jubilee · Major delays")
expect(title.props.content).toBe("Jubilee · Major delays") expect(title).toBeDefined()
}) })
}) })

View File

@@ -3,7 +3,7 @@ import type { FeedItemRenderer } from "@aelis/core"
import { FeedCard, SansSerifText } from "@aelis/components" import { FeedCard, SansSerifText } from "@aelis/components"
import type { TflAlertData } from "./types.ts" import type { TflAlertData, TflStatusData } from "./types.ts"
import { TflAlertSeverity } from "./types.ts" import { TflAlertSeverity } from "./types.ts"
@@ -21,20 +21,26 @@ function formatDistance(km: number): string {
return `${(meters / 1000).toFixed(1)}km away` return `${(meters / 1000).toFixed(1)}km away`
} }
export const renderTflAlert: FeedItemRenderer<"tfl-alert", TflAlertData> = (item) => { function renderAlertRow(alert: TflAlertData) {
const { lineName, severity, description, closestStationDistance } = item.data const severityLabel = SEVERITY_LABEL[alert.severity]
const severityLabel = SEVERITY_LABEL[severity]
return ( return (
<FeedCard> <>
<SansSerifText content={`${lineName} · ${severityLabel}`} style="text-base font-semibold" /> <SansSerifText
<SansSerifText content={description} style="text-sm" /> content={`${alert.lineName} · ${severityLabel}`}
{closestStationDistance !== null ? ( style="text-base font-semibold"
/>
<SansSerifText content={alert.description} style="text-sm" />
{alert.closestStationDistance !== null ? (
<SansSerifText <SansSerifText
content={`Nearest station: ${formatDistance(closestStationDistance)}`} content={`Nearest station: ${formatDistance(alert.closestStationDistance)}`}
style="text-xs text-stone-500" style="text-xs text-stone-500"
/> />
) : null} ) : null}
</FeedCard> </>
) )
} }
export const renderTflStatus: FeedItemRenderer<"tfl-status", TflStatusData> = (item) => {
return <FeedCard>{item.data.alerts.map((alert) => renderAlertRow(alert))}</FeedCard>
}

View File

@@ -138,13 +138,15 @@ describe("TflSource", () => {
test("changes which lines are fetched", async () => { test("changes which lines are fetched", async () => {
const source = new TflSource({ client: lineFilteringApi }) const source = new TflSource({ client: lineFilteringApi })
const before = await source.fetchItems(createContext()) const before = await source.fetchItems(createContext())
expect(before.length).toBe(2) expect(before).toHaveLength(1)
expect(before[0]!.data.alerts).toHaveLength(2)
source.setLinesOfInterest(["northern"]) source.setLinesOfInterest(["northern"])
const after = await source.fetchItems(createContext()) const after = await source.fetchItems(createContext())
expect(after.length).toBe(1) expect(after).toHaveLength(1)
expect(after[0]!.data.line).toBe("northern") expect(after[0]!.data.alerts).toHaveLength(1)
expect(after[0]!.data.alerts[0]!.line).toBe("northern")
}) })
test("DEFAULT_LINES_OF_INTEREST restores all lines", async () => { test("DEFAULT_LINES_OF_INTEREST restores all lines", async () => {
@@ -153,23 +155,52 @@ describe("TflSource", () => {
lines: ["northern"], lines: ["northern"],
}) })
const filtered = await source.fetchItems(createContext()) const filtered = await source.fetchItems(createContext())
expect(filtered.length).toBe(1) expect(filtered[0]!.data.alerts).toHaveLength(1)
source.setLinesOfInterest([...TflSource.DEFAULT_LINES_OF_INTEREST]) source.setLinesOfInterest([...TflSource.DEFAULT_LINES_OF_INTEREST])
const all = await source.fetchItems(createContext()) const all = await source.fetchItems(createContext())
expect(all.length).toBe(2) expect(all[0]!.data.alerts).toHaveLength(2)
}) })
}) })
describe("fetchItems", () => { describe("fetchItems", () => {
test("returns feed items array", async () => { test("returns at most one feed item", async () => {
const source = new TflSource({ client: api }) const source = new TflSource({ client: api })
const items = await source.fetchItems(createContext()) const items = await source.fetchItems(createContext())
expect(Array.isArray(items)).toBe(true) expect(items).toHaveLength(1)
}) })
test("feed items have correct base structure", async () => { test("returns empty array when no disruptions", async () => {
const emptyApi: ITflApi = {
async fetchLineStatuses(): Promise<TflLineStatus[]> {
return []
},
async fetchStations(): Promise<StationLocation[]> {
return []
},
}
const source = new TflSource({ client: emptyApi })
const items = await source.fetchItems(createContext())
expect(items).toHaveLength(0)
})
test("combined item has correct base structure", async () => {
const source = new TflSource({ client: api })
const items = await source.fetchItems(createContext())
const item = items[0]!
expect(item.id).toBe("tfl-status")
expect(item.type).toBe("tfl-status")
expect(item.sourceId).toBe("aelis.tfl")
expect(item.signals).toBeDefined()
expect(typeof item.signals!.urgency).toBe("number")
expect(item.timestamp).toBeInstanceOf(Date)
expect(Array.isArray(item.data.alerts)).toBe(true)
expect(item.data.alerts.length).toBeGreaterThan(0)
})
test("alerts have correct data structure", async () => {
const source = new TflSource({ client: api }) const source = new TflSource({ client: api })
const location: Location = { const location: Location = {
lat: 51.5074, lat: 51.5074,
@@ -178,72 +209,140 @@ describe("TflSource", () => {
timestamp: new Date(), timestamp: new Date(),
} }
const items = await source.fetchItems(createContext(location)) const items = await source.fetchItems(createContext(location))
const alerts = items[0]!.data.alerts
for (const item of items) { for (const alert of alerts) {
expect(typeof item.id).toBe("string") expect(typeof alert.line).toBe("string")
expect(item.id).toMatch(/^tfl-alert-/) expect(typeof alert.lineName).toBe("string")
expect(item.type).toBe("tfl-alert") expect(["minor-delays", "major-delays", "closure"]).toContain(alert.severity)
expect(item.signals).toBeDefined() expect(typeof alert.description).toBe("string")
expect(typeof item.signals!.urgency).toBe("number")
expect(item.timestamp).toBeInstanceOf(Date)
}
})
test("feed items have correct data structure", async () => {
const source = new TflSource({ client: api })
const location: Location = {
lat: 51.5074,
lng: -0.1278,
accuracy: 10,
timestamp: new Date(),
}
const items = await source.fetchItems(createContext(location))
for (const item of items) {
expect(typeof item.data.line).toBe("string")
expect(typeof item.data.lineName).toBe("string")
expect(["minor-delays", "major-delays", "closure"]).toContain(item.data.severity)
expect(typeof item.data.description).toBe("string")
expect( expect(
item.data.closestStationDistance === null || alert.closestStationDistance === null || typeof alert.closestStationDistance === "number",
typeof item.data.closestStationDistance === "number",
).toBe(true) ).toBe(true)
} }
}) })
test("feed item ids are unique", async () => { test("signals use highest severity urgency", async () => {
const source = new TflSource({ client: api }) const mixedApi: ITflApi = {
async fetchLineStatuses(): Promise<TflLineStatus[]> {
return [
{
lineId: "northern",
lineName: "Northern",
severity: "minor-delays",
description: "Minor delays",
},
{
lineId: "central",
lineName: "Central",
severity: "closure",
description: "Closed",
},
{
lineId: "jubilee",
lineName: "Jubilee",
severity: "major-delays",
description: "Major delays",
},
]
},
async fetchStations(): Promise<StationLocation[]> {
return []
},
}
const source = new TflSource({ client: mixedApi })
const items = await source.fetchItems(createContext()) const items = await source.fetchItems(createContext())
const ids = items.map((item) => item.id) expect(items[0]!.signals!.urgency).toBe(1.0) // closure urgency
const uniqueIds = new Set(ids) expect(items[0]!.signals!.timeRelevance).toBe("imminent") // closure time relevance
expect(uniqueIds.size).toBe(ids.length)
}) })
test("feed items are sorted by urgency descending", async () => { test("signals use single alert severity when only one disruption", async () => {
const source = new TflSource({ client: api }) const singleApi: ITflApi = {
async fetchLineStatuses(): Promise<TflLineStatus[]> {
return [
{
lineId: "northern",
lineName: "Northern",
severity: "minor-delays",
description: "Minor delays",
},
]
},
async fetchStations(): Promise<StationLocation[]> {
return []
},
}
const source = new TflSource({ client: singleApi })
const items = await source.fetchItems(createContext()) const items = await source.fetchItems(createContext())
for (let i = 1; i < items.length; i++) { expect(items[0]!.signals!.urgency).toBe(0.6) // minor-delays urgency
const prev = items[i - 1]! expect(items[0]!.signals!.timeRelevance).toBe("upcoming")
const curr = items[i]!
expect(prev.signals!.urgency).toBeGreaterThanOrEqual(curr.signals!.urgency!)
}
}) })
test("urgency values match severity levels", async () => { test("alerts sorted by closestStationDistance ascending, nulls last", async () => {
const source = new TflSource({ client: api }) const distanceApi: ITflApi = {
const items = await source.fetchItems(createContext()) async fetchLineStatuses(): Promise<TflLineStatus[]> {
return [
{
lineId: "northern",
lineName: "Northern",
severity: "minor-delays",
description: "Delays",
},
{
lineId: "central",
lineName: "Central",
severity: "minor-delays",
description: "Delays",
},
{
lineId: "jubilee",
lineName: "Jubilee",
severity: "minor-delays",
description: "Delays",
},
]
},
async fetchStations(): Promise<StationLocation[]> {
return [
{ id: "s1", name: "Station A", lat: 51.51, lng: -0.13, lines: ["central"] },
{ id: "s2", name: "Station B", lat: 51.52, lng: -0.14, lines: ["northern"] },
// No stations for jubilee — its distance will be null
]
},
}
const source = new TflSource({ client: distanceApi })
const location: Location = {
lat: 51.5074,
lng: -0.1278,
accuracy: 10,
timestamp: new Date(),
}
const items = await source.fetchItems(createContext(location))
const alerts = items[0]!.data.alerts
const severityUrgency: Record<string, number> = { // Alerts with distances should come before nulls
closure: 1.0, const withDistance = alerts.filter((a) => a.closestStationDistance !== null)
"major-delays": 0.8, const withoutDistance = alerts.filter((a) => a.closestStationDistance === null)
"minor-delays": 0.6,
// All distance alerts come first
const firstNullIndex = alerts.findIndex((a) => a.closestStationDistance === null)
if (firstNullIndex !== -1) {
for (let i = 0; i < firstNullIndex; i++) {
expect(alerts[i]!.closestStationDistance).not.toBeNull()
}
} }
for (const item of items) { // Distance alerts are in ascending order
expect(item.signals!.urgency).toBe(severityUrgency[item.data.severity]!) for (let i = 1; i < withDistance.length; i++) {
expect(withDistance[i]!.closestStationDistance!).toBeGreaterThanOrEqual(
withDistance[i - 1]!.closestStationDistance!,
)
} }
expect(withoutDistance.length).toBe(1)
expect(withoutDistance[0]!.line).toBe("jubilee")
}) })
test("closestStationDistance is number when location provided", async () => { test("closestStationDistance is number when location provided", async () => {
@@ -256,9 +355,9 @@ describe("TflSource", () => {
} }
const items = await source.fetchItems(createContext(location)) const items = await source.fetchItems(createContext(location))
for (const item of items) { for (const alert of items[0]!.data.alerts) {
expect(typeof item.data.closestStationDistance).toBe("number") expect(typeof alert.closestStationDistance).toBe("number")
expect(item.data.closestStationDistance!).toBeGreaterThan(0) expect(alert.closestStationDistance!).toBeGreaterThan(0)
} }
}) })
@@ -266,8 +365,8 @@ describe("TflSource", () => {
const source = new TflSource({ client: api }) const source = new TflSource({ client: api })
const items = await source.fetchItems(createContext()) const items = await source.fetchItems(createContext())
for (const item of items) { for (const alert of items[0]!.data.alerts) {
expect(item.data.closestStationDistance).toBeNull() expect(alert.closestStationDistance).toBeNull()
} }
}) })
}) })
@@ -309,8 +408,9 @@ describe("TflSource", () => {
await source.executeAction("set-lines-of-interest", ["northern"]) await source.executeAction("set-lines-of-interest", ["northern"])
const items = await source.fetchItems(createContext()) const items = await source.fetchItems(createContext())
expect(items.length).toBe(1) expect(items).toHaveLength(1)
expect(items[0]!.data.line).toBe("northern") expect(items[0]!.data.alerts).toHaveLength(1)
expect(items[0]!.data.alerts[0]!.line).toBe("northern")
}) })
test("executeAction throws on invalid input", async () => { test("executeAction throws on invalid input", async () => {

View File

@@ -8,10 +8,10 @@ import type {
ITflApi, ITflApi,
StationLocation, StationLocation,
TflAlertData, TflAlertData,
TflAlertFeedItem,
TflAlertSeverity, TflAlertSeverity,
TflLineId, TflLineId,
TflSourceOptions, TflSourceOptions,
TflStatusFeedItem,
} from "./types.ts" } from "./types.ts"
import { TflApi, lineId } from "./tfl-api.ts" import { TflApi, lineId } from "./tfl-api.ts"
@@ -51,7 +51,7 @@ const SEVERITY_TIME_RELEVANCE: Record<TflAlertSeverity, TimeRelevance> = {
* const { items } = await engine.refresh() * const { items } = await engine.refresh()
* ``` * ```
*/ */
export class TflSource implements FeedSource<TflAlertFeedItem> { export class TflSource implements FeedSource<TflStatusFeedItem> {
static readonly DEFAULT_LINES_OF_INTEREST: readonly TflLineId[] = [ static readonly DEFAULT_LINES_OF_INTEREST: readonly TflLineId[] = [
"bakerloo", "bakerloo",
"central", "central",
@@ -123,56 +123,58 @@ export class TflSource implements FeedSource<TflAlertFeedItem> {
this.lines = lines this.lines = lines
} }
async fetchItems(context: Context): Promise<TflAlertFeedItem[]> { async fetchItems(context: Context): Promise<TflStatusFeedItem[]> {
const [statuses, stations] = await Promise.all([ const [statuses, stations] = await Promise.all([
this.client.fetchLineStatuses(this.lines), this.client.fetchLineStatuses(this.lines),
this.client.fetchStations(), this.client.fetchStations(),
]) ])
if (statuses.length === 0) {
return []
}
const location = context.get(LocationKey) const location = context.get(LocationKey)
const items: TflAlertFeedItem[] = statuses.map((status) => { const alerts: TflAlertData[] = statuses.map((status) => ({
const closestStationDistance = location line: status.lineId,
lineName: status.lineName,
severity: status.severity,
description: status.description,
closestStationDistance: location
? findClosestStationDistance(status.lineId, stations, location.lat, location.lng) ? findClosestStationDistance(status.lineId, stations, location.lat, location.lng)
: null : null,
}))
const data: TflAlertData = { // Sort by closest station distance ascending, nulls last
line: status.lineId, alerts.sort((a, b) => {
lineName: status.lineName, if (a.closestStationDistance === null && b.closestStationDistance === null) return 0
severity: status.severity, if (a.closestStationDistance === null) return 1
description: status.description, if (b.closestStationDistance === null) return -1
closestStationDistance, return a.closestStationDistance - b.closestStationDistance
} })
const signals: FeedItemSignals = { // Signals from the highest-severity alert
urgency: SEVERITY_URGENCY[status.severity], const highestSeverity = alerts.reduce<TflAlertSeverity>(
timeRelevance: SEVERITY_TIME_RELEVANCE[status.severity], (worst, alert) =>
} SEVERITY_URGENCY[alert.severity] > SEVERITY_URGENCY[worst] ? alert.severity : worst,
alerts[0]!.severity,
)
return { const signals: FeedItemSignals = {
id: `tfl-alert-${status.lineId}-${status.severity}`, urgency: SEVERITY_URGENCY[highestSeverity],
timeRelevance: SEVERITY_TIME_RELEVANCE[highestSeverity],
}
return [
{
id: "tfl-status",
sourceId: this.id, sourceId: this.id,
type: TflFeedItemType.Alert, type: TflFeedItemType.Status,
timestamp: context.time, timestamp: context.time,
data, data: { alerts },
signals, signals,
} },
}) ]
// Sort by urgency (desc), then by proximity (asc) if location available
items.sort((a, b) => {
const aUrgency = a.signals?.urgency ?? 0
const bUrgency = b.signals?.urgency ?? 0
if (bUrgency !== aUrgency) {
return bUrgency - aUrgency
}
if (a.data.closestStationDistance !== null && b.data.closestStationDistance !== null) {
return a.data.closestStationDistance - b.data.closestStationDistance
}
return 0
})
return items
} }
} }

View File

@@ -22,12 +22,19 @@ export interface TflAlertData extends Record<string, unknown> {
export const TflFeedItemType = { export const TflFeedItemType = {
Alert: "tfl-alert", Alert: "tfl-alert",
Status: "tfl-status",
} as const } as const
export type TflFeedItemType = (typeof TflFeedItemType)[keyof typeof TflFeedItemType] export type TflFeedItemType = (typeof TflFeedItemType)[keyof typeof TflFeedItemType]
export type TflAlertFeedItem = FeedItem<typeof TflFeedItemType.Alert, TflAlertData> export type TflAlertFeedItem = FeedItem<typeof TflFeedItemType.Alert, TflAlertData>
export interface TflStatusData extends Record<string, unknown> {
alerts: TflAlertData[]
}
export type TflStatusFeedItem = FeedItem<typeof TflFeedItemType.Status, TflStatusData>
export interface TflSourceOptions { export interface TflSourceOptions {
apiKey?: string apiKey?: string
client?: ITflApi client?: ITflApi