mta-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # mta-js
2
+
3
+ A TypeScript client for MTA subway, bus, stop, and alert data with a small normalized API.
4
+
5
+ ```ts
6
+ import { MTA } from "mta-js";
7
+
8
+ const mta = new MTA({
9
+ busTimeKey: process.env.MTA_BUS_KEY,
10
+ databaseUrl: "file:.mta-cache/gtfs.sqlite",
11
+ });
12
+
13
+ await mta.subway.arrivals({ stopId: "A27", route: "A" });
14
+ await mta.bus.vehicles({ route: "B63" });
15
+ await mta.alerts.current({ mode: "subway" });
16
+ await mta.stops.near({ lat, lon, modes: ["subway", "bus"] });
17
+ ```
18
+
19
+ ## Database
20
+
21
+ Realtime feeds only become useful after they are joined back to static GTFS stops, routes, and trips. Today, `databaseUrl` points to a SQLite database used by `bun:sqlite`.
22
+
23
+ ```ts
24
+ new MTA({ databaseUrl: ":memory:" });
25
+ new MTA({ databaseUrl: "file:.mta-cache/gtfs.sqlite" });
26
+ new MTA({ databaseUrl: "/var/data/mta-gtfs.sqlite" });
27
+ ```
28
+
29
+ For serverless deploys, `databaseUrl` can also point at a remote SQLite snapshot. The client downloads the remote database into local temp storage before opening it with `bun:sqlite`; async API calls wait for that hydration automatically.
30
+
31
+ ```ts
32
+ const mta = new MTA({
33
+ databaseUrl: "https://cdn.example.com/mta-gtfs.sqlite",
34
+ });
35
+
36
+ await mta.subway.arrivals({ stopId: "A27", route: "A" });
37
+ ```
38
+
39
+ By default, remote databases are hydrated into the system temp directory and reused while that serverless instance stays warm. You can pin the local hydration path when your platform gives you a writable temp directory:
40
+
41
+ ```ts
42
+ const mta = new MTA({
43
+ databaseUrl: "https://cdn.example.com/mta-gtfs.sqlite",
44
+ databaseLocalPath: "/tmp/mta-gtfs.sqlite",
45
+ });
46
+ ```
47
+
48
+ If you want to pay the hydration cost before handling requests, await readiness during startup:
49
+
50
+ ```ts
51
+ const mta = new MTA({
52
+ databaseUrl: "https://cdn.example.com/mta-gtfs.sqlite",
53
+ });
54
+
55
+ await mta.ready();
56
+ ```
57
+
58
+ Remote SQLite hydration is a read-through snapshot strategy. Local writes, like importing fresh GTFS during a serverless request, update the hydrated copy only; they are not written back to the remote URL.
59
+
60
+ Turso/libSQL URLs are also supported as embedded replicas. The remote database syncs into a local SQLite file first, then `mta-js` reads that local replica.
61
+
62
+ ```ts
63
+ const mta = new MTA({
64
+ databaseUrl: "libsql://mtaapi-transcendent-leo-e3.aws-us-east-1.turso.io",
65
+ databaseAuthToken: process.env.TURSO_AUTH_TOKEN,
66
+ databaseLocalPath: "/tmp/mta-gtfs.sqlite",
67
+ });
68
+
69
+ await mta.ready();
70
+ ```
71
+
72
+ Most Turso databases require an auth token. You can omit `databaseAuthToken` only for databases configured to allow anonymous reads.
73
+
74
+ The name is intentionally broader than a filesystem path so the public API can grow into hosted database adapters later without changing constructor shape.
75
+
76
+ You can inspect whether each transit mode has static GTFS ready before serving traffic:
77
+
78
+ ```ts
79
+ await mta.database.status();
80
+ ```
81
+
82
+ ## DB Push
83
+
84
+ `mta-js` has a Drizzle-like schema push for its fixed GTFS schema. It does not generate migration files; it applies idempotent `create table if not exists` and `create index if not exists` statements.
85
+
86
+ ```ts
87
+ const mta = new MTA({
88
+ databaseUrl: process.env.TURSO_DATABASE_URL,
89
+ databaseAuthToken: process.env.TURSO_AUTH_TOKEN,
90
+ });
91
+
92
+ await mta.database.push();
93
+ ```
94
+
95
+ You can import official subway GTFS with the CLI. `core` is the default production strategy and imports stops/routes only; `schedule` also imports trips and stop times.
96
+
97
+ ```sh
98
+ bun src/cli.ts db import --mode=subway --strategy=core
99
+ bun src/cli.ts db import --mode=subway --strategy=schedule
100
+ ```
101
+
102
+ You can also make startup self-heal static data for a mode. If the import marker is missing, this writes the seed to the configured database and rehydrates the local replica. The default strategy is `core`.
103
+
104
+ ```ts
105
+ await mta.database.ensureStaticData({
106
+ mode: "subway",
107
+ strategy: "core",
108
+ seed: {
109
+ stops: [
110
+ { stop_id: "L06", stop_name: "1 Av", stop_lat: 40.730953, stop_lon: -73.981628 },
111
+ { stop_id: "L06N", stop_name: "1 Av", parent_station: "L06" },
112
+ { stop_id: "L06S", stop_name: "1 Av", parent_station: "L06" },
113
+ ],
114
+ routes: [
115
+ { route_id: "L", route_short_name: "L", route_long_name: "14 St-Canarsie Local", route_type: 1 },
116
+ ],
117
+ },
118
+ });
119
+ ```
120
+
121
+ From the CLI:
122
+
123
+ ```sh
124
+ MTA_DATABASE_URL=libsql://your-db.turso.io \
125
+ MTA_DATABASE_AUTH_TOKEN=... \
126
+ bun src/cli.ts db import --mode=subway --strategy=core
127
+ ```
128
+
129
+ ## Vercel Workflow Sync
130
+
131
+ In production, keep static GTFS imports out of user-facing API requests. `mta-js` provides the database operations, and your app should own the Vercel Workflow setup that runs those operations on a schedule.
132
+
133
+ Install and configure Workflow in your Vercel app, then copy a workflow like this into the app. Use generic database environment variables so the backing store can be a libSQL/Turso database today and another supported database later.
134
+
135
+ ```ts
136
+ // workflows/sync-mta-gtfs.ts
137
+ import { MTA } from "mta-js";
138
+
139
+ export async function syncMtaGtfs() {
140
+ "use workflow";
141
+
142
+ const schema = await pushSchema();
143
+ const subway = await importSubwayCore();
144
+
145
+ return { schema, subway };
146
+ }
147
+
148
+ async function pushSchema() {
149
+ "use step";
150
+
151
+ const mta = new MTA({
152
+ databaseUrl: process.env.MTA_DATABASE_URL,
153
+ databaseAuthToken: process.env.MTA_DATABASE_AUTH_TOKEN,
154
+ databaseLocalPath: "/tmp/mta-sync.sqlite",
155
+ });
156
+
157
+ try {
158
+ return await mta.database.push();
159
+ } finally {
160
+ mta.close();
161
+ }
162
+ }
163
+
164
+ async function importSubwayCore() {
165
+ "use step";
166
+
167
+ const mta = new MTA({
168
+ databaseUrl: process.env.MTA_DATABASE_URL,
169
+ databaseAuthToken: process.env.MTA_DATABASE_AUTH_TOKEN,
170
+ databaseLocalPath: "/tmp/mta-sync.sqlite",
171
+ });
172
+
173
+ try {
174
+ return await mta.database.importStaticData({
175
+ mode: "subway",
176
+ strategy: "core",
177
+ sourceUrl: "https://rrgtfsfeeds.s3.amazonaws.com/gtfs_subway.zip",
178
+ });
179
+ } finally {
180
+ mta.close();
181
+ }
182
+ }
183
+ ```
184
+
185
+ Start the workflow from an app route. This route can be invoked manually or by Vercel Cron.
186
+
187
+ ```ts
188
+ // app/api/sync-mta-gtfs/route.ts
189
+ import { syncMtaGtfs } from "@/workflows/sync-mta-gtfs";
190
+ import { start } from "workflow/api";
191
+
192
+ export async function POST() {
193
+ const run = await start(syncMtaGtfs);
194
+ return Response.json({ runId: run.runId });
195
+ }
196
+ ```
197
+
198
+ ```json
199
+ {
200
+ "crons": [
201
+ {
202
+ "path": "/api/sync-mta-gtfs",
203
+ "schedule": "0 8 * * *"
204
+ }
205
+ ]
206
+ }
207
+ ```
208
+
209
+ Runtime application routes can stay small and fast:
210
+
211
+ ```ts
212
+ // app/api/transit/l/route.ts
213
+ import { MTA, StaticDataMissingError } from "mta-js";
214
+
215
+ const mta = new MTA({
216
+ databaseUrl: process.env.MTA_DATABASE_URL,
217
+ databaseAuthToken: process.env.MTA_DATABASE_AUTH_TOKEN,
218
+ databaseLocalPath: "/tmp/mta.sqlite",
219
+ busTimeKey: process.env.MTA_BUS_KEY,
220
+ });
221
+
222
+ export async function GET() {
223
+ await mta.ready();
224
+
225
+ const status = await mta.database.status();
226
+ if (!status.subway.ready) {
227
+ throw new StaticDataMissingError("subway");
228
+ }
229
+
230
+ const arrivals = await mta.subway.arrivals({
231
+ stopId: "L06",
232
+ route: "L",
233
+ limit: 5,
234
+ });
235
+
236
+ return Response.json({ arrivals });
237
+ }
238
+ ```
239
+
240
+ Use `strategy: "core"` for normal production serving. It writes far less data than a full schedule import and is enough for stop names, route branding, nearby stops, realtime arrivals, alerts, and bus vehicle calls. Use `strategy: "schedule"` only when you need static schedule lookups from `trips` and `stop_times`.
241
+
242
+ Live Turso integration tests are opt-in so normal test runs do not depend on credentials, network, or local proxy certificate state:
243
+
244
+ ```sh
245
+ TURSO_INTEGRATION_TEST=1 \
246
+ TURSO_DATABASE_URL=libsql://your-db.turso.io \
247
+ TURSO_AUTH_TOKEN=... \
248
+ bun test
249
+ ```
250
+
251
+ The live write integration test is separately opt-in so read-only Turso tokens do not create false confidence:
252
+
253
+ ```sh
254
+ TURSO_INTEGRATION_TEST=1 \
255
+ TURSO_WRITE_TEST=1 \
256
+ TURSO_DATABASE_URL=libsql://your-db.turso.io \
257
+ TURSO_AUTH_TOKEN=... \
258
+ bun test
259
+ ```
260
+
261
+ If libSQL reports `invalid peer certificate: UnknownIssuer`, disable HTTPS interception for Turso/libSQL in your proxy tool or run the live Turso test without the proxy. The native libSQL sync client may not trust a debugging proxy certificate even when `fetch` requests do.
262
+
263
+ Live MTA/Bustime integration tests are opt-in so normal test runs do not depend on external MTA availability:
264
+
265
+ ```sh
266
+ MTA_LIVE_TEST=1 bun test
267
+ ```
268
+
269
+ With a BusTime key:
270
+
271
+ ```sh
272
+ MTA_LIVE_TEST=1 \
273
+ MTA_BUS_KEY=... \
274
+ bun test
275
+ ```
276
+
277
+ Full subway GTFS import tests are also opt-in because they download, unzip, and import the real MTA subway GTFS feed:
278
+
279
+ ```sh
280
+ MTA_LIVE_TEST=1 \
281
+ MTA_FULL_GTFS_TEST=1 \
282
+ bun test
283
+ ```
284
+
285
+ To run the full GTFS import against Turso:
286
+
287
+ ```sh
288
+ MTA_LIVE_TEST=1 \
289
+ MTA_FULL_GTFS_TEST=1 \
290
+ TURSO_INTEGRATION_TEST=1 \
291
+ TURSO_WRITE_TEST=1 \
292
+ TURSO_DATABASE_URL=libsql://your-db.turso.io \
293
+ TURSO_AUTH_TOKEN=... \
294
+ bun test
295
+ ```
296
+
297
+ ## Static GTFS
298
+
299
+ ```ts
300
+ await mta.static.importZipFromUrl(
301
+ "https://rrgtfsfeeds.s3.amazonaws.com/gtfs_subway.zip",
302
+ "subway",
303
+ );
304
+ ```
305
+
306
+ Tests and small scripts can seed the cache directly:
307
+
308
+ ```ts
309
+ mta.static.importSeed(
310
+ {
311
+ stops: [{ stop_id: "A27", stop_name: "Jay St-MetroTech" }],
312
+ routes: [{ route_id: "A", route_short_name: "A", route_type: 1 }],
313
+ },
314
+ "subway",
315
+ );
316
+ ```
317
+
318
+ ## Development
319
+
320
+ ```sh
321
+ bun install
322
+ bun test
323
+ bun run typecheck
324
+ ```
@@ -0,0 +1,26 @@
1
+ import { MTA } from "../index";
2
+
3
+ const mta = new MTA({
4
+ databaseUrl: process.env.TURSO_DATABASE_URL,
5
+ databaseAuthToken: process.env.TURSO_AUTH_TOKEN,
6
+ databaseLocalPath: "/tmp/mta.sqlite",
7
+ busTimeKey: process.env.MTA_BUS_KEY,
8
+ });
9
+
10
+ export async function GET() {
11
+ await mta.ready();
12
+
13
+ const [database, lTrainArrivals, m23Vehicles] = await Promise.all([
14
+ mta.database.status(),
15
+ mta.subway.arrivals({ stopId: "L06", route: "L", limit: 5 }),
16
+ mta.bus.vehicles({ route: "M23", limit: 5 }),
17
+ ]);
18
+
19
+ return Response.json({
20
+ database,
21
+ examples: {
22
+ lTrainArrivals,
23
+ m23Vehicles,
24
+ },
25
+ });
26
+ }