gtfs-sqljs 0.2.2 → 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
@@ -714,19 +714,25 @@ declare class GtfsSqlJs {
714
714
  */
715
715
  private constructor();
716
716
  /**
717
- * Create GtfsSqlJs instance from GTFS ZIP file
717
+ * Create GtfsSqlJs instance from GTFS ZIP file path or URL
718
718
  */
719
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>;
720
725
  /**
721
726
  * Create GtfsSqlJs instance from existing SQLite database
722
727
  */
723
728
  static fromDatabase(database: ArrayBuffer, options?: Omit<GtfsSqlJsOptions, 'zipPath' | 'database'>): Promise<GtfsSqlJs>;
724
729
  /**
725
- * 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
726
732
  */
727
- private initFromZip;
733
+ private initFromZipData;
728
734
  /**
729
- * Helper method to load GTFS data from zip data (ArrayBuffer)
735
+ * Helper method to load GTFS data from zip data
730
736
  * Used by both cache-enabled and cache-disabled paths
731
737
  */
732
738
  private loadFromZipData;
@@ -1058,8 +1064,8 @@ interface IndexDefinition {
1058
1064
  declare const GTFS_SCHEMA: TableSchema[];
1059
1065
 
1060
1066
  /**
1061
- * Compute SHA-256 checksum of data
1062
- * 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+).
1063
1069
  */
1064
1070
  declare function computeChecksum(data: ArrayBuffer | Uint8Array): Promise<string>;
1065
1071
  /**
package/dist/index.js CHANGED
@@ -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;
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;
904
925
  }
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;
917
- }
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;
@@ -1810,7 +1810,7 @@ function buildOrderedStopList(db, tripIds) {
1810
1810
  if (!tripStopSequences.has(tripId)) {
1811
1811
  tripStopSequences.set(tripId, []);
1812
1812
  }
1813
- tripStopSequences.get(tripId).push({ stop_id: stopId, stop_sequence: stopSequence });
1813
+ tripStopSequences.get(tripId)?.push({ stop_id: stopId, stop_sequence: stopSequence });
1814
1814
  }
1815
1815
  stmt.free();
1816
1816
  const orderedStopIds = [];
@@ -2308,7 +2308,7 @@ function getTripUpdates(db, filters = {}, stalenessThreshold = 120) {
2308
2308
  if (!stopTimesByTripId.has(stu.trip_id)) {
2309
2309
  stopTimesByTripId.set(stu.trip_id, []);
2310
2310
  }
2311
- stopTimesByTripId.get(stu.trip_id).push(stu);
2311
+ stopTimesByTripId.get(stu.trip_id)?.push(stu);
2312
2312
  }
2313
2313
  for (const tu of tripUpdates) {
2314
2314
  tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
@@ -2334,7 +2334,7 @@ function getAllTripUpdates(db) {
2334
2334
  if (!stopTimesByTripId.has(stu.trip_id)) {
2335
2335
  stopTimesByTripId.set(stu.trip_id, []);
2336
2336
  }
2337
- stopTimesByTripId.get(stu.trip_id).push(stu);
2337
+ stopTimesByTripId.get(stu.trip_id)?.push(stu);
2338
2338
  }
2339
2339
  for (const tu of tripUpdates) {
2340
2340
  tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
@@ -2357,11 +2357,19 @@ var GtfsSqlJs = class _GtfsSqlJs {
2357
2357
  this.lastRealtimeFetchTimestamp = null;
2358
2358
  }
2359
2359
  /**
2360
- * Create GtfsSqlJs instance from GTFS ZIP file
2360
+ * Create GtfsSqlJs instance from GTFS ZIP file path or URL
2361
2361
  */
2362
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) {
2363
2371
  const instance = new _GtfsSqlJs();
2364
- await instance.initFromZip(zipPath, options);
2372
+ await instance.initFromZipData(zipData, options, source);
2365
2373
  return instance;
2366
2374
  }
2367
2375
  /**
@@ -2373,9 +2381,10 @@ var GtfsSqlJs = class _GtfsSqlJs {
2373
2381
  return instance;
2374
2382
  }
2375
2383
  /**
2376
- * 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
2377
2386
  */
2378
- async initFromZip(zipPath, options) {
2387
+ async initFromZipData(zipData, options, source) {
2379
2388
  const onProgress = options.onProgress;
2380
2389
  const {
2381
2390
  cache: userCache,
@@ -2396,20 +2405,14 @@ var GtfsSqlJs = class _GtfsSqlJs {
2396
2405
  percentComplete: 0,
2397
2406
  message: "Checking cache..."
2398
2407
  });
2399
- let zipData2;
2400
- if (typeof zipPath === "string") {
2401
- zipData2 = await fetchZip(zipPath, onProgress);
2402
- } else {
2403
- zipData2 = zipPath;
2404
- }
2405
- const filesize = zipData2.byteLength;
2406
- const checksum = await computeZipChecksum(zipData2);
2408
+ const filesize = zipData.byteLength;
2409
+ const checksum = await computeZipChecksum(zipData);
2407
2410
  const cacheKey = generateCacheKey(
2408
2411
  checksum,
2409
2412
  LIB_VERSION,
2410
2413
  cacheVersion,
2411
2414
  filesize,
2412
- typeof zipPath === "string" ? zipPath : void 0,
2415
+ source,
2413
2416
  skipFiles
2414
2417
  );
2415
2418
  const cacheEntry = await cache.get(cacheKey);
@@ -2458,7 +2461,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2458
2461
  return;
2459
2462
  }
2460
2463
  }
2461
- await this.loadFromZipData(zipData2, options, onProgress);
2464
+ await this.loadFromZipData(zipData, options, onProgress);
2462
2465
  onProgress?.({
2463
2466
  phase: "saving_cache",
2464
2467
  currentFile: null,
@@ -2474,7 +2477,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2474
2477
  checksum,
2475
2478
  version: cacheVersion,
2476
2479
  timestamp: Date.now(),
2477
- source: typeof zipPath === "string" ? zipPath : void 0,
2480
+ source,
2478
2481
  size: dbBuffer.byteLength,
2479
2482
  skipFiles
2480
2483
  });
@@ -2490,12 +2493,6 @@ var GtfsSqlJs = class _GtfsSqlJs {
2490
2493
  });
2491
2494
  return;
2492
2495
  }
2493
- let zipData;
2494
- if (typeof zipPath === "string") {
2495
- zipData = await fetchZip(zipPath, onProgress);
2496
- } else {
2497
- zipData = zipPath;
2498
- }
2499
2496
  await this.loadFromZipData(zipData, options, onProgress);
2500
2497
  onProgress?.({
2501
2498
  phase: "complete",
@@ -2509,10 +2506,13 @@ var GtfsSqlJs = class _GtfsSqlJs {
2509
2506
  });
2510
2507
  }
2511
2508
  /**
2512
- * Helper method to load GTFS data from zip data (ArrayBuffer)
2509
+ * Helper method to load GTFS data from zip data
2513
2510
  * Used by both cache-enabled and cache-disabled paths
2514
2511
  */
2515
2512
  async loadFromZipData(zipData, options, onProgress) {
2513
+ if (!this.SQL) {
2514
+ throw new Error("SQL.js not initialized");
2515
+ }
2516
2516
  this.db = new this.SQL.Database();
2517
2517
  this.db.run("PRAGMA synchronous = OFF");
2518
2518
  this.db.run("PRAGMA journal_mode = MEMORY");
@@ -2544,7 +2544,7 @@ var GtfsSqlJs = class _GtfsSqlJs {
2544
2544
  percentComplete: 35,
2545
2545
  message: "Extracting GTFS ZIP file"
2546
2546
  });
2547
- const files = await loadGTFSZip(zipData);
2547
+ const files = await loadGTFSZip(zipData, options.skipFiles);
2548
2548
  onProgress?.({
2549
2549
  phase: "inserting_data",
2550
2550
  currentFile: null,