Compare commits

..

1 Commits

Author SHA1 Message Date
38c490b80e fix: handle empty lines array in TFL source
Empty lines array caused fetchLineStatuses to build /Line//Status
URL, resulting in a 404 from the TFL API. Now defaults to all
lines when the array is empty.

Also switches fetchStations to Promise.allSettled so individual
line failures don't break the entire station fetch.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-29 21:53:56 +00:00
6 changed files with 168 additions and 323 deletions

View File

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

View File

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

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, TflStatusData } from "./types.ts" import type { TflAlertData } from "./types.ts"
import { TflAlertSeverity } from "./types.ts" import { TflAlertSeverity } from "./types.ts"
@@ -21,26 +21,20 @@ function formatDistance(km: number): string {
return `${(meters / 1000).toFixed(1)}km away` return `${(meters / 1000).toFixed(1)}km away`
} }
function renderAlertRow(alert: TflAlertData) { export const renderTflAlert: FeedItemRenderer<"tfl-alert", TflAlertData> = (item) => {
const severityLabel = SEVERITY_LABEL[alert.severity] const { lineName, severity, description, closestStationDistance } = item.data
const severityLabel = SEVERITY_LABEL[severity]
return ( return (
<> <FeedCard>
<SansSerifText <SansSerifText content={`${lineName} · ${severityLabel}`} style="text-base font-semibold" />
content={`${alert.lineName} · ${severityLabel}`} <SansSerifText content={description} style="text-sm" />
style="text-base font-semibold" {closestStationDistance !== null ? (
/>
<SansSerifText content={alert.description} style="text-sm" />
{alert.closestStationDistance !== null ? (
<SansSerifText <SansSerifText
content={`Nearest station: ${formatDistance(alert.closestStationDistance)}`} content={`Nearest station: ${formatDistance(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,15 +138,13 @@ 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).toHaveLength(1) expect(before.length).toBe(2)
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).toHaveLength(1) expect(after.length).toBe(1)
expect(after[0]!.data.alerts).toHaveLength(1) expect(after[0]!.data.line).toBe("northern")
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 () => {
@@ -155,52 +153,23 @@ describe("TflSource", () => {
lines: ["northern"], lines: ["northern"],
}) })
const filtered = await source.fetchItems(createContext()) const filtered = await source.fetchItems(createContext())
expect(filtered[0]!.data.alerts).toHaveLength(1) expect(filtered.length).toBe(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[0]!.data.alerts).toHaveLength(2) expect(all.length).toBe(2)
}) })
}) })
describe("fetchItems", () => { describe("fetchItems", () => {
test("returns at most one feed item", async () => { test("returns feed items array", 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(items).toHaveLength(1) expect(Array.isArray(items)).toBe(true)
}) })
test("returns empty array when no disruptions", async () => { test("feed items have correct base structure", 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,
@@ -209,140 +178,72 @@ 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 alert of alerts) { for (const item of items) {
expect(typeof alert.line).toBe("string") expect(typeof item.id).toBe("string")
expect(typeof alert.lineName).toBe("string") expect(item.id).toMatch(/^tfl-alert-/)
expect(["minor-delays", "major-delays", "closure"]).toContain(alert.severity) expect(item.type).toBe("tfl-alert")
expect(typeof alert.description).toBe("string") expect(item.signals).toBeDefined()
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(
alert.closestStationDistance === null || typeof alert.closestStationDistance === "number", item.data.closestStationDistance === null ||
typeof item.data.closestStationDistance === "number",
).toBe(true) ).toBe(true)
} }
}) })
test("signals use highest severity urgency", async () => { test("feed item ids are unique", async () => {
const mixedApi: ITflApi = { const source = new TflSource({ client: api })
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())
expect(items[0]!.signals!.urgency).toBe(1.0) // closure urgency const ids = items.map((item) => item.id)
expect(items[0]!.signals!.timeRelevance).toBe("imminent") // closure time relevance const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
}) })
test("signals use single alert severity when only one disruption", async () => { test("feed items are sorted by urgency descending", async () => {
const singleApi: ITflApi = { const source = new TflSource({ client: api })
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())
expect(items[0]!.signals!.urgency).toBe(0.6) // minor-delays urgency for (let i = 1; i < items.length; i++) {
expect(items[0]!.signals!.timeRelevance).toBe("upcoming") const prev = items[i - 1]!
const curr = items[i]!
expect(prev.signals!.urgency).toBeGreaterThanOrEqual(curr.signals!.urgency!)
}
}) })
test("alerts sorted by closestStationDistance ascending, nulls last", async () => { test("urgency values match severity levels", async () => {
const distanceApi: ITflApi = { const source = new TflSource({ client: api })
async fetchLineStatuses(): Promise<TflLineStatus[]> { const items = await source.fetchItems(createContext())
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
// Alerts with distances should come before nulls const severityUrgency: Record<string, number> = {
const withDistance = alerts.filter((a) => a.closestStationDistance !== null) closure: 1.0,
const withoutDistance = alerts.filter((a) => a.closestStationDistance === null) "major-delays": 0.8,
"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()
}
} }
// Distance alerts are in ascending order for (const item of items) {
for (let i = 1; i < withDistance.length; i++) { expect(item.signals!.urgency).toBe(severityUrgency[item.data.severity]!)
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 () => {
@@ -355,9 +256,9 @@ describe("TflSource", () => {
} }
const items = await source.fetchItems(createContext(location)) const items = await source.fetchItems(createContext(location))
for (const alert of items[0]!.data.alerts) { for (const item of items) {
expect(typeof alert.closestStationDistance).toBe("number") expect(typeof item.data.closestStationDistance).toBe("number")
expect(alert.closestStationDistance!).toBeGreaterThan(0) expect(item.data.closestStationDistance!).toBeGreaterThan(0)
} }
}) })
@@ -365,8 +266,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 alert of items[0]!.data.alerts) { for (const item of items) {
expect(alert.closestStationDistance).toBeNull() expect(item.data.closestStationDistance).toBeNull()
} }
}) })
}) })
@@ -408,9 +309,8 @@ 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).toHaveLength(1) expect(items.length).toBe(1)
expect(items[0]!.data.alerts).toHaveLength(1) expect(items[0]!.data.line).toBe("northern")
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<TflStatusFeedItem> { export class TflSource implements FeedSource<TflAlertFeedItem> {
static readonly DEFAULT_LINES_OF_INTEREST: readonly TflLineId[] = [ static readonly DEFAULT_LINES_OF_INTEREST: readonly TflLineId[] = [
"bakerloo", "bakerloo",
"central", "central",
@@ -123,58 +123,56 @@ export class TflSource implements FeedSource<TflStatusFeedItem> {
this.lines = lines this.lines = lines
} }
async fetchItems(context: Context): Promise<TflStatusFeedItem[]> { async fetchItems(context: Context): Promise<TflAlertFeedItem[]> {
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 alerts: TflAlertData[] = statuses.map((status) => ({ const items: TflAlertFeedItem[] = statuses.map((status) => {
line: status.lineId, const closestStationDistance = location
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
}))
// Sort by closest station distance ascending, nulls last const data: TflAlertData = {
alerts.sort((a, b) => { line: status.lineId,
if (a.closestStationDistance === null && b.closestStationDistance === null) return 0 lineName: status.lineName,
if (a.closestStationDistance === null) return 1 severity: status.severity,
if (b.closestStationDistance === null) return -1 description: status.description,
return a.closestStationDistance - b.closestStationDistance closestStationDistance,
}
const signals: FeedItemSignals = {
urgency: SEVERITY_URGENCY[status.severity],
timeRelevance: SEVERITY_TIME_RELEVANCE[status.severity],
}
return {
id: `tfl-alert-${status.lineId}-${status.severity}`,
sourceId: this.id,
type: TflFeedItemType.Alert,
timestamp: context.time,
data,
signals,
}
}) })
// Signals from the highest-severity alert // Sort by urgency (desc), then by proximity (asc) if location available
const highestSeverity = alerts.reduce<TflAlertSeverity>( items.sort((a, b) => {
(worst, alert) => const aUrgency = a.signals?.urgency ?? 0
SEVERITY_URGENCY[alert.severity] > SEVERITY_URGENCY[worst] ? alert.severity : worst, const bUrgency = b.signals?.urgency ?? 0
alerts[0]!.severity, if (bUrgency !== aUrgency) {
) return bUrgency - aUrgency
}
if (a.data.closestStationDistance !== null && b.data.closestStationDistance !== null) {
return a.data.closestStationDistance - b.data.closestStationDistance
}
return 0
})
const signals: FeedItemSignals = { return items
urgency: SEVERITY_URGENCY[highestSeverity],
timeRelevance: SEVERITY_TIME_RELEVANCE[highestSeverity],
}
return [
{
id: "tfl-status",
sourceId: this.id,
type: TflFeedItemType.Status,
timestamp: context.time,
data: { alerts },
signals,
},
]
} }
} }

View File

@@ -22,19 +22,12 @@ 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