- Breaking: drop Node.js 18 support (
engines.nodeis now>=20.0.0). Theawait import('node:crypto')fallback for Node 18's missingglobalThis.cryptois removed from the cache checksum code. This also fixes vite emittingModule "crypto" has been externalized for browser compatibilityfor the coregtfs-sqljsentry when bundling for the browser. Node 18 has been EOL since April 2025. - Add
getCalendars(filters?)— bulk read of thecalendartable (filters:serviceIdsingle value or array,limit).getCalendarByServiceId()remains as a convenience wrapper. (#46) getCalendarDates()no longer requires a service id — it now accepts an optional filters object (serviceId,date,limit) and returns the wholecalendar_datestable when called without arguments. The legacygetCalendarDates('SERVICE_ID')string form is still accepted. (#46)- Add
getFeedInfo()— returns thefeed_inforows (an array, since the spec allows multiple rows). Useful forfeed_start_date/feed_end_datebounds andfeed_versiondisplay/cache keys. (#46) - Add
getFrequencies(filters?)— read thefrequenciestable (filters:tripIdsingle value or array,limit), e.g. to detect frequency-based trips whosestop_timesare offsets fromstart_time.exact_times: 0is preserved (not coerced toundefined). (#46) - Fix:
ProgressInfoandProgressCallbacktypes are now exported from the package entry point, as documented in the README (import { type ProgressInfo } from 'gtfs-sqljs'previously failed). - README: document that the remaining vite
fs/path/cryptowarnings pointing atsql.js/dist/sql-wasm.jscome from sql.js's UMD build, are harmless in the browser, and cannot be fixed from gtfs-sqljs. - CI: test matrix is now Node 20.x / 22.x / 24.x (was 18.x / 20.x / 21.x), matching the new
enginesrange. GitHub Actions bumped across workflows (checkout/setup-node/upload-artifactto v7,configure-pagesto v6,upload-pages-artifact/deploy-pagesto v5).
- Add
getTripSchedules(filters)— display-ready trip schedules with everything pre-computed: seconds since the start of the service day (past-midnight times > 86400 supported), unix epochs computed in the agency's timezone (DST-safe "noon minus 12h" rule, zero-dependency viaIntl), realtime delays resolved per the GTFS-RT spec (delay ↔ absolute time cross-computed, arrival ↔ departure borrowed, delay propagated to subsequent stops until the next update, trip-level delay fallback,stop_id-only matching),SKIPPED/NO_DATA/canceled flags,display_epoch(departure, or arrival at the terminus;displayMode: 'arrival'flips it), and stop names joined in. Filters:tripIdand/orrouteId(+directionId), required servicedate, optionalnow,displayMode,timezone. Batched internally — one trip or a whole route's day costs the same five indexed queries. - Add pure time/realtime helpers (exported, usable without a database):
parseGtfsTime,gtfsTimeToEpoch,serviceDayStartEpoch, andresolveRealtime— the resolution engine behindgetTripSchedules. - Update the website demo's stop-times view to
getTripSchedules(timezone-correct times, realtime departures with strikethrough schedule, canceled/skipped indicators, no more per-stop queries). Addexamples/trip-schedules.ts. - Breaking: split
ScheduleRelationshipintoTripScheduleRelationshipandStopTimeScheduleRelationship. The single enum conflated two protobuf enums with different numeric values: at stop level,SKIPPEDis1andNO_DATAis2per the GTFS-RT spec, but the old enum decoded1asADDEDand definedSKIPPED = 4(a value that never occurs in feeds).StopTimeUpdate.schedule_relationshipandStopTimeRealtime.schedule_relationshipare now typed asStopTimeScheduleRelationship; trip-level fields asTripScheduleRelationship.ScheduleRelationshipremains as a deprecated alias ofTripScheduleRelationship(membersSKIPPED/NO_DATAare gone — they were wrong). - Fix: GTFS-RT loader dropped legitimate zero values.
delay: 0(explicitly on time),stop_sequence: 0,uncertainty: 0,bearing: 0,speed: 0, andcurrent_status: INCOMING_AT (0)were coerced to NULL by||-based defaulting; now preserved with??. - Fix: stop time updates identified by
stop_idonly (withoutstop_sequence, allowed by the spec) are now stored reliably: thert_stop_time_updatestable no longer has a(trip_id, stop_sequence)primary key (SQLite treats NULLs in a composite key as distinct rows) and gained a(trip_id, stop_sequence)index instead.
- Add
buildGraph(tripIds)method andGraph/EdgeTrip/EdgeDatatypes. Builds a directed stop-to-stop graph from the given trips, with each deduplicated edge carrying the list of originating trips (plusroute_idanddirection_id). Handles non-contiguousstop_sequencevalues viaLEAD(). HelpersedgeCount()andedges()are also exported.
The library now talks to a small async GtfsDatabase interface. sql.js becomes one adapter among others (better-sqlite3 ships in the box; op-sqlite / expo-sqlite / … pluggable by the user). Three things change at every call site: (1) query methods return Promise<T>, (2) an adapter is required, (3) sql.js is an optional peer dependency — you install it yourself.
See the full migration write-up in README and the Usage Guide.
- All filter shapes (
{ routeId, date, directionId, … }) and returned GTFS / GTFS-RT object shapes are identical. No SQL query changes, no schema changes. - The high-level entry points (
fromZip,fromZipData,fromDatabase) keep their names and argument order; onlyoptionsgainsadapter.
The core package no longer depends on sql.js. Install whichever adapter(s) you use:
# Previously (v0.5 and earlier): already transitive — nothing to do.
# Now:
npm install sql.js # browser / Node WASM
npm install better-sqlite3 # Node native, file-backedTypical sql.js migration:
- import { GtfsSqlJs } from 'gtfs-sqljs';
+ import { GtfsSqlJs } from 'gtfs-sqljs';
+ import { createSqlJsAdapter } from 'gtfs-sqljs/adapters/sql-js';
- const gtfs = await GtfsSqlJs.fromZip(url, { locateFile });
+ const gtfs = await GtfsSqlJs.fromZip(url, {
+ adapter: await createSqlJsAdapter({ locateFile }),
+ });
- const routes = gtfs.getRoutes();
- const stops = gtfs.getStops({ name: 'Station' });
+ const routes = await gtfs.getRoutes();
+ const stops = await gtfs.getStops({ name: 'Station' });
- const buffer = gtfs.export();
- gtfs.close();
+ const buffer = await gtfs.export();
+ await gtfs.close();TypeScript flags the missing awaits for you; plain JS does not — grep for gtfs.get and gtfs.close(/gtfs.export( before shipping.
getDatabase() now returns a GtfsDatabase (the adapter surface), not a raw sql.js Database. All its methods are async. This is the most likely silent failure during migration:
const db = gtfs.getDatabase();
- const stmt = db.prepare('SELECT * FROM stops WHERE stop_lat > ?');
- stmt.bind([40.7]);
- while (stmt.step()) {
- const row = stmt.getAsObject();
+ const stmt = await db.prepare('SELECT * FROM stops WHERE stop_lat > ?');
+ await stmt.bind([40.7]);
+ while (await stmt.step()) {
+ const row = await stmt.getAsObject();
console.log(row);
}
- stmt.free();
+ await stmt.free();If you need the genuine sql.js Database (for features gtfs-sqljs does not wrap), keep a reference to it at the point where you built the adapter — the library no longer re-exposes it.
If you already open a database handle yourself (typical for file-backed drivers), skip the factory and attach the handle directly:
import BetterSqlite3 from 'better-sqlite3';
import { GtfsSqlJs } from 'gtfs-sqljs';
import { wrapBetterSqlite3 } from 'gtfs-sqljs/adapters/better-sqlite3';
const raw = new BetterSqlite3('./gtfs.db', { readonly: true });
const gtfs = await GtfsSqlJs.attach(wrapBetterSqlite3(raw), {
skipSchema: true, // file already has the GTFS schema
});attach() does not take an adapter. By default it does not close the raw handle when gtfs.close() runs — pass ownsDatabase: true if you want the library to own it.
| v0.5 | v0.6 |
|---|---|
GtfsSqlJsOptions.SQL |
createSqlJsAdapter({ SQL }) |
GtfsSqlJsOptions.locateFile |
createSqlJsAdapter({ locateFile }) |
re-exported SqlJsStatic, sql.js Database type |
import from sql.js directly, or use GtfsDatabase |
Calling fromZip / fromZipData / fromDatabase without options.adapter now throws a runtime Error pointing at createSqlJsAdapter — useful when you miss a call site.
- New
src/adapters/types.tspublic surface:GtfsDatabase,GtfsStatement,GtfsDatabaseAdapter,SqlValue,Row,ExportNotSupportedError. - sql.js adapter at subpath
gtfs-sqljs/adapters/sql-js(exportscreateSqlJsAdapter,wrapSqlJsDatabase). The core module no longer imports sql.js. - better-sqlite3 adapter at subpath
gtfs-sqljs/adapters/better-sqlite3(exportswrapBetterSqlite3,createBetterSqlite3Adapter). First-class Node / file-backed path; the adapter is the only file in the repo that importsbetter-sqlite3, so projects that do not reference this subpath never pull in the native module. Exercised bytests/e2e-better-sqlite3.test.tson every CI run. - Cache layer now catches
ExportNotSupportedErrorfrom adapters that cannot serialize in-memory and logs a warning instead of failing the load; file-backed drivers persist their own DB on disk.
- Ingestion is ~35-45% faster on medium-to-large feeds: ASTUCE (Rouen, ~430k stop_times rows) drops from ~2650 ms to ~1670 ms; Car Jaune from ~312 ms to ~188 ms. Wins come from parsing each CSV only once (progress totals now use a fast newline-based row-count estimate), loading rows as positional arrays instead of per-row objects, and reusing a single prepared INSERT per table instead of re-preparing a multi-row statement per 1000-row batch.
- Dropped the bulk-load PRAGMA block (
synchronous,journal_mode,temp_store,cache_size,locking_mode) from ingestion. Benchmarked aggregate effect on sql.js is within noise (≤1%); removing them simplifies the code and unblocks the pluggable adapter.
ProgressInfo.totalRowsis now an estimate based on CSV line count — typically exact, but may differ by a few rows per file in edge cases (e.g. trailing blank lines). For a precise post-ingest row count, query the database directly withCOUNT(*).
- Add Claude Code skill file with API reference, usage examples, WASM setup instructions, and Web Worker guidance for LLM code agents
fromZip()no longer reads local file paths in Node.js. Read the file yourself and usefromZipData()insteadfetchRealtimeData()/loadRealtimeData()no longer read local file paths in Node.js. Use the newloadRealtimeDataFromBuffers()method with pre-read data instead
- Add
loadRealtimeDataFromBuffers(buffers)method toGtfsSqlJsfor loading GTFS-RT data from pre-loaded protobufUint8Arraybuffers without fetching
- Remove all Node.js
fsimports andisNodeEnvironment()checks, making the published module fully platform-neutral - Delete
src/utils/env.ts(no remaining callers)
- Make
route_short_nameandroute_long_nameoptional, matching GTFS spec (conditionally required: at least one must be present) - Allow
transfers.transfer_typeto be empty (defaults to 0 per GTFS spec) - Make
stop_times.arrival_timeandstop_times.departure_timeoptional for intermediate stops per GTFS spec - Make
stops.stop_latandstops.stop_lonoptional for generic nodes (location_type=3) and boarding areas (location_type=4) per GTFS spec
fromZip()now only acceptsstring(path or URL). If you were passingArrayBufferorUint8Array, use the newfromZipData()method insteadskipFilesbehavior change: files listed inskipFilesare now skipped during ZIP extraction entirely (not just excluded from DB loading), improving performance for large feeds
- Add
GtfsSqlJs.fromZipData(zipData, options?, source?)static method for loading from pre-loaded ZIP data (ArrayBufferorUint8Array) - Extract only known GTFS files from ZIP, skipping unrecognized files for faster extraction
- Simplify checksum module to use global
crypto.subtledirectly (available in both browsers and Node.js 18+, which is the minimum engine version); remove multi-branch environment detection, dynamicimport('crypto')fallback, and emptycatchblock - Extract shared
isNodeEnvironment()helper intoutils/env.ts, replacing inlinetypeof processchecks in zip-loader and gtfs-rt-loader - Narrow
loadGTFSZip()parameter fromstring | ArrayBuffer | Uint8ArraytoArrayBuffer | Uint8Array(string path was dead code) - Replace
unknown[]with properProtobufTimeRange[]andProtobufEntitySelector[]types in gtfs-rt-loader - Refactor
convertKeysToSnakeCaseto useObject.entries(), removingfor..inloop withas Record<string, unknown>cast - Replace
as Record<string, unknown>widening casts onstmt.getAsObject()across all query files with properParamsObjecttype from sql.js - Replace non-null assertions (
!) with optional chaining (?.) forMap.get()calls in rt-trip-updates and stop-times - Replace
this.SQL!.Database()non-null assertion with explicit guard in gtfs-sqljs
- Fix
getStopTimesandgetTripsreturning all results instead of none when called with a date outside the feed's validity range - Allow
fare_attributes.transfersto be empty (NULL), meaning unlimited transfers per GTFS spec
- Add
pickupTypeanddropOffTypefilters togetStopTimes, withCOALESCEhandling so NULL (empty) is treated as 0 (regular) per GTFS spec - Add
PickupDropOffTypeenum for GTFS static pickup/drop-off type values - Replace
getCalendarByServiceId(serviceId)withgetCalendars(filters?)for consistent filter-based API - Fix README: replace non-existent
getStopByIdwithgetStops({ stopId })
- Remove dist/ from repository (built at publish time)
- Upgrade vitest from v1 to v4
- Upgrade ESLint from v8 to v9 with flat config migration
- Upgrade @typescript-eslint from v6 to v8 (via typescript-eslint)
- Upgrade TypeScript from v5.3 to v5.9
- Upgrade protobufjs from v7 to v8
- Upgrade @types/node from v20 to v25
- Upgrade sql.js, tsup, @types/papaparse to latest within-range versions
- Fix 10 npm audit vulnerabilities (minimatch ReDoS, esbuild dev server)
- Publish as ESM-only package (
"type": "module") - Remove CJS build output
- Add automated CD workflow for npm publishing on GitHub release
- Remove migration guide from README (first public release)
- Initial public release
- GTFS static data loading from ZIP files (URL or local path)
- High-performance bulk loading with progress tracking
- Flexible filter-based query API for stops, routes, trips, stop times, shapes
- GTFS Realtime support (alerts, trip updates, vehicle positions)
- GeoJSON export for shapes
- Smart caching with IndexedDB and FileSystem stores
- Database export/import support
- Full TypeScript types