gtfs-sqljs 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="logo.svg" alt="gtfs-sqljs logo" width="200" height="200">
2
+ <img src="illustrations/logo.svg" alt="gtfs-sqljs logo" width="200" height="200">
3
3
  <h1>gtfs-sqljs</h1>
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/gtfs-sqljs)](https://www.npmjs.com/package/gtfs-sqljs)
@@ -97,7 +97,8 @@ Full API documentation: [API Reference](https://sysdevrun.github.io/gtfs-sqljs/d
97
97
 
98
98
  ### Static Methods
99
99
 
100
- - `GtfsSqlJs.fromZip(zipPath, options?)` - Create instance from GTFS ZIP file
100
+ - `GtfsSqlJs.fromZip(zipPath, options?)` - Create instance from GTFS ZIP file path or URL
101
+ - `GtfsSqlJs.fromZipData(zipData, options?)` - Create instance from pre-loaded GTFS ZIP data (`ArrayBuffer` or `Uint8Array`)
101
102
  - `GtfsSqlJs.fromDatabase(database, options?)` - Create instance from existing database
102
103
 
103
104
  ### Instance Methods
package/dist/index.d.ts CHANGED
@@ -385,7 +385,8 @@ interface FareAttribute {
385
385
  price: number;
386
386
  currency_type: string;
387
387
  payment_method: number;
388
- transfers: number;
388
+ /** Number of transfers permitted. null means unlimited. */
389
+ transfers: number | null;
389
390
  agency_id?: string;
390
391
  transfer_duration?: number;
391
392
  }
@@ -713,19 +714,25 @@ declare class GtfsSqlJs {
713
714
  */
714
715
  private constructor();
715
716
  /**
716
- * Create GtfsSqlJs instance from GTFS ZIP file
717
+ * Create GtfsSqlJs instance from GTFS ZIP file path or URL
717
718
  */
718
719
  static fromZip(zipPath: string, options?: Omit<GtfsSqlJsOptions, 'zipPath' | 'database'>): Promise<GtfsSqlJs>;
720
+ /**
721
+ * Create GtfsSqlJs instance from pre-loaded GTFS ZIP data
722
+ * @param source - Optional original path/URL, used for cache key generation and metadata
723
+ */
724
+ static fromZipData(zipData: ArrayBuffer | Uint8Array, options?: Omit<GtfsSqlJsOptions, 'zipPath' | 'database'>, source?: string): Promise<GtfsSqlJs>;
719
725
  /**
720
726
  * Create GtfsSqlJs instance from existing SQLite database
721
727
  */
722
728
  static fromDatabase(database: ArrayBuffer, options?: Omit<GtfsSqlJsOptions, 'zipPath' | 'database'>): Promise<GtfsSqlJs>;
723
729
  /**
724
- * Initialize from ZIP file
730
+ * Initialize from pre-loaded ZIP data
731
+ * @param source - Optional original path/URL, used for cache key generation and metadata
725
732
  */
726
- private initFromZip;
733
+ private initFromZipData;
727
734
  /**
728
- * Helper method to load GTFS data from zip data (ArrayBuffer)
735
+ * Helper method to load GTFS data from zip data
729
736
  * Used by both cache-enabled and cache-disabled paths
730
737
  */
731
738
  private loadFromZipData;
@@ -1057,8 +1064,8 @@ interface IndexDefinition {
1057
1064
  declare const GTFS_SCHEMA: TableSchema[];
1058
1065
 
1059
1066
  /**
1060
- * Compute SHA-256 checksum of data
1061
- * Uses Web Crypto API (available in both browser and Node.js 18+)
1067
+ * Compute SHA-256 checksum of data.
1068
+ * Uses the global Web Crypto API (available in browsers and Node.js 18+).
1062
1069
  */
1063
1070
  declare function computeChecksum(data: ArrayBuffer | Uint8Array): Promise<string>;
1064
1071
  /**
package/dist/index.js CHANGED
@@ -185,7 +185,7 @@ var GTFS_SCHEMA = [
185
185
  { name: "price", type: "REAL", required: true },
186
186
  { name: "currency_type", type: "TEXT", required: true },
187
187
  { name: "payment_method", type: "INTEGER", required: true },
188
- { name: "transfers", type: "INTEGER", required: true },
188
+ { name: "transfers", type: "INTEGER", required: false },
189
189
  { name: "agency_id", type: "TEXT", required: false },
190
190
  { name: "transfer_duration", type: "INTEGER", required: false }
191
191
  ]
@@ -340,25 +340,28 @@ function getAllCreateIndexStatements() {
340
340
 
341
341
  // src/loaders/zip-loader.ts
342
342
  import JSZip from "jszip";
343
- async function loadGTFSZip(source) {
344
- let zipData;
345
- if (typeof source === "string") {
346
- zipData = await fetchZip(source);
347
- } else {
348
- zipData = source;
349
- }
343
+
344
+ // src/utils/env.ts
345
+ function isNodeEnvironment() {
346
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
347
+ }
348
+
349
+ // src/loaders/zip-loader.ts
350
+ async function loadGTFSZip(zipData, skipFiles) {
350
351
  const zip = await JSZip.loadAsync(zipData);
352
+ const skipSet = skipFiles ? new Set(skipFiles) : null;
351
353
  const files = {};
352
354
  const filePromises = [];
353
355
  zip.forEach((relativePath, file) => {
354
- if (!file.dir && relativePath.endsWith(".txt")) {
355
- const fileName = relativePath.split("/").pop() || relativePath;
356
- filePromises.push(
357
- file.async("string").then((content) => {
358
- files[fileName] = content;
359
- })
360
- );
361
- }
356
+ if (file.dir) return;
357
+ const fileName = relativePath.split("/").pop() || relativePath;
358
+ if (!(fileName in GTFS_FILE_MAPPING)) return;
359
+ if (skipSet?.has(fileName)) return;
360
+ filePromises.push(
361
+ file.async("string").then((content) => {
362
+ files[fileName] = content;
363
+ })
364
+ );
362
365
  });
363
366
  await Promise.all(filePromises);
364
367
  return files;
@@ -411,8 +414,7 @@ async function fetchZip(source, onProgress) {
411
414
  }
412
415
  throw new Error("fetch is not available to load URL");
413
416
  }
414
- const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
415
- if (isNode) {
417
+ if (isNodeEnvironment()) {
416
418
  try {
417
419
  const fs = await import("fs");
418
420
  const buffer = await fs.promises.readFile(source);
@@ -430,6 +432,24 @@ async function fetchZip(source, onProgress) {
430
432
  }
431
433
  throw new Error("No method available to load ZIP file");
432
434
  }
435
+ var GTFS_FILE_MAPPING = {
436
+ "agency.txt": "agency",
437
+ "stops.txt": "stops",
438
+ "routes.txt": "routes",
439
+ "trips.txt": "trips",
440
+ "stop_times.txt": "stop_times",
441
+ "calendar.txt": "calendar",
442
+ "calendar_dates.txt": "calendar_dates",
443
+ "fare_attributes.txt": "fare_attributes",
444
+ "fare_rules.txt": "fare_rules",
445
+ "shapes.txt": "shapes",
446
+ "frequencies.txt": "frequencies",
447
+ "transfers.txt": "transfers",
448
+ "pathways.txt": "pathways",
449
+ "levels.txt": "levels",
450
+ "feed_info.txt": "feed_info",
451
+ "attributions.txt": "attributions"
452
+ };
433
453
 
434
454
  // src/loaders/csv-parser.ts
435
455
  import Papa from "papaparse";
@@ -867,8 +887,7 @@ async function fetchProtobuf(source) {
867
887
  const arrayBuffer2 = await response2.arrayBuffer();
868
888
  return new Uint8Array(arrayBuffer2);
869
889
  }
870
- const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
871
- if (isNode) {
890
+ if (isNodeEnvironment()) {
872
891
  try {
873
892
  const fs = await import("fs");
874
893
  const buffer = await fs.promises.readFile(source);
@@ -898,24 +917,13 @@ function loadGtfsRtProto() {
898
917
  function camelToSnake(str) {
899
918
  return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
900
919
  }
901
- function convertKeysToSnakeCase(obj) {
902
- if (obj === null || obj === void 0) {
903
- return obj;
904
- }
905
- if (Array.isArray(obj)) {
906
- return obj.map(convertKeysToSnakeCase);
907
- }
908
- if (typeof obj === "object") {
909
- const result = {};
910
- for (const key in obj) {
911
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
912
- const snakeKey = camelToSnake(key);
913
- result[snakeKey] = convertKeysToSnakeCase(obj[key]);
914
- }
915
- }
916
- return result;
920
+ function convertObjectKeysToSnakeCase(obj) {
921
+ const result = {};
922
+ for (const [key, value] of Object.entries(obj)) {
923
+ const snakeKey = camelToSnake(key);
924
+ result[snakeKey] = value !== null && typeof value === "object" && !Array.isArray(value) ? convertObjectKeysToSnakeCase(value) : value;
917
925
  }
918
- return obj;
926
+ return result;
919
927
  }
920
928
  function parseTranslatedString(ts) {
921
929
  if (!ts || !ts.translation || ts.translation.length === 0) {
@@ -936,8 +944,8 @@ function insertAlerts(db, alerts, timestamp) {
936
944
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
937
945
  `);
938
946
  for (const alert of alerts) {
939
- const activePeriodSnake = convertKeysToSnakeCase(alert.activePeriod || []);
940
- const informedEntitySnake = convertKeysToSnakeCase(alert.informedEntity || []);
947
+ const activePeriodSnake = (alert.activePeriod ?? []).map(convertObjectKeysToSnakeCase);
948
+ const informedEntitySnake = (alert.informedEntity ?? []).map(convertObjectKeysToSnakeCase);
941
949
  stmt.run([
942
950
  alert.id,
943
951
  JSON.stringify(activePeriodSnake),
@@ -1098,24 +1106,16 @@ async function loadRealtimeData(db, feedUrls) {
1098
1106
  }
1099
1107
 
1100
1108
  // src/cache/checksum.ts
1101
- async function getCrypto() {
1102
- if (typeof crypto !== "undefined" && crypto.subtle) {
1103
- return crypto;
1104
- }
1105
- if (typeof globalThis !== "undefined") {
1106
- try {
1107
- const { webcrypto } = await import("crypto");
1108
- return webcrypto;
1109
- } catch {
1110
- throw new Error("Web Crypto API not available");
1111
- }
1112
- }
1113
- throw new Error("Crypto not available in this environment");
1114
- }
1115
1109
  async function computeChecksum(data) {
1116
- const cryptoInstance = await getCrypto();
1117
- const bufferSource = data instanceof Uint8Array ? data : new Uint8Array(data);
1118
- const hashBuffer = await cryptoInstance.subtle.digest("SHA-256", bufferSource);
1110
+ let buffer;
1111
+ if (data instanceof ArrayBuffer) {
1112
+ buffer = data;
1113
+ } else {
1114
+ const copy = new ArrayBuffer(data.byteLength);
1115
+ new Uint8Array(copy).set(data);
1116
+ buffer = copy;
1117
+ }
1118
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
1119
1119
  const hashArray = Array.from(new Uint8Array(hashBuffer));
1120
1120
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
1121
1121
  return hashHex;
@@ -1591,11 +1591,10 @@ function getTrips(db, filters = {}, stalenessThreshold = 120) {
1591
1591
  }
1592
1592
  if (serviceIds) {
1593
1593
  const serviceIdArray = Array.isArray(serviceIds) ? serviceIds : [serviceIds];
1594
- if (serviceIdArray.length > 0) {
1595
- const placeholders = serviceIdArray.map(() => "?").join(", ");
1596
- conditions.push(needsRoutesJoin ? `t.service_id IN (${placeholders})` : `service_id IN (${placeholders})`);
1597
- params.push(...serviceIdArray);
1598
- }
1594
+ if (serviceIdArray.length === 0) return [];
1595
+ const placeholders = serviceIdArray.map(() => "?").join(", ");
1596
+ conditions.push(needsRoutesJoin ? `t.service_id IN (${placeholders})` : `service_id IN (${placeholders})`);
1597
+ params.push(...serviceIdArray);
1599
1598
  }
1600
1599
  if (directionId !== void 0) {
1601
1600
  const directionIds = Array.isArray(directionId) ? directionId : [directionId];
@@ -1720,11 +1719,10 @@ function getStopTimes(db, filters = {}, stalenessThreshold = 120) {
1720
1719
  }
1721
1720
  if (serviceIds) {
1722
1721
  const serviceIdArray = Array.isArray(serviceIds) ? serviceIds : [serviceIds];
1723
- if (serviceIdArray.length > 0) {
1724
- const placeholders = serviceIdArray.map(() => "?").join(", ");
1725
- conditions.push(`t.service_id IN (${placeholders})`);
1726
- params.push(...serviceIdArray);
1727
- }
1722
+ if (serviceIdArray.length === 0) return [];
1723
+ const placeholders = serviceIdArray.map(() => "?").join(", ");
1724
+ conditions.push(`t.service_id IN (${placeholders})`);
1725
+ params.push(...serviceIdArray);
1728
1726
  }
1729
1727
  if (directionId !== void 0) {
1730
1728
  const directionIds = Array.isArray(directionId) ? directionId : [directionId];
@@ -1812,7 +1810,7 @@ function buildOrderedStopList(db, tripIds) {
1812
1810
  if (!tripStopSequences.has(tripId)) {
1813
1811
  tripStopSequences.set(tripId, []);
1814
1812
  }
1815
- tripStopSequences.get(tripId).push({ stop_id: stopId, stop_sequence: stopSequence });
1813
+ tripStopSequences.get(tripId)?.push({ stop_id: stopId, stop_sequence: stopSequence });
1816
1814
  }
1817
1815
  stmt.free();
1818
1816
  const orderedStopIds = [];
@@ -2310,7 +2308,7 @@ function getTripUpdates(db, filters = {}, stalenessThreshold = 120) {
2310
2308
  if (!stopTimesByTripId.has(stu.trip_id)) {
2311
2309
  stopTimesByTripId.set(stu.trip_id, []);
2312
2310
  }
2313
- stopTimesByTripId.get(stu.trip_id).push(stu);
2311
+ stopTimesByTripId.get(stu.trip_id)?.push(stu);
2314
2312
  }
2315
2313
  for (const tu of tripUpdates) {
2316
2314
  tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
@@ -2336,7 +2334,7 @@ function getAllTripUpdates(db) {
2336
2334
  if (!stopTimesByTripId.has(stu.trip_id)) {
2337
2335
  stopTimesByTripId.set(stu.trip_id, []);
2338
2336
  }
2339
- stopTimesByTripId.get(stu.trip_id).push(stu);
2337
+ stopTimesByTripId.get(stu.trip_id)?.push(stu);
2340
2338
  }
2341
2339
  for (const tu of tripUpdates) {
2342
2340
  tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
@@ -2359,11 +2357,19 @@ var GtfsSqlJs = class _GtfsSqlJs {
2359
2357
  this.lastRealtimeFetchTimestamp = null;
2360
2358
  }
2361
2359
  /**
2362
- * Create GtfsSqlJs instance from GTFS ZIP file
2360
+ * Create GtfsSqlJs instance from GTFS ZIP file path or URL
2363
2361
  */
2364
2362
  static async fromZip(zipPath, options = {}) {
2363
+ const zipData = await fetchZip(zipPath, options.onProgress);
2364
+ return _GtfsSqlJs.fromZipData(zipData, options, zipPath);
2365
+ }
2366
+ /**
2367
+ * Create GtfsSqlJs instance from pre-loaded GTFS ZIP data
2368
+ * @param source - Optional original path/URL, used for cache key generation and metadata
2369
+ */
2370
+ static async fromZipData(zipData, options = {}, source) {
2365
2371
  const instance = new _GtfsSqlJs();
2366
- await instance.initFromZip(zipPath, options);
2372
+ await instance.initFromZipData(zipData, options, source);
2367
2373
  return instance;
2368
2374
  }
2369
2375
  /**
@@ -2375,9 +2381,10 @@ var GtfsSqlJs = class _GtfsSqlJs {
2375
2381
  return instance;
2376
2382
  }
2377
2383
  /**
2378
- * Initialize from ZIP file
2384
+ * Initialize from pre-loaded ZIP data
2385
+ * @param source - Optional original path/URL, used for cache key generation and metadata
2379
2386
  */
2380
- async initFromZip(zipPath, options) {
2387
+ async initFromZipData(zipData, options, source) {
2381
2388
  const onProgress = options.onProgress;
2382
2389
  const {
2383
2390
  cache: userCache,
@@ -2398,20 +2405,14 @@ var GtfsSqlJs = class _GtfsSqlJs {
2398
2405
  percentComplete: 0,
2399
2406
  message: "Checking cache..."
2400
2407
  });
2401
- let zipData2;
2402
- if (typeof zipPath === "string") {
2403
- zipData2 = await fetchZip(zipPath, onProgress);
2404
- } else {
2405
- zipData2 = zipPath;
2406
- }
2407
- const filesize = zipData2.byteLength;
2408
- const checksum = await computeZipChecksum(zipData2);
2408
+ const filesize = zipData.byteLength;
2409
+ const checksum = await computeZipChecksum(zipData);
2409
2410
  const cacheKey = generateCacheKey(
2410
2411
  checksum,
2411
2412
  LIB_VERSION,
2412
2413
  cacheVersion,
2413
2414
  filesize,
2414
- typeof zipPath === "string" ? zipPath : void 0,
2415
+ source,
2415
2416
  skipFiles
2416
2417
  );
2417
2418
  const cacheEntry = await cache.get(cacheKey);
@@ -2460,7 +2461,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2460
2461
  return;
2461
2462
  }
2462
2463
  }
2463
- await this.loadFromZipData(zipData2, options, onProgress);
2464
+ await this.loadFromZipData(zipData, options, onProgress);
2464
2465
  onProgress?.({
2465
2466
  phase: "saving_cache",
2466
2467
  currentFile: null,
@@ -2476,7 +2477,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2476
2477
  checksum,
2477
2478
  version: cacheVersion,
2478
2479
  timestamp: Date.now(),
2479
- source: typeof zipPath === "string" ? zipPath : void 0,
2480
+ source,
2480
2481
  size: dbBuffer.byteLength,
2481
2482
  skipFiles
2482
2483
  });
@@ -2492,12 +2493,6 @@ var GtfsSqlJs = class _GtfsSqlJs {
2492
2493
  });
2493
2494
  return;
2494
2495
  }
2495
- let zipData;
2496
- if (typeof zipPath === "string") {
2497
- zipData = await fetchZip(zipPath, onProgress);
2498
- } else {
2499
- zipData = zipPath;
2500
- }
2501
2496
  await this.loadFromZipData(zipData, options, onProgress);
2502
2497
  onProgress?.({
2503
2498
  phase: "complete",
@@ -2511,10 +2506,13 @@ var GtfsSqlJs = class _GtfsSqlJs {
2511
2506
  });
2512
2507
  }
2513
2508
  /**
2514
- * Helper method to load GTFS data from zip data (ArrayBuffer)
2509
+ * Helper method to load GTFS data from zip data
2515
2510
  * Used by both cache-enabled and cache-disabled paths
2516
2511
  */
2517
2512
  async loadFromZipData(zipData, options, onProgress) {
2513
+ if (!this.SQL) {
2514
+ throw new Error("SQL.js not initialized");
2515
+ }
2518
2516
  this.db = new this.SQL.Database();
2519
2517
  this.db.run("PRAGMA synchronous = OFF");
2520
2518
  this.db.run("PRAGMA journal_mode = MEMORY");
@@ -2546,7 +2544,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2546
2544
  percentComplete: 35,
2547
2545
  message: "Extracting GTFS ZIP file"
2548
2546
  });
2549
- const files = await loadGTFSZip(zipData);
2547
+ const files = await loadGTFSZip(zipData, options.skipFiles);
2550
2548
  onProgress?.({
2551
2549
  phase: "inserting_data",
2552
2550
  currentFile: null,