From 2ba751ace840143b01d7bf6477810a1e14d22309 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 12:19:19 -0700 Subject: [PATCH] feat: read the transit feed catalog through the transit source Add getFeeds() to the transit source. It reads the public catalog route the server already serves, so code outside the layer can find feeds near a location without loading the layer. --- scripts/format-scope.json | 3 +- src/layers/transit/source.js | 15 ++++++++++ src/layers/transit/source.test.mjs | 46 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/layers/transit/source.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 9c4436652..6cfe9fe4e 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -873,5 +873,6 @@ "src/tools/queries/aviation.test.mjs", "src/tools/queries/space.test.mjs", "src/tools/queries/media.test.mjs", - "src/tools/queries/places.test.mjs" + "src/tools/queries/places.test.mjs", + "src/layers/transit/source.test.mjs" ] diff --git a/src/layers/transit/source.js b/src/layers/transit/source.js index 6ccdb9e55..702b5ed00 100644 --- a/src/layers/transit/source.js +++ b/src/layers/transit/source.js @@ -5,6 +5,21 @@ export function createTransitSource({ fetchImpl = (...args) => fetch(...args), } = {}) { return { + /** Read the public catalog of enabled feeds. */ + async getFeeds({ signal } = {}) { + signal?.throwIfAborted(); + const response = await fetchImpl('/api/transit/feeds', { + signal, + headers: { Accept: 'application/json' }, + }); + if (!response.ok) + throw new Error('Transit feeds HTTP ' + response.status); + const body = await response.json(); + signal?.throwIfAborted(); + if (!Array.isArray(body?.feeds)) + throw new Error('Malformed transit feed catalog'); + return body.feeds; + }, getHistory(feedId, vehicleId, { signal } = {}) { signal?.throwIfAborted(); return fetchTransitHistory(feedId, vehicleId, signal, fetchImpl); diff --git a/src/layers/transit/source.test.mjs b/src/layers/transit/source.test.mjs new file mode 100644 index 000000000..88d66664a --- /dev/null +++ b/src/layers/transit/source.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createTransitSource } from './source.js'; + +const json = (body, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +test('the feed catalog is read from the public catalog route', async () => { + const requested = []; + const feeds = [ + { + id: 'mbta', + name: 'MBTA', + center: { lat: 42.36, lon: -71.06 }, + loadRadiusKm: 60, + }, + ]; + const source = createTransitSource({ + fetchImpl: async (url) => { + requested.push(url); + return json({ feeds }); + }, + }); + assert.deepEqual(await source.getFeeds(), feeds); + assert.deepEqual(requested, ['/api/transit/feeds']); +}); + +test('feed catalog failures are reported', async () => { + const failing = createTransitSource({ fetchImpl: async () => json({}, 503) }); + await assert.rejects(failing.getFeeds(), /Transit feeds HTTP 503/); + const malformed = createTransitSource({ + fetchImpl: async () => json({ feeds: {} }), + }); + await assert.rejects(malformed.getFeeds(), /Malformed transit feed catalog/); + const aborted = new AbortController(); + aborted.abort(); + await assert.rejects( + createTransitSource({ + fetchImpl: async () => json({ feeds: [] }), + }).getFeeds({ signal: aborted.signal }), + (error) => error.name === 'AbortError', + ); +});