🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

mta-js

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mta-js - npm Package Compare versions

Comparing version
2.0.0
to
2.1.0
+193
scripts/generate-known-types.ts
import { parse } from "csv-parse/sync"
import { unzipSync } from "fflate"
const defaultBusGtfsUrls = [
"https://rrgtfsfeeds.s3.amazonaws.com/gtfs_b.zip",
"https://rrgtfsfeeds.s3.amazonaws.com/gtfs_bx.zip",
"https://rrgtfsfeeds.s3.amazonaws.com/gtfs_m.zip",
"https://rrgtfsfeeds.s3.amazonaws.com/gtfs_q.zip",
"https://rrgtfsfeeds.s3.amazonaws.com/gtfs_si.zip",
]
const snapshotUrl =
process.env.MTA_STOPS_SNAPSHOT_URL ??
process.env.NEXT_PUBLIC_MTA_STOPS_SNAPSHOT_URL
if (!snapshotUrl) {
throw new Error(
"MTA_STOPS_SNAPSHOT_URL or NEXT_PUBLIC_MTA_STOPS_SNAPSHOT_URL is required to generate known MTA types.",
)
}
const busGtfsUrls = (
process.env.MTA_BUS_GTFS_URLS ??
process.env.MTA_BUS_GTFS_URL ??
defaultBusGtfsUrls.join(",")
)
.split(",")
.map((url) => url.trim())
.filter(Boolean)
type SnapshotStop = {
id: string
mode?: string
}
type SnapshotRoute = {
id?: string
shortName?: string
type?: number
}
type Snapshot = {
generatedAt?: string
stops?: SnapshotStop[]
stopRoutes?: Record<string, SnapshotRoute[]>
indexes?: {
routesToStops?: Record<string, string[]>
}
}
type GtfsRoute = {
route_id: string
route_short_name?: string
route_long_name?: string
route_type?: string
route_color?: string
route_text_color?: string
}
const snapshot = (await fetch(snapshotUrl).then((response) => {
if (!response.ok) {
throw new Error(`Unable to fetch stops snapshot: ${response.status} ${response.statusText}`)
}
return response.json()
})) as Snapshot
const stops = snapshot.stops ?? []
const routeEntries = Object.entries(snapshot.indexes?.routesToStops ?? {})
const routeMetadata = new Map<string, SnapshotRoute>()
const routeTypes = new Map<string, number>()
for (const route of await loadBusGtfsRoutes(busGtfsUrls)) {
for (const id of [route.id, route.shortName]) {
if (!id) continue
routeMetadata.set(id, route)
routeTypes.set(id, 3)
}
}
for (const routes of Object.values(snapshot.stopRoutes ?? {})) {
for (const route of routes) {
for (const id of [route.id, route.shortName]) {
if (!id) continue
routeMetadata.set(id, route)
if (route.type !== undefined) routeTypes.set(id, route.type)
}
}
}
function aliasesForRoute(routeId: string) {
const route = routeMetadata.get(routeId)
const aliases = new Set([routeId, route?.id, route?.shortName].filter((id): id is string => Boolean(id)))
for (const id of [...aliases]) {
if (id.endsWith("-SBS")) aliases.add(id.replace(/-SBS$/, ""))
if (id.endsWith("+")) aliases.add(id.replace(/\+$/, ""))
}
return [...aliases]
}
function routeType(routeId: string) {
return (
routeTypes.get(routeId) ??
routeTypes.get(`${routeId}-SBS`) ??
routeTypes.get(`${routeId}+`) ??
routeMetadata.get(routeId)?.type
)
}
function looksLikeSubwayRoute(routeId: string) {
if (routeType(routeId) !== undefined) return false
return /^[A-Z0-9]{1,2}$/.test(routeId)
}
const subwayRouteAliases = new Set(["SI", "SIR"])
const routeIds = [
...new Set([
...routeEntries.flatMap(([routeId]) => aliasesForRoute(routeId)),
...[...routeMetadata.keys()].flatMap((routeId) => aliasesForRoute(routeId)),
]),
]
const subwayRoutes = routeIds.filter(
(routeId) =>
routeType(routeId) === 1 ||
looksLikeSubwayRoute(routeId) ||
subwayRouteAliases.has(routeId),
)
const busRoutes = routeIds.filter((routeId) => routeType(routeId) === 3)
const stopIds = stops.map((stop) => stop.id)
const subwayStopIds = stops.filter((stop) => stop.mode === "subway").map((stop) => stop.id)
const busStopIds = stops.filter((stop) => stop.mode === "bus").map((stop) => stop.id)
async function loadBusGtfsRoutes(urls: string[]) {
const routes: SnapshotRoute[] = []
await Promise.all(
urls.map(async (url) => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Unable to fetch bus GTFS routes from ${url}: ${response.status} ${response.statusText}`)
}
const files = unzipSync(new Uint8Array(await response.arrayBuffer()))
const rows = parseGtfsFile<GtfsRoute>(files, "routes.txt")
routes.push(
...rows.map((route) => ({
id: route.route_id,
shortName: route.route_short_name || route.route_id,
longName: route.route_long_name || undefined,
type: Number(route.route_type || 3),
color: route.route_color || undefined,
textColor: route.route_text_color || undefined,
})),
)
}),
)
return routes
}
function parseGtfsFile<T>(files: Record<string, Uint8Array>, name: string) {
const bytes = files[name]
if (!bytes) return []
return parse(new TextDecoder().decode(bytes), {
bom: true,
columns: true,
skip_empty_lines: true,
}) as T[]
}
function uniqueSorted(values: string[]) {
return [...new Set(values)].sort((a, b) => a.localeCompare(b, "en", { numeric: true }))
}
function union(name: string, values: string[]) {
const sorted = uniqueSorted(values)
if (!sorted.length) return `export type ${name} = never;\n`
return [
`export type ${name} =`,
...sorted.map((value, index) => ` ${index === 0 ? "" : "| "}${JSON.stringify(value)}`),
].join("\n") + "\n"
}
const generated = `// Generated by scripts/generate-known-types.ts from the hosted stops snapshot.\n// Snapshot generated at: ${snapshot.generatedAt ?? "unknown"}\n// Do not edit by hand.\n\n${union("KnownRoute", routeIds)}\n${union("KnownSubwayRoute", subwayRoutes)}\n${union("KnownBusRoute", busRoutes)}\n${union("KnownStopId", stopIds)}\n${union("KnownSubwayStopId", subwayStopIds)}\n${union("KnownBusStopId", busStopIds)}`
await Bun.write(new URL("../src/generated.ts", import.meta.url), generated)
console.log(
`Generated src/generated.ts with ${uniqueSorted(routeIds).length} routes and ${uniqueSorted(stopIds).length} stops.`,
)

Sorry, the diff of this file is too big to display

+4
-8

@@ -19,2 +19,3 @@ import { defaultEndpoints, subwayRouteColors } from "./src/defaults";

StopsNearQuery,
SubwayArrivalQuery,
TransitMode,

@@ -111,9 +112,3 @@ Vehicle,

async arrivals(query: {
stopId: string;
route?: string;
direction?: Direction | "uptown" | "downtown";
limit?: number;
includeRaw?: boolean;
}): Promise<Arrival[]> {
async arrivals(query: SubwayArrivalQuery): Promise<Arrival[]> {
if (this.mta.hostedApiEnabled()) {

@@ -151,3 +146,3 @@ return this.mta.hostedJson<Arrival[]>("/api/v1/subway/arrivals", query);

stopIds: Set<string>,
query: { stopId: string; route?: string; direction?: Direction | "uptown" | "downtown"; includeRaw?: boolean },
query: SubwayArrivalQuery,
) {

@@ -487,2 +482,3 @@ const arrivals: Arrival[] = [];

export * from "./src/errors";
export type * from "./src/generated";
export type * from "./src/types";
{
"name": "mta-js",
"version": "2.0.0",
"version": "2.1.0",
"description": "A TypeScript client for MTA realtime feeds and the hosted MTA API.",

@@ -13,2 +13,3 @@ "license": "MIT",

"src",
"scripts",
"examples",

@@ -24,2 +25,3 @@ "README.md"

"scripts": {
"generate:types": "bun scripts/generate-known-types.ts",
"test": "bun test",

@@ -26,0 +28,0 @@ "typecheck": "bunx tsc --noEmit"

@@ -80,2 +80,10 @@ # mta-js

Route and stop inputs include generated autocomplete for known MTA values while
remaining permissive for future route and stop additions. Refresh the generated
types from the hosted stops snapshot by setting `MTA_STOPS_SNAPSHOT_URL` or
`NEXT_PUBLIC_MTA_STOPS_SNAPSHOT_URL` and running `bun run generate:types`. Bus
route autocomplete is generated from public MTA borough GTFS route files by
default; set `MTA_BUS_GTFS_URLS` to a comma-separated list of GTFS zip URLs to
override them.
## Endpoints

@@ -82,0 +90,0 @@

@@ -0,1 +1,21 @@

import type {
KnownBusRoute,
KnownBusStopId,
KnownRoute,
KnownStopId,
KnownSubwayRoute,
KnownSubwayStopId,
} from "./generated";
export type AutocompleteString<TKnown extends string> =
| TKnown
| (string & {});
export type RouteId = AutocompleteString<KnownRoute>;
export type SubwayRoute = AutocompleteString<KnownSubwayRoute>;
export type BusRoute = AutocompleteString<KnownBusRoute>;
export type StopId = AutocompleteString<KnownStopId>;
export type SubwayStopId = AutocompleteString<KnownSubwayStopId>;
export type BusStopId = AutocompleteString<KnownBusStopId>;
export type TransitMode = "subway" | "bus" | "lirr" | "metro-north";

@@ -174,4 +194,4 @@

export interface SubwayArrivalQuery {
stopId: string;
route?: string;
stopId: SubwayStopId;
route?: SubwayRoute;
direction?: Direction | "uptown" | "downtown";

@@ -183,4 +203,4 @@ limit?: number;

export interface BusArrivalQuery {
stopId: string;
route?: string;
stopId: BusStopId;
route?: BusRoute;
limit?: number;

@@ -191,3 +211,3 @@ includeRaw?: boolean;

export interface BusVehicleQuery {
route?: string;
route?: BusRoute;
vehicleId?: string;

@@ -200,4 +220,4 @@ limit?: number;

mode?: TransitMode;
route?: string;
stopId?: string;
route?: RouteId;
stopId?: StopId;
includeRaw?: boolean;

@@ -210,3 +230,3 @@ }

modes?: TransitMode[];
route?: string;
route?: RouteId;
includeRoutes?: boolean;

@@ -213,0 +233,0 @@ radiusMeters?: number;