+72
-1
@@ -24,2 +24,3 @@ import { defaultEndpoints, subwayRouteColors } from "./src/defaults"; | ||
| MTAOptions, | ||
| NearbyStop, | ||
| Route, | ||
@@ -45,2 +46,4 @@ Stop, | ||
| readonly now: () => Date; | ||
| readonly apiKey?: string; | ||
| readonly apiBaseUrl: string; | ||
| readonly busTimeKey?: string; | ||
@@ -57,2 +60,4 @@ readonly endpoints: MTAEndpoints; | ||
| this.now = options.now ?? (() => new Date()); | ||
| this.apiKey = options.apiKey; | ||
| this.apiBaseUrl = options.apiBaseUrl ?? "https://www.mtaapi.dev"; | ||
| this.busTimeKey = options.busTimeKey; | ||
@@ -120,2 +125,24 @@ this.realtimeCacheTtlMs = options.realtimeCacheTtlMs ?? 15_000; | ||
| } | ||
| hostedApiEnabled() { | ||
| return Boolean(this.apiKey); | ||
| } | ||
| async hostedJson<T>(path: string, query: object = {}): Promise<T> { | ||
| if (!this.apiKey) { | ||
| throw new Error("mta-js hosted API calls require an apiKey."); | ||
| } | ||
| const url = urlWithParams( | ||
| new URL(path, this.apiBaseUrl).toString(), | ||
| serializeHostedQuery(query), | ||
| ); | ||
| return fetchJson(this.fetch, url, { | ||
| headers: { | ||
| Authorization: `Bearer ${this.apiKey}`, | ||
| "x-api-key": this.apiKey, | ||
| }, | ||
| }) as Promise<T>; | ||
| } | ||
| } | ||
@@ -142,2 +169,6 @@ | ||
| async status(): Promise<DatabaseStatus> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<DatabaseStatus>("/api/v1/database/status"); | ||
| } | ||
| await this.mta.ready(); | ||
@@ -214,2 +245,6 @@ return this.mta.static.status(); | ||
| }): Promise<Arrival[]> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<Arrival[]>("/api/v1/subway/arrivals", query); | ||
| } | ||
| await this.mta.ready(); | ||
@@ -297,2 +332,6 @@ const routeIds = query.route ? [normalizeRouteId(query.route)] : Object.keys(this.mta.endpoints.subwayFeeds); | ||
| async arrivals(query: BusArrivalQuery): Promise<Arrival[]> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<Arrival[]>("/api/v1/bus/arrivals", query); | ||
| } | ||
| await this.mta.ready(); | ||
@@ -342,2 +381,6 @@ const key = this.requireKey(); | ||
| async vehicles(query: BusVehicleQuery = {}): Promise<Vehicle[]> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<Vehicle[]>("/api/v1/bus/vehicles", query); | ||
| } | ||
| await this.mta.ready(); | ||
@@ -389,2 +432,6 @@ const key = this.requireKey(); | ||
| async current(query: AlertQuery = {}): Promise<Alert[]> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<Alert[]>("/api/v1/alerts", query); | ||
| } | ||
| await this.mta.ready(); | ||
@@ -431,3 +478,7 @@ const feed = await this.mta.realtimeFeed(this.mta.endpoints.alerts); | ||
| near(query: StopsNearQuery): Promise<Stop[]> { | ||
| near(query: StopsNearQuery): Promise<NearbyStop[]> { | ||
| if (this.mta.hostedApiEnabled()) { | ||
| return this.mta.hostedJson<NearbyStop[]>("/api/v1/stops/near", query); | ||
| } | ||
| return this.mta.ready().then(() => { | ||
@@ -442,2 +493,22 @@ for (const mode of query.modes ?? []) { | ||
| function serializeHostedQuery(query: object) { | ||
| const params: Record<string, string | number | boolean | undefined> = {}; | ||
| for (const [key, value] of Object.entries(query)) { | ||
| if ( | ||
| value === undefined || | ||
| typeof value === "string" || | ||
| typeof value === "number" || | ||
| typeof value === "boolean" | ||
| ) { | ||
| params[key] = value; | ||
| continue; | ||
| } | ||
| if (Array.isArray(value)) { | ||
| params[key] = value.join(","); | ||
| } | ||
| } | ||
| return params; | ||
| } | ||
| function normalizeRouteId(route: string) { | ||
@@ -444,0 +515,0 @@ return route.toUpperCase().trim(); |
+1
-1
| { | ||
| "name": "mta-js", | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", | ||
| "description": "A TypeScript client for MTA realtime and static GTFS data.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+43
-0
@@ -19,2 +19,37 @@ # mta-js | ||
| ## Hosted API | ||
| Pass an `apiKey` to use the hosted MTA API instead of calling MTA feeds and local | ||
| static GTFS directly. The public method names stay the same, but requests are sent | ||
| to `https://www.mtaapi.dev/api/v1` with the key attached. | ||
| ```ts | ||
| import { MTA } from "mta-js"; | ||
| const mta = new MTA({ | ||
| apiKey: process.env.MTA_API_KEY, | ||
| }); | ||
| await mta.subway.arrivals({ stopId: "A27", route: "A" }); | ||
| await mta.bus.arrivals({ stopId: "308214", route: "M23" }); | ||
| await mta.bus.vehicles({ route: "M23", limit: 5 }); | ||
| await mta.alerts.current({ mode: "subway" }); | ||
| await mta.stops.near({ | ||
| lat: 40.7356, | ||
| lon: -73.9804, | ||
| modes: ["subway", "bus"], | ||
| route: "M23", | ||
| includeRoutes: true, | ||
| }); | ||
| ``` | ||
| Use `apiBaseUrl` to point at a preview, staging, or self-hosted compatible API: | ||
| ```ts | ||
| const mta = new MTA({ | ||
| apiKey: process.env.MTA_API_KEY, | ||
| apiBaseUrl: "https://staging.example.com", | ||
| }); | ||
| ``` | ||
| ## Database | ||
@@ -319,2 +354,10 @@ | ||
| ## Roadmap | ||
| - Expand the package beyond NYC MTA into a normalized US metro SDK while keeping the existing MTA API stable. | ||
| - Prioritize large systems with documented APIs and realtime data first: MBTA (Boston), WMATA (DC), CTA (Chicago), and BART (Bay Area). | ||
| - Add adapters for mid-tier systems that expose GTFS, GTFS Realtime, or partial APIs, including SEPTA, LA Metro, and MARTA. | ||
| - Model agency-specific quirks behind shared concepts like arrivals, vehicles, service alerts, route-aware nearby stops, static GTFS import, and hosted API access. | ||
| - Track smaller agencies separately because support quality varies: some only publish static GTFS, some have buried realtime feeds, and some have no public API surface. | ||
| ## Development | ||
@@ -321,0 +364,0 @@ |
+6
-3
@@ -11,4 +11,4 @@ import { FeedError } from "./errors"; | ||
| export async function fetchJson(fetchImpl: typeof fetch, url: string) { | ||
| const response = await fetchImpl(url); | ||
| export async function fetchJson(fetchImpl: typeof fetch, url: string, init?: RequestInit) { | ||
| const response = await fetchImpl(url, init); | ||
| if (!response.ok) { | ||
@@ -20,3 +20,6 @@ throw new FeedError(`MTA API request failed: ${response.status} ${response.statusText}`, response); | ||
| export function urlWithParams(base: string, params: Record<string, string | number | undefined>) { | ||
| export function urlWithParams( | ||
| base: string, | ||
| params: Record<string, string | number | boolean | undefined>, | ||
| ) { | ||
| const url = new URL(base); | ||
@@ -23,0 +26,0 @@ for (const [key, value] of Object.entries(params)) { |
+12
-0
@@ -6,2 +6,4 @@ export type TransitMode = "subway" | "bus" | "lirr" | "metro-north"; | ||
| export interface MTAOptions { | ||
| apiKey?: string; | ||
| apiBaseUrl?: string; | ||
| busTimeKey?: string; | ||
@@ -122,2 +124,10 @@ databaseUrl?: string; | ||
| export type NearbyStop = Stop & { | ||
| distanceMeters?: number; | ||
| servedRoutes?: Route[]; | ||
| routeMatch?: boolean; | ||
| routeHeadsigns?: string[]; | ||
| note?: string; | ||
| }; | ||
| export interface Arrival { | ||
@@ -201,4 +211,6 @@ mode: TransitMode; | ||
| modes?: TransitMode[]; | ||
| route?: string; | ||
| includeRoutes?: boolean; | ||
| radiusMeters?: number; | ||
| limit?: number; | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
72286
5.56%1842
4.13%368
13.23%22
4.76%