fix(transit): real route-id modes for Prague, Bordeaux and Rouen; dated Warsaw/Poznań credits; data.gov.my refresh

- Prague PID: map route-id ranges from PID's GTFS (metro L991-993, S/R trains
  L1000s/L1100s, P ferries L1800s, trolleybuses 50-59, X lines, funicular LD)
  instead of 'below 100 is a tram, rest bus'.
- Bordeaux TBM: trams ride numbered ids 59-62/163/164 (A-F), boats 951-953;
  the old letter match never fired.
- Rouen Astuce: TCAR:90 Metro as tram, TCAR:99 Calypso as ferry; Le Havre FUNI.
- Warsaw and Poznan: credit carries the data date (and Warsaw links its API),
  as their terms require.
- data.gov.my feeds: CC BY 4.0, refreshMs 30 s to stay under 4 requests/min;
  proxy honours per-feed refreshMs.
- Denver: quote the RTD agreement; tests use the feeds' real route ids.
This commit is contained in:
Said Turksever committed 2026-11-09 13:50:14 +00:00
1 parent e631839a4e
commit 7a053c640d
10 files changed
+287 -59

No files matched your search

+8 -8
View File
@@ -105,11 +105,11 @@ How to read this:
| **SMTR Rio de Janeiro** (dados.mobilidade.rio) | Transit layer live vehicles, Rio de Janeiro: municipal buses (SPPO) and the Mobi-Rio BRT, as two feeds | CC BY 4.0, stated on both data.rio catalog pages. Keyless GTFS-Realtime published by the Secretaria Municipal de Transportes, labelled beta, with a gateway limit of 5 requests a second and 60 a minute. Line names come from SMTR’s GTFS `routes.txt` (dados.mobilidade.rio/gtfs/schedule), under the same licence. | Attribution to SMTR / Prefeitura da Cidade do Rio de Janeiro under CC BY 4.0 |
| **ZDiTM Szczecin** (zditm.szczecin.pl) | Transit layer live vehicles, Szczecin, Poland: buses and trams as one feed | CC0 1.0, stated on the ZDiTM developer page: “The data are provided free of charge under the CC0 1.0 licence, for use in any context, with no prior notification required.” Keyless GTFS-Realtime; the page says one update a minute, measured at about every 20 seconds. The feed carries no route id, so every vehicle takes the default bus mode. | None required (CC0); credited to ZDiTM Szczecin as a courtesy |
| **Carris Metropolitana** (api.carrismetropolitana.pt) | Transit layer live vehicles, Lisbon Metropolitan Area, Portugal | Keyless GTFS-Realtime documented in the operator’s public API repository (github.com/carrismetropolitana/api). No licence is stated for the realtime endpoint; the operator’s static GTFS is CC BY 4.0. Included on the same basis as the Metro Transit and OVapi feeds and credited as a courtesy; it can be switched off if Carris objects. Speeds are published in km/h and converted to m/s by the proxy. Route ids are read as the line number per the operator’s `/v2/routes`. | Courtesy credit to Carris Metropolitana |
| **Prague PID** (api.golemio.cz) | Transit layer live vehicles, Prague and Central Bohemia, Czechia | CC BY (https://pid.cz/o-systemu/opendata/). Stated: “Data, která lze stáhnout přímo zde z webu, jsou stejně jako celý obsah webu opatřena licencí CC-BY” The PID open-data page lists this realtime feed and licenses the data it publishes under CC BY, crediting the author and any changes. The same page says data served from other sources follows that source’s licence; the feed is served by Golemio, the city’s data platform, which states none of its own. Keyless since the catalogue moved it off the X-Access-Token endpoint. Speeds are m/s, checked against movement on 2026-11-07. Route ids are `L` plus the line number; lines below 100 are trams. | Pražská integrovaná doprava (PID) / Golemio |
| **Warsaw buses and trams** (mkuran.pl) | Transit layer live vehicles, Warsaw, Poland | Warsaw Open Data Project Usage Terms (https://mkuran.pl/gtfs/). Stated: “The City Hall requires a credit by providing the name Miasto Stołeczne Warszawa, data download date (use the timestamp from the FeedHeader) and link to http://api.um.warszawa.pl.” Positions come from the Warsaw City Hall API, repackaged as GTFS-RT by Mikołaj Kuranowski’s unofficial feed. The credit names the city and its API; the download date it asks for is the feed timestamp the layer already shows with each snapshot. Vehicle timestamps ran about two minutes old on 2026-11-07, which the delayed playback absorbs. | Miasto Stołeczne Warszawa (api.um.warszawa.pl) |
| **Bordeaux TBM** (bdx.mecatran.com) | Transit layer live vehicles, Bordeaux, France | Licence Ouverte / Etalab 2.0 (https://transport.data.gouv.fr/datasets/offres-de-services-bus-tram-et-scolaire-au-format-gtfs-netex-gtfs-rt-siri-lite). The quote is the licence field of the national access point’s dataset record (fr-lo is the Licence Ouverte), which asks only for credit. The `apiKey` in the URL is not a credential: it is the same fixed string printed in the open-data listing for everyone. Speeds are m/s. Trams run on lettered lines A–D; numbered lines are buses. | Bordeaux Métropole (TBM) |
| **Prague PID** (api.golemio.cz) | Transit layer live vehicles, Prague and Central Bohemia, Czechia | CC BY (https://pid.cz/o-systemu/opendata/). Stated: “Data, která lze stáhnout přímo zde z webu, jsou stejně jako celý obsah webu opatřena licencí CC-BY” The PID open-data page lists this realtime feed and licenses the data it publishes under CC BY, crediting the author and any changes. The same page says data served from other sources follows that source’s licence; the feed is served by Golemio, the city’s data platform, which states none of its own. Keyless since the catalogue moved it off the X-Access-Token endpoint. Speeds are m/s, checked against movement on 2026-11-07. Route ids are `L` plus a number whose range gives the mode and the line (metro `L991` is A, train `L1004` is S4, ferry `L1801` is P1), read from PID’s GTFS routes.txt. | Pražská integrovaná doprava (PID) / Golemio |
| **Warsaw buses and trams** (mkuran.pl) | Transit layer live vehicles, Warsaw, Poland | Warsaw Open Data Project Usage Terms (https://mkuran.pl/gtfs/). Stated: “The City Hall requires a credit by providing the name Miasto Stołeczne Warszawa, data download date (use the timestamp from the FeedHeader) and link to http://api.um.warszawa.pl.” Positions come from the Warsaw City Hall API, repackaged as GTFS-RT by Mikołaj Kuranowski’s unofficial feed. The credit names the city, links its API and carries the date of the feed timestamp, the three things the terms ask for. Vehicle timestamps ran about two minutes old on 2026-11-07, which the delayed playback absorbs. | Miasto Stołeczne Warszawa (api.um.warszawa.pl) |
| **Bordeaux TBM** (bdx.mecatran.com) | Transit layer live vehicles, Bordeaux, France | Licence Ouverte / Etalab 2.0 (https://transport.data.gouv.fr/datasets/offres-de-services-bus-tram-et-scolaire-au-format-gtfs-netex-gtfs-rt-siri-lite). The quote is the licence field of the national access point’s dataset record (fr-lo is the Licence Ouverte), which asks only for credit. The `apiKey` in the URL is not a credential: it is the same fixed string printed in the open-data listing for everyone. Speeds are m/s. The trams (A–F) and BAT³ boats ride numbered route ids (tram A is `59`), mapped from TBM’s static GTFS; other numbered lines are buses. | Bordeaux Métropole (TBM) |
| **Sendai City Bus** (api-public.odpt.org) | Transit layer live vehicles, Sendai, Japan | CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). Served by the Open Data Challenge for Public Transportation (ODPT) public API, which needs no key for this feed; the catalogue records CC BY 4.0 as its licence. Vehicle timestamps ran about two minutes old on 2026-11-07. Speeds are m/s. | 仙台市交通局 (Sendai City Transportation Bureau) / ODPT |
| **Poznań buses and trams** (ztm.poznan.pl) | Transit layer live vehicles, Poznań, Poland | ZTM Poznań open-data terms (https://www.ztm.poznan.pl/otwarte-dane/dla-deweloperow/). Stated: “Treści udostępniane są bezwnioskowo i bezpłatnie, na zasadach powszechnego dostępu i stanowią źródło informacji.” Reuse is free on three conditions: name the source, give the date of the last update, and say the data were processed. The credit names ZTM and says processed; the snapshot timestamp is the update date. The feed carries no bearing, so heading comes from movement between fixes. | Zarząd Transportu Miejskiego w Poznaniu (processed) |
| **Poznań buses and trams** (ztm.poznan.pl) | Transit layer live vehicles, Poznań, Poland | ZTM Poznań open-data terms (https://www.ztm.poznan.pl/otwarte-dane/dla-deweloperow/). Stated: “Treści udostępniane są bezwnioskowo i bezpłatnie, na zasadach powszechnego dostępu i stanowią źródło informacji.” Reuse is free on three conditions: name the source, give the date of the last update, and say the data were processed. The credit names ZTM, says processed, and carries the date of the feed timestamp as the update date. The feed carries no bearing, so heading comes from movement between fixes. | Zarząd Transportu Miejskiego w Poznaniu (processed) |
| **Gdańsk buses and trams** (ckan2.multimediagdansk.pl) | Transit layer live vehicles, Gdańsk, Poland | CC BY 4.0 (https://dane.gov.pl/pl/dataset/38599,rozklady-jazdy-i-lokalizacja-przystankow-gtfs-rt). The national catalogue (dane.gov.pl) lists this dataset under CC BY 4.0. No bearing; heading comes from movement. Speeds are m/s. | Zarząd Transportu Miejskiego w Gdańsku |
| **Edmonton Transit Service** (gtfs.edmonton.ca) | Transit layer live vehicles, Edmonton, AB | City of Edmonton Open Data Agreement (https://www.edmonton.ca/transportation/Web-version2.1-OpenDataAgreement.pdf). The agreement is the same municipal template as Mississauga and Burlington: a licence to use, modify and distribute for any lawful purpose, in any medium. It was read from search excerpts; the PDF itself would not extract here. Route ids are zero-padded. Speeds are m/s. | Contains information licensed under the Open Data Agreement – City of Edmonton |
| **Halifax Transit** (gtfs.halifax.ca) | Transit layer live vehicles, Halifax, NS | Open Government Licence – Halifax v1.0 (https://www.halifax.ca/home/open-data/open-data-license), modelled on OGL – Canada 2.0. Stated: “The information provider grants you a worldwide, royalty-free, perpetual, non-exclusive licence to use the Information, including for commercial purposes, subject to the terms below.” No logos or implied endorsement. Speeds are m/s. | Contains information licenced under the Open Government Licence – Halifax |
@@ -125,15 +125,15 @@ How to read this:
| **Venice ACTV waterbuses** (tpl.actv.it) | Transit layer live vehicles, Venice, Italy | ACTV open-data catalogue (https://actv.avmspa.it/it/content/catalogo-dei-dati-metadati-e-banche-dati-actv). Same catalogue and basis as the ACTV bus feed. | ACTV S.p.A. |
| **Adelaide Metro** (gtfs.adelaidemetro.com.au) | Transit layer live vehicles, Adelaide, SA | No licence stated for the realtime feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1772). Official keyless endpoint; the state catalogue page refused automated reads, so no licence is quoted. Same basis as Metro Transit and OVapi. Speeds are m/s. The server picks its encoding from Accept, hence the override. | Adelaide Metro |
| **MARTA buses** (gtfs-rt.itsmarta.com) | Transit layer live vehicles, Atlanta, GA | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1601). Official keyless endpoint; no licence found. Same basis as Metro Transit and OVapi. Speeds are m/s. | MARTA |
| **Denver RTD** (open-data.rtd-denver.com) | Transit layer live vehicles, Denver, CO | RTD GTFS-realtime licence agreement (https://www.rtd-denver.com/business-center/open-spatial-data/gtfs-realtime-license-agreement). RTD links a licence agreement for its realtime feeds; the page refused automated reads (403), so it is not quoted. The feed itself is keyless. Speeds are m/s. | Regional Transportation District (RTD) |
| **Denver RTD** (open-data.rtd-denver.com) | Transit layer live vehicles, Denver, CO | RTD GTFS-realtime licence agreement (https://www.rtd-denver.com/business-center/open-spatial-data/gtfs-realtime-license-agreement). Stated: “RTD hereby grants you non-exclusive, limited and revocable rights to use, reproduce, and redistribute the Data”. Read 2026-11-09: a click-through agreement that RTD may change without notice; it permits redistribution, asks for no credit wording, and bars RTD trademarks from use with the data, so the credit stays plain text. The feed itself is keyless. Speeds are m/s. | Regional Transportation District (RTD) |
| **Cluj-Napoca CTP** (cluj-rt-feed.gtfs.ro) | Transit layer live vehicles, Cluj-Napoca, Romania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2614). Keyless; no licence found. Same basis as Metro Transit and OVapi. | Compania de Transport Public Cluj-Napoca |
| **Iași CTP** (iasi-rt-feed.gtfs.ro) | Transit layer live vehicles, Iași, Romania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2118). Keyless; no licence found. Same basis as Metro Transit and OVapi. | Compania de Transport Public Iași |
| **Rzeszów city transport** (mpkrzeszow.pl) | Transit layer live vehicles, Rzeszów, Poland | MPK Rzeszów open data (https://www.mpkrzeszow.pl/otwartedane/). Listed on the operator’s open-data page, which names no licence. No bearing; heading comes from movement. | MPK Rzeszów |
| **Málaga EMT** (gtfs.emtmalaga.es) | Transit layer live vehicles, Málaga, Spain | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2213). Keyless; no licence found. Same basis as Metro Transit and OVapi. No bearing; heading comes from movement. | EMT Málaga |
| **Carris (Lisbon city)** (gateway.carris.pt) | Transit layer live vehicles, Lisbon, Portugal | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2930). Keyless official gateway; no licence found. Same basis as Metro Transit and OVapi. Carries neither bearing nor speed, and vehicle reports run about a minute old. Route ids append a variant. | Carris |
| **BAS.MY Johor Bahru** (api.data.gov.my) | Transit layer live vehicles, Johor Bahru, Malaysia | No licence stated for the feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3174). Served keyless by Malaysia’s national open-data API; no licence for the feed was found. Same basis as Metro Transit and OVapi. Without the trailing slash the API redirects. Route ids append an operator code. | BAS.MY / data.gov.my |
| **BAS.MY Kota Bharu** (api.data.gov.my) | Transit layer live vehicles, Kota Bharu, Malaysia | No licence stated for the feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3169). Same API and basis as Johor Bahru. | BAS.MY / data.gov.my |
| **Rouen Astuce** (gtfs.bus-tracker.fr) | Transit layer live vehicles, Rouen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie). The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`). | Métropole Rouen Normandie (Astuce) / bus-tracker.fr |
| **BAS.MY Johor Bahru** (api.data.gov.my) | Transit layer live vehicles, Johor Bahru, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Stated: “All GTFS Realtime feeds are licensed under CC BY 4.0.” Served keyless by Malaysia’s national open-data API, which caps GTFS Realtime at 4 requests a minute and refreshes positions every 30 seconds, so the proxy caches this feed for 30 seconds. Without the trailing slash the API redirects. Route ids append an operator code. | BAS.MY / data.gov.my |
| **BAS.MY Kota Bharu** (api.data.gov.my) | Transit layer live vehicles, Kota Bharu, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Same API, licence and rate limit as Johor Bahru. | BAS.MY / data.gov.my |
| **Rouen Astuce** (gtfs.bus-tracker.fr) | Transit layer live vehicles, Rouen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie). The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`); line 90 is the Métro (a street-running light rail, drawn as a tram) and 99 the Calypso boat. | Métropole Rouen Normandie (Astuce) / bus-tracker.fr |
| **Toulon Mistral** (feed-rdtpm-toulon.ratpdev.com) | Transit layer live vehicles, Toulon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-de-la-metropole-toulon-provence-mediterranee). Dataset record’s licence field quoted. Speeds are km/h: readings ran 3.5 times the measured movement on 2026-11-07, so the proxy converts them. Route ids are zero-padded. | Métropole Toulon Provence Méditerranée (Mistral) |
| **Le Havre LiA** (gtfs.bus-tracker.fr) | Transit layer live vehicles, Le Havre, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri). The network publishes under ODbL (dataset record quoted); read through the bus-tracker.fr relay, which converts its SIRI feed to GTFS-RT. The layer draws positions and redistributes no database. Lines A and B are the tramway. | Le Havre Seine Métropole (LiA) / bus-tracker.fr |
| **Tours Fil Bleu** (data.filbleu.fr) | Transit layer live vehicles, Tours, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/fil-bleu-syndicat-des-mobilites-gtfs-gtfs-rt). Dataset record’s licence field quoted. | Fil Bleu – Syndicat des Mobilités de Touraine |
+5 -2
View File
@@ -7,5 +7,8 @@
Edmonton, Halifax and Victoria. Each feed records its licence, or that none
is stated, in the registry and DATA_SOURCES.md. Speeds published in km/h
(Lisbon, Montpellier, Toulon) are converted to m/s by the proxy; route ids
read as the line on the vehicle; Prague, Bordeaux and Le Havre trams and
Venice waterbuses take their own mode (IMBrandao).
read as the line on the vehicle; Prague metro, trains, trams and ferries,
Bordeaux trams and boats, the Rouen Métro, Le Havre trams and Venice
waterbuses take their own mode. The Warsaw and Poznań credits carry the
data date their terms ask for, and the data.gov.my feeds are fetched at
most twice a minute to stay inside the API's rate limit (IMBrandao).
+24 -4
View File
@@ -660,20 +660,40 @@ export const OSM_PLACES_CREDIT = {
/**
* Per-feed transit credit, registered the first time that feed's vehicles
* render (see `src/data/transitFeeds.js` for the license of each).
* @param {{ id: string, attribution: string, license: string, licenseUrl: string }} feed
*
* Two optional registry fields serve licences that ask for more than a name:
* `sourceUrl` links the attribution to the publisher (Warsaw asks for a link
* to its API), and `creditDated` adds the UTC date of the data, from the feed
* header or else the fetch time (Warsaw and Poznań ask for the download or
* update date). A dated credit is keyed by its date, so a new day registers a
* new line instead of keeping yesterday's.
* @param {{ id: string, attribution: string, license: string, licenseUrl: string,
* sourceUrl?: string, creditDated?: boolean }} feed
* @param {{ feedTimestamp?: number|null, fetchedAt?: number|null }} [snapshot]
* `feedTimestamp` in epoch seconds, `fetchedAt` in epoch ms.
* @returns {{ key: string, html: string }}
*/
export function transitFeedCredit(feed) {
export function transitFeedCredit(feed, snapshot = null) {
const escape = (text) =>
String(text)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
const source = feed.sourceUrl
? `<a href="${escape(feed.sourceUrl)}" target="_blank" rel="noopener">${escape(feed.attribution)}</a>`
: escape(feed.attribution);
const dataMs = Number.isFinite(snapshot?.feedTimestamp)
? snapshot.feedTimestamp * 1000
: snapshot?.fetchedAt;
const date =
feed.creditDated === true && Number.isFinite(dataMs) && dataMs > 0
? new Date(dataMs).toISOString().slice(0, 10)
: null;
return {
key: `transit-${feed.id}`,
key: date ? `transit-${feed.id}-${date}` : `transit-${feed.id}`,
html:
`Transit (${escape(feed.attribution)}): ` +
`Transit (${source}${date ? `, data of ${date}` : ''}): ` +
`<a href="${escape(feed.licenseUrl)}" target="_blank" rel="noopener">${escape(feed.license)}</a>`,
};
}
+27 -1
View File
@@ -1,6 +1,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { DATA_CREDITS, NYC_DOT_DISCLAIMER } from './dataCredits.js';
import {
DATA_CREDITS,
NYC_DOT_DISCLAIMER,
transitFeedCredit,
} from './dataCredits.js';
import { getTransitFeed } from './transitFeeds.js';
test('every credit carries a unique key and some markup to render', () => {
const keys = DATA_CREDITS.map((entry) => entry.key);
@@ -167,3 +172,24 @@ test('the PR #1506 camera packs are credited with their sources and terms', () =
/href="https:\/\/www\.river\.go\.jp\//,
);
});
test('Warsaw and Poznań credits carry the data date their licences ask for', () => {
const warsaw = getTransitFeed('ztm-warszawa');
// 2026-11-09T06:00:00Z as a GTFS-RT header timestamp (seconds).
const credit = transitFeedCredit(warsaw, { feedTimestamp: 1_794_204_000 });
assert.match(credit.html, /, data of 2026-11-09\)/);
assert.match(credit.html, /href="http:\/\/api\.um\.warszawa\.pl"/);
assert.equal(credit.key, 'transit-ztm-warszawa-2026-11-09');
// Without a feed timestamp the fetch time is the download date.
const poznan = transitFeedCredit(getTransitFeed('ztm-poznan'), {
feedTimestamp: null,
fetchedAt: Date.UTC(2026, 10, 10, 12),
});
assert.match(poznan.html, /data of 2026-11-10/);
// Undated feeds keep one credit line for the session.
const mbta = transitFeedCredit(getTransitFeed('mbta'), {
feedTimestamp: 1_794_204_000,
});
assert.equal(mbta.key, 'transit-mbta');
assert.doesNotMatch(mbta.html, /data of/);
});
+129 -39
View File
@@ -416,8 +416,6 @@ function routeIdPattern(pattern) {
};
}
/** Prague PID: `L125` is line 125. */
const pidRouteLabel = routeIdPattern(/^L(\d+)$/);
/** Zero-padded numbers (Edmonton `005`, Toulon `0002`, Bordeaux `01`). */
const unpaddedRouteLabel = routeIdPattern(/^0+(\d+\w*)$/);
/** Turin GTT appends `U` (urban) to the line: `10NU` is night line 10N. */
@@ -434,35 +432,110 @@ const busTrackerRouteLabel = routeIdPattern(/^[A-Z]+:(.+)$/);
const mybasJohorRouteLabel = routeIdPattern(/^(.+?)CWLMYJB$/);
/**
* Le Havre LiA runs its two tram lines as A and B; every other line is a bus.
* Le Havre LiA runs its two tram lines as A and B and the funicular as
* `FUNI`; every other line is a bus.
* @param {string|null} routeId
* @returns {string}
*/
function liaRouteMode(routeId) {
if (!routeId) return 'unknown';
if (routeId === 'FUNI') return 'rail';
return /^(?:[A-Z]+:)?[AB]$/.test(routeId) ? 'tram' : 'bus';
}
/**
* Prague PID numbers its day and night trams below 100 and its buses from 100
* up; anything else (rail, ferries, the funicular) is left unresolved.
* @param {string|null} routeId
* @returns {string}
* Rouen Astuce lines that are not buses, by bus-tracker.fr route id, from the
* relay's own `astuce-tcar.zip` routes.txt (checked 2026-11-09): line 90 is
* the Métro (GTFS type 1, a light rail that runs mostly on the street) and 99
* the Calypso river boat.
*/
function pidRouteMode(routeId) {
const match = /^L(\d+)$/.exec(routeId ?? '');
if (!match) return 'unknown';
return Number(match[1]) < 100 ? 'tram' : 'bus';
const ASTUCE_ROUTES = Object.freeze({
'TCAR:90': Object.freeze({ mode: 'tram', label: 'Métro' }),
'TCAR:99': Object.freeze({ mode: 'ferry', label: 'Calypso' }),
});
/** @param {string|null} routeId @returns {string} */
function astuceRouteMode(routeId) {
if (!routeId) return 'unknown';
return ASTUCE_ROUTES[routeId]?.mode ?? 'bus';
}
/** `TCAR:93` is Rouen line 93; the Métro and Calypso keep their names. */
function astuceRouteLabel(routeId) {
return ASTUCE_ROUTES[routeId ?? '']?.label ?? busTrackerRouteLabel(routeId);
}
/**
* Bordeaux TBM runs trams on lettered lines A–D; every numbered line is a bus.
* Prague PID route ids are `L` plus a number whose range says what runs it,
* per PID's GTFS routes.txt (data.pid.cz, checked 2026-11-09): 1–99 trams
* (50–59 trolleybuses, 49 the Petřín funicular `LD`), 100–990 buses (the
* 800s are the X replacement lines, `L803` is X3), 991–993 metro A–C,
* 1000s and 1100s the S and R trains (`L1004` is S4, `L1109` R9), the rest
* of 1200–1399 other trains, 1700s town buses, 1800s the P ferries, and from
* 2000 more buses (`L2841` is 841). A label the range cannot give stays the
* route id.
* @param {string|null} routeId
* @returns {{mode: string, label: string|null}}
*/
function pidRoute(routeId) {
const match = /^L(\d+)$/.exec(routeId ?? '');
if (!match) return { mode: 'unknown', label: routeId };
const n = Number(match[1]);
if (n === 49) return { mode: 'rail', label: 'LD' };
if (n >= 50 && n <= 59) return { mode: 'bus', label: String(n) };
if (n < 100) return { mode: 'tram', label: String(n) };
if (n >= 800 && n < 900) return { mode: 'bus', label: `X${n - 800}` };
if (n < 991) return { mode: 'bus', label: String(n) };
if (n <= 993) return { mode: 'subway', label: 'ABC'[n - 991] };
if (n >= 1000 && n < 1100) return { mode: 'rail', label: `S${n - 1000}` };
if (n >= 1100 && n < 1200) return { mode: 'rail', label: `R${n - 1100}` };
if (n >= 1200 && n < 1400) return { mode: 'rail', label: routeId };
if (n >= 1800 && n < 1900) return { mode: 'ferry', label: `P${n - 1800}` };
if (n >= 2800 && n < 2900) return { mode: 'bus', label: String(n - 2000) };
return { mode: n >= 1700 ? 'bus' : 'unknown', label: routeId };
}
/** @param {string|null} routeId @returns {string} */
function pidRouteMode(routeId) {
return pidRoute(routeId).mode;
}
/** @param {string|null} routeId @returns {string|null} */
function pidRouteLabel(routeId) {
return pidRoute(routeId).label;
}
/**
* Bordeaux TBM tram and boat lines by route id, from TBM's static GTFS
* routes.txt (checked 2026-11-09): the trams are numbered ids, not letters.
*/
const TBM_TRAM_LINES = Object.freeze({
59: 'A',
60: 'B',
61: 'C',
62: 'D',
163: 'E',
164: 'F',
});
const TBM_BOAT_LINES = new Set(['951', '952', '953']);
/**
* Bordeaux TBM: trams A–F and the BAT³ river shuttles; every other line is a
* bus.
* @param {string|null} routeId
* @returns {string}
*/
function tbmRouteMode(routeId) {
if (!routeId) return 'unknown';
return /^[A-D]$/.test(routeId) ? 'tram' : 'bus';
if (Object.hasOwn(TBM_TRAM_LINES, routeId)) return 'tram';
return TBM_BOAT_LINES.has(routeId) ? 'ferry' : 'bus';
}
/** `59` is tram A; zero-padded bus ids lose their padding (`01` is 1). */
function tbmRouteLabel(routeId) {
return Object.hasOwn(TBM_TRAM_LINES, routeId ?? '')
? TBM_TRAM_LINES[routeId]
: unpaddedRouteLabel(routeId);
}
/**
@@ -491,10 +564,14 @@ function transitFeed(fields) {
* url: string, headers?: Record<string, string>,
* license: string, licenseUrl: string, attribution: string,
* defaultMode: string, routeMode?: (routeId: string|null) => string,
* speedUnit?: 'km/h',
* speedUnit?: 'km/h', refreshMs?: number, sourceUrl?: string,
* creditDated?: boolean,
* }>>}
* `speedUnit` marks a feed that publishes km/h where the spec requires m/s;
* the proxy converts it before the snapshot leaves the server.
* the proxy converts it before the snapshot leaves the server. `refreshMs`
* stretches the proxy's cache for an upstream that rate-limits below one
* request per 15 s TTL. `sourceUrl` and `creditDated` add a link and a date
* to the credit for licences that ask for them (`transitFeedCredit`).
*/
export const TRANSIT_FEED_REGISTRY = Object.freeze([
Object.freeze({
@@ -884,7 +961,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
defaultMode: 'bus',
routeLabel: rioRouteLabel,
}),
Object.freeze({
transitFeed({
id: 'zditm-szczecin',
name: 'Szczecin buses and trams',
operator:
@@ -892,40 +969,38 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
region: 'Szczecin, Poland',
// Every vehicle in a 2026-11-05 snapshot sat between 53.34–53.55 N and
// 14.44–14.77 E, inside 20 km of this point.
center: Object.freeze({ lat: 53.44, lon: 14.56 }),
center: { lat: 53.44, lon: 14.56 },
loadRadiusKm: 25,
url: 'https://www.zditm.szczecin.pl/storage/gtfs/gtfs-rt-vehicles.pb',
license: 'CC0 1.0',
licenseUrl: 'https://www.zditm.szczecin.pl/en/zditm/for-developers',
attribution:
'Zarząd Dróg i Transportu Miejskiego w Szczecinie (ZDiTM Szczecin)',
defaultEnabled: true,
terms: Object.freeze({
terms: {
quote:
'The data are provided free of charge under the CC0 1.0 licence, for use in any context, with no prior notification required.',
note: 'CC0 needs no credit; it is given anyway as a courtesy. ZDiTM gives no guarantee of accuracy or completeness. The developer page says the feed updates every minute; measured on 2026-11-05 the file changed about every 20 seconds, with vehicle timestamps up to 50 seconds old. Speeds are m/s: checked against vehicle movement between two snapshots. The feed carries no route id at all, so every vehicle takes the feed default mode even though the city also runs trams.',
}),
},
defaultMode: 'bus',
}),
Object.freeze({
transitFeed({
id: 'carris-metropolitana-lisboa',
name: 'Carris Metropolitana',
operator: 'Carris Metropolitana (TML), Lisbon Metropolitan Area',
region: 'Lisbon Metropolitan Area, Portugal',
// Every vehicle in a 2026-11-07 snapshot, from Mafra to Setúbal, sat
// between 38.47–39.00 N and 9.47–8.79 W, inside 40 km of this point.
center: Object.freeze({ lat: 38.73, lon: -9.13 }),
center: { lat: 38.73, lon: -9.13 },
loadRadiusKm: 45,
url: 'https://api.carrismetropolitana.pt/v2/vehicles.pb',
license: 'No separate licence for the realtime feed',
licenseUrl: 'https://github.com/carrismetropolitana/api',
attribution: 'Carris Metropolitana',
defaultEnabled: true,
terms: Object.freeze({
terms: {
quote:
'GET /vehicles.pb — Returns vehicle locations in Protobuf format, following the GTFS-RT Vehicle Locations standard.',
note: 'The operator publishes this feed keyless and documented for developers in its public API repository, but states no licence for the realtime endpoint; its static GTFS (github.com/carrismetropolitana/gtfs) is CC BY 4.0. Credited as a courtesy, on the same basis as the Metro Transit and OVapi feeds; switch it off if Carris objects. The quote above is the API README, not a licence. `/vehicles.pb` answers with a 307 to `/v2/vehicles.pb`, so the registry points at the target. Speeds are km/h, not the m/s the spec requires: 2026-11-07 readings of 31–33 matched 6–9 m/s of measured movement, so the proxy converts them. Vehicle reports arrive every 30–50 seconds. Route ids are the GTFS route id inside an area prefix and are read as the line number.',
}),
},
defaultMode: 'bus',
routeLabel: carrisMetropolitanaRouteLabel,
speedUnit: 'km/h',
@@ -946,7 +1021,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
terms: {
quote:
'Data, která lze stáhnout přímo zde z webu, jsou stejně jako celý obsah webu opatřena licencí CC-BY',
note: 'The PID open-data page lists this realtime feed and licenses the data it publishes under CC BY, crediting the author and any changes. The same page says data served from other sources follows that source’s licence; the feed is served by Golemio, the city’s data platform, which states none of its own. Keyless since the catalogue moved it off the X-Access-Token endpoint. Speeds are m/s, checked against movement on 2026-11-07. Route ids are `L` plus the line number; lines below 100 are trams.',
note: 'The PID open-data page lists this realtime feed and licenses the data it publishes under CC BY, crediting the author and any changes. The same page says data served from other sources follows that source’s licence; the feed is served by Golemio, the city’s data platform, which states none of its own. Keyless since the catalogue moved it off the X-Access-Token endpoint. Speeds are m/s, checked against movement on 2026-11-07. Route ids are `L` plus a number whose range gives the mode and the line (metro `L991` is A, train `L1004` is S4, ferry `L1801` is P1); `pidRoute` holds the table, read from PID’s GTFS routes.txt on 2026-11-09.',
},
defaultMode: 'bus',
routeLabel: pidRouteLabel,
@@ -963,10 +1038,12 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
license: 'Warsaw Open Data Project Usage Terms',
licenseUrl: 'https://mkuran.pl/gtfs/',
attribution: 'Miasto Stołeczne Warszawa (api.um.warszawa.pl)',
sourceUrl: 'http://api.um.warszawa.pl',
creditDated: true,
terms: {
quote:
'The City Hall requires a credit by providing the name Miasto Stołeczne Warszawa, data download date (use the timestamp from the FeedHeader) and link to http://api.um.warszawa.pl.',
note: 'Positions come from the Warsaw City Hall API, repackaged as GTFS-RT by Mikołaj Kuranowski’s unofficial feed. The credit names the city and its API; the download date it asks for is the feed timestamp the layer already shows with each snapshot. Vehicle timestamps ran about two minutes old on 2026-11-07, which the delayed playback absorbs.',
note: 'Positions come from the Warsaw City Hall API, repackaged as GTFS-RT by Mikołaj Kuranowski’s unofficial feed. The credit names the city, links its API (`sourceUrl`) and carries the date of the feed timestamp (`creditDated`), the three things the terms ask for. Vehicle timestamps ran about two minutes old on 2026-11-07, which the delayed playback absorbs.',
},
defaultMode: 'bus',
}),
@@ -984,10 +1061,10 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
attribution: 'Bordeaux Métropole (TBM)',
terms: {
quote: 'licence: fr-lo',
note: 'The quote is the licence field of the national access point’s dataset record (fr-lo is the Licence Ouverte), which asks only for credit. The `apiKey` in the URL is not a credential: it is the same fixed string printed in the open-data listing for everyone. Speeds are m/s. Trams run on lettered lines A–D; numbered lines are buses.',
note: 'The quote is the licence field of the national access point’s dataset record (fr-lo is the Licence Ouverte), which asks only for credit. The `apiKey` in the URL is not a credential: it is the same fixed string printed in the open-data listing for everyone. Speeds are m/s. The trams (A–F) and BAT³ boats ride numbered route ids (tram A is `59`), so `tbmRouteMode` and `tbmRouteLabel` map them from TBM’s static GTFS; other numbered lines are buses.',
},
defaultMode: 'bus',
routeLabel: unpaddedRouteLabel,
routeLabel: tbmRouteLabel,
routeMode: tbmRouteMode,
}),
transitFeed({
@@ -1018,10 +1095,11 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
license: 'ZTM Poznań open-data terms',
licenseUrl: 'https://www.ztm.poznan.pl/otwarte-dane/dla-deweloperow/',
attribution: 'Zarząd Transportu Miejskiego w Poznaniu (processed)',
creditDated: true,
terms: {
quote:
'Treści udostępniane są bezwnioskowo i bezpłatnie, na zasadach powszechnego dostępu i stanowią źródło informacji.',
note: 'Reuse is free on three conditions: name the source, give the date of the last update, and say the data were processed. The credit names ZTM and says processed; the snapshot timestamp is the update date. The feed carries no bearing, so heading comes from movement between fixes.',
note: 'Reuse is free on three conditions: name the source, give the date of the last update, and say the data were processed. The credit names ZTM, says processed, and carries the date of the feed timestamp (`creditDated`) as the update date. The feed carries no bearing, so heading comes from movement between fixes.',
},
defaultMode: 'bus',
}),
@@ -1295,7 +1373,9 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
'https://www.rtd-denver.com/business-center/open-spatial-data/gtfs-realtime-license-agreement',
attribution: 'Regional Transportation District (RTD)',
terms: {
note: 'RTD links a licence agreement for its realtime feeds; the page refused automated reads (403), so it is not quoted. The feed itself is keyless. Speeds are m/s.',
quote:
'RTD hereby grants you non-exclusive, limited and revocable rights to use, reproduce, and redistribute the Data',
note: 'Read 2026-11-09. The agreement is a click-through (“By clicking to download the Data files, you confirm that you have read and agree”) that RTD may change without notice; it permits redistribution, asks for no credit wording, and bars RTD trademarks from being used with the data, so the credit stays plain text. The feed itself is keyless. Speeds are m/s.',
},
defaultMode: 'bus',
}),
@@ -1388,11 +1468,16 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
center: { lat: 1.51, lon: 103.75 },
loadRadiusKm: 35,
url: 'https://api.data.gov.my/gtfs-realtime/vehicle-position/mybas-johor/',
license: 'No licence stated for the feed',
licenseUrl: 'https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3174',
license: 'CC BY 4.0',
licenseUrl: 'https://developer.data.gov.my/realtime-api/gtfs-realtime',
attribution: 'BAS.MY / data.gov.my',
// data.gov.my allows 4 GTFS-Realtime requests a minute and refreshes
// vehicle positions every 30 seconds, so the proxy asks half as often as
// the default 15 s TTL would.
refreshMs: 30_000,
terms: {
note: 'Served keyless by Malaysia’s national open-data API; no licence for the feed was found. Same basis as Metro Transit and OVapi. Without the trailing slash the API redirects. Route ids append an operator code.',
quote: 'All GTFS Realtime feeds are licensed under CC BY 4.0.',
note: 'Served keyless by Malaysia’s national open-data API (developer.data.gov.my, read 2026-11-09). Its rate-limit page caps GTFS Realtime at 4 requests a minute and answers 429 above that; the feed updates every 30 seconds, hence `refreshMs`. Without the trailing slash the API redirects. Route ids append an operator code.',
},
defaultMode: 'bus',
routeLabel: mybasJohorRouteLabel,
@@ -1406,10 +1491,14 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
center: { lat: 6.06, lon: 102.22 },
loadRadiusKm: 60,
url: 'https://api.data.gov.my/gtfs-realtime/vehicle-position/mybas-kota-bharu/',
license: 'No licence stated for the feed',
licenseUrl: 'https://mobilitydatabase.org/feeds/gtfs_rt/mdb-3169',
license: 'CC BY 4.0',
licenseUrl: 'https://developer.data.gov.my/realtime-api/gtfs-realtime',
attribution: 'BAS.MY / data.gov.my',
terms: { note: 'Same API and basis as Johor Bahru.' },
refreshMs: 30_000,
terms: {
quote: 'All GTFS Realtime feeds are licensed under CC BY 4.0.',
note: 'Same API, licence and rate limit as Johor Bahru.',
},
defaultMode: 'bus',
}),
transitFeed({
@@ -1426,10 +1515,11 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([
attribution: 'Métropole Rouen Normandie (Astuce) / bus-tracker.fr',
terms: {
quote: 'licence: lov2',
note: 'The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`).',
note: 'The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`); `TCAR:90` is the Métro (a street-running light rail, drawn as a tram) and `TCAR:99` the Calypso boat.',
},
defaultMode: 'bus',
routeLabel: busTrackerRouteLabel,
routeLabel: astuceRouteLabel,
routeMode: astuceRouteMode,
}),
transitFeed({
id: 'mpm-toulon',
+56 -1
View File
@@ -701,7 +701,6 @@ test('trams, waterbuses and buses are told apart where the ids allow it', () =>
const mode = (id, routeId) => transitModeFor(getTransitFeed(id), routeId);
assert.equal(mode('pid-praha', 'L22'), 'tram');
assert.equal(mode('pid-praha', 'L135'), 'bus');
assert.equal(mode('tbm-bordeaux', 'A'), 'tram');
assert.equal(mode('tbm-bordeaux', '16'), 'bus');
assert.equal(mode('lia-le-havre', 'B'), 'tram');
assert.equal(mode('lia-le-havre', '3'), 'bus');
@@ -719,3 +718,59 @@ test('feeds that publish km/h are marked for conversion, and only those', () =>
'tam-montpellier',
]);
});
// Route ids as the live feeds and the operators' own GTFS routes.txt carried
// them on 2026-11-09: the ranges and numbered tram ids are not guessable from
// the line names riders see.
test('Prague route-id ranges give metro, trains, ferries and trolleybuses their mode', () => {
const feed = getTransitFeed('pid-praha');
const read = (routeId) => [
transitModeFor(feed, routeId),
transitRouteLabel(feed, routeId),
];
assert.deepEqual(read('L22'), ['tram', '22']);
assert.deepEqual(read('L58'), ['bus', '58'], 'trolleybus');
assert.deepEqual(read('L49'), ['rail', 'LD'], 'Petřín funicular');
assert.deepEqual(read('L135'), ['bus', '135']);
assert.deepEqual(read('L803'), ['bus', 'X3']);
assert.deepEqual(read('L991'), ['subway', 'A']);
assert.deepEqual(read('L993'), ['subway', 'C']);
assert.deepEqual(read('L1004'), ['rail', 'S4']);
assert.deepEqual(read('L1109'), ['rail', 'R9']);
assert.deepEqual(read('L1204'), ['rail', 'L1204'], 'no label to derive');
assert.deepEqual(read('L1801'), ['ferry', 'P1']);
assert.deepEqual(read('L1703'), ['bus', 'L1703']);
assert.deepEqual(read('L2841'), ['bus', '841']);
assert.deepEqual(read('S4'), ['bus', 'S4'], 'unknown ids keep the default');
});
test('Bordeaux trams ride numbered route ids and keep their letters', () => {
const feed = getTransitFeed('tbm-bordeaux');
for (const [routeId, line] of [
['59', 'A'],
['62', 'D'],
['163', 'E'],
]) {
assert.equal(transitModeFor(feed, routeId), 'tram', routeId);
assert.equal(transitRouteLabel(feed, routeId), line);
}
assert.equal(transitModeFor(feed, '952'), 'ferry');
assert.equal(transitModeFor(feed, '01'), 'bus');
assert.equal(transitRouteLabel(feed, '01'), '1');
});
test('Rouen Métro and Calypso are not buses', () => {
const feed = getTransitFeed('astuce-rouen');
assert.equal(transitModeFor(feed, 'TCAR:90'), 'tram');
assert.equal(transitRouteLabel(feed, 'TCAR:90'), 'Métro');
assert.equal(transitModeFor(feed, 'TCAR:99'), 'ferry');
assert.equal(transitModeFor(feed, 'TCAR:93'), 'bus');
assert.equal(transitRouteLabel(feed, 'TAE:F'), 'F');
assert.equal(transitModeFor(getTransitFeed('lia-le-havre'), 'FUNI'), 'rail');
});
test('data.gov.my feeds ask the proxy for no more than its 4 a minute', () => {
for (const id of ['mybas-johor', 'mybas-kota-bharu']) {
assert.ok(getTransitFeed(id).refreshMs >= 30_000, id);
}
});
+17 -2
View File
@@ -268,17 +268,32 @@ export function buildTransitSnapshot(feed, bytes, now = Date.now()) {
};
}
/**
* Fresh window for one feed's cached snapshot: the shared TTL, stretched by a
* registry `refreshMs` for upstreams that rate-limit below one request per
* TTL (data.gov.my allows 4 a minute). Never shorter than the shared TTL.
* @param {{ refreshMs?: number }|null|undefined} feed Registry entry.
* @returns {number}
*/
export function transitFeedTtlMs(feed) {
const refresh = feed?.refreshMs;
return Number.isFinite(refresh) && refresh > TRANSIT_PROXY_TTL_MS
? refresh
: TRANSIT_PROXY_TTL_MS;
}
/**
* Classify a cache entry for the request policy.
* @param {{ at: number }|null|undefined} entry Cached snapshot (`at` = fetch ms).
* @param {number} now
* @param {number} [ttlMs] Fresh window; `transitFeedTtlMs(feed)` for a feed.
* @returns {'none'|'fresh'|'stale'|'expired'}
*/
export function transitCacheState(entry, now) {
export function transitCacheState(entry, now, ttlMs = TRANSIT_PROXY_TTL_MS) {
if (!entry || !Number.isFinite(entry.at)) return 'none';
const age = now - entry.at;
if (age < 0) return 'fresh';
if (age < TRANSIT_PROXY_TTL_MS) return 'fresh';
if (age < ttlMs) return 'fresh';
if (age < TRANSIT_PROXY_STALE_MAX_MS) return 'stale';
return 'expired';
}
+12
View File
@@ -12,6 +12,7 @@ import {
repairVehicleTimestamps,
resolveTransitRoute,
transitCacheState,
transitFeedTtlMs,
transitRedirectDecision,
transitResponseHeaders,
transitUpstreamHeaders,
@@ -406,3 +407,14 @@ test('Adelaide asks for */* because a protobuf Accept gets base64 text back', ()
/protobuf/,
);
});
test('a feed refreshMs stretches its fresh window, never shortens it', () => {
const johor = getTransitFeed('mybas-johor');
assert.equal(transitFeedTtlMs(johor), 30_000);
assert.equal(transitFeedTtlMs(getTransitFeed('mbta')), TRANSIT_PROXY_TTL_MS);
assert.equal(transitFeedTtlMs({ refreshMs: 1_000 }), TRANSIT_PROXY_TTL_MS);
const now = 1_000_000;
const entry = { at: now - 20_000 };
assert.equal(transitCacheState(entry, now), 'stale');
assert.equal(transitCacheState(entry, now, transitFeedTtlMs(johor)), 'fresh');
});
+7 -1
View File
@@ -401,7 +401,13 @@ export function createIngestion({ state, services, parts, source }) {
);
}
if (state._viewer)
registerDynamicCredit(state._viewer, transitFeedCredit(feed));
registerDynamicCredit(
state._viewer,
transitFeedCredit(feed, {
feedTimestamp: snapshot?.feedTimestamp,
fetchedAt,
}),
);
if (
state._selectedKey &&
state._vehicles.get(state._selectedKey)?.feedId === feed.id
+2 -1
View File
@@ -25,6 +25,7 @@ import {
nextTransitBackoffMs,
resolveTransitRoute,
transitCacheState,
transitFeedTtlMs,
transitRedirectDecision,
transitResponseHeaders,
transitUpstreamHeaders,
@@ -266,7 +267,7 @@ export function createTransitService({ fetchImpl = fetch } = {}) {
);
}
const cached = cache.get(feed.id);
const state = transitCacheState(cached, now);
const state = transitCacheState(cached, now, transitFeedTtlMs(feed));
if (state === 'fresh') {
return reply(
200,