gtfs 4.17.7 → 4.18.1

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.
@@ -658,7 +658,8 @@ function setDefaultConfig(initialConfig) {
658
658
  ignoreDuplicates: false,
659
659
  ignoreErrors: false,
660
660
  gtfsRealtimeExpirationSeconds: 0,
661
- verbose: true
661
+ verbose: true,
662
+ downloadTimeout: 3e4
662
663
  };
663
664
  return {
664
665
  ...defaults,
@@ -682,56 +683,9 @@ function pluralize(singularWord, pluralWord, count) {
682
683
  }
683
684
 
684
685
  // src/lib/import-gtfs-realtime.ts
685
- async function fetchGtfsRealtimeData(urlConfig, task) {
686
- task.log(`Downloading GTFS-Realtime from ${urlConfig.url}`);
687
- const response = await fetch(urlConfig.url, {
688
- method: "GET",
689
- headers: {
690
- ...urlConfig.headers ?? {},
691
- "Accept-Encoding": "gzip"
692
- },
693
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
694
- });
695
- if (response.status !== 200) {
696
- task.logWarning(
697
- `Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
698
- );
699
- return null;
700
- }
701
- const buffer = await response.arrayBuffer();
702
- const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
703
- new Uint8Array(buffer)
704
- );
705
- return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
706
- enums: String,
707
- longs: String,
708
- bytes: String,
709
- defaults: false,
710
- arrays: true,
711
- objects: true,
712
- oneofs: true
713
- });
714
- }
715
- function removeExpiredRealtimeData(config) {
716
- const db = openDb(config);
717
- log(config)(`Removing expired GTFS-Realtime data`);
718
- db.prepare(
719
- `DELETE FROM vehicle_positions WHERE expiration_timestamp <= strftime('%s','now')`
720
- ).run();
721
- db.prepare(
722
- `DELETE FROM trip_updates WHERE expiration_timestamp <= strftime('%s','now')`
723
- ).run();
724
- db.prepare(
725
- `DELETE FROM stop_time_updates WHERE expiration_timestamp <= strftime('%s','now')`
726
- ).run();
727
- db.prepare(
728
- `DELETE FROM service_alerts WHERE expiration_timestamp <= strftime('%s','now')`
729
- ).run();
730
- db.prepare(
731
- `DELETE FROM service_alert_informed_entities WHERE expiration_timestamp <= strftime('%s','now')`
732
- ).run();
733
- log(config)(`Removed expired GTFS-Realtime data\r`, true);
734
- }
686
+ var BATCH_SIZE = 1e3;
687
+ var MAX_RETRIES = 3;
688
+ var RETRY_DELAY = 1e3;
735
689
  function prepareRealtimeFieldValue(entity, column, task) {
736
690
  if (column.name === "created_timestamp") {
737
691
  return task.currentTimestamp;
@@ -748,163 +702,265 @@ function prepareRealtimeFieldValue(entity, column, task) {
748
702
  );
749
703
  return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
750
704
  }
751
- async function processRealtimeAlerts(db, gtfsRealtimeData, task) {
752
- const alertStmt = db.prepare(
753
- `REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(
754
- ", "
755
- )}) VALUES (${serviceAlerts.schema.map(() => "?").join(", ")})`
705
+ function createPreparedStatement(db, model) {
706
+ const columns = model.schema.map((column) => column.name);
707
+ const placeholders = model.schema.map(() => "?").join(", ");
708
+ return db.prepare(
709
+ `REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
756
710
  );
757
- const informedEntityStmt = db.prepare(
758
- `REPLACE INTO ${serviceAlertInformedEntities.filenameBase} (${serviceAlertInformedEntities.schema.map((column) => column.name).join(
759
- ", "
760
- )}) VALUES (${serviceAlertInformedEntities.schema.map(() => "?").join(", ")})`
761
- );
762
- let totalLineCount = 0;
763
- db.transaction(() => {
764
- for (const entity of gtfsRealtimeData.entity) {
765
- const fieldValues = serviceAlerts.schema.map(
766
- (column) => prepareRealtimeFieldValue(entity, column, task)
711
+ }
712
+ async function processBatch(items, batchSize, processor) {
713
+ let totalRecordCount = 0;
714
+ let totalErrorCount = 0;
715
+ for (let i = 0; i < items.length; i += batchSize) {
716
+ const batch = items.slice(i, i + batchSize);
717
+ try {
718
+ const result = await processor(batch);
719
+ totalRecordCount += result.recordCount;
720
+ totalErrorCount += result.errorCount;
721
+ } catch (error) {
722
+ const errorMessage = error instanceof Error ? error.message : String(error);
723
+ totalErrorCount += batch.length;
724
+ console.error(`Batch processing error: ${errorMessage}`);
725
+ }
726
+ }
727
+ return { recordCount: totalRecordCount, errorCount: totalErrorCount };
728
+ }
729
+ async function fetchGtfsRealtimeData(type, task) {
730
+ const urlConfig = getUrlConfig(type, task);
731
+ if (!urlConfig) {
732
+ return null;
733
+ }
734
+ task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
735
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
736
+ try {
737
+ const response = await fetch(urlConfig.url, {
738
+ method: "GET",
739
+ headers: {
740
+ ...urlConfig.headers ?? {},
741
+ "Accept-Encoding": "gzip"
742
+ },
743
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
744
+ });
745
+ if (response.status !== 200) {
746
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
747
+ }
748
+ const buffer = await response.arrayBuffer();
749
+ const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
750
+ new Uint8Array(buffer)
767
751
  );
768
- try {
769
- alertStmt.run(fieldValues);
770
- if (entity.alert.informedEntity?.length) {
771
- const informedEntities = entity.alert.informedEntity.map(
772
- (informedEntity) => {
752
+ const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
753
+ enums: String,
754
+ longs: String,
755
+ bytes: String,
756
+ defaults: false,
757
+ arrays: true,
758
+ objects: true,
759
+ oneofs: true
760
+ });
761
+ return feedMessage;
762
+ } catch (error) {
763
+ const errorMessage = error instanceof Error ? error.message : String(error);
764
+ if (attempt === MAX_RETRIES) {
765
+ if (task.ignoreErrors) {
766
+ task.logError(
767
+ `Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
768
+ );
769
+ return null;
770
+ }
771
+ throw error;
772
+ }
773
+ task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
774
+ await new Promise(
775
+ (resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
776
+ );
777
+ }
778
+ }
779
+ return null;
780
+ }
781
+ function getUrlConfig(type, task) {
782
+ switch (type) {
783
+ case "alerts":
784
+ return task.realtimeAlerts;
785
+ case "tripupdates":
786
+ return task.realtimeTripUpdates;
787
+ case "vehiclepositions":
788
+ return task.realtimeVehiclePositions;
789
+ default:
790
+ return void 0;
791
+ }
792
+ }
793
+ function createServiceAlertsProcessor(db, task) {
794
+ const alertStmt = createPreparedStatement(db, serviceAlerts);
795
+ const informedEntityStmt = createPreparedStatement(
796
+ db,
797
+ serviceAlertInformedEntities
798
+ );
799
+ return async (batch) => {
800
+ let recordCount = 0;
801
+ let errorCount = 0;
802
+ db.transaction(() => {
803
+ for (const entity of batch) {
804
+ try {
805
+ const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
806
+ alertStmt.run(alertValues);
807
+ recordCount++;
808
+ if (entity.alert?.informedEntity?.length) {
809
+ for (const informedEntity of entity.alert.informedEntity) {
773
810
  informedEntity.parent = entity;
774
- return serviceAlertInformedEntities.schema.map(
811
+ const entityValues = serviceAlertInformedEntities.schema.map(
775
812
  (column) => prepareRealtimeFieldValue(informedEntity, column, task)
776
813
  );
814
+ informedEntityStmt.run(entityValues);
815
+ recordCount++;
777
816
  }
778
- );
779
- for (const values of informedEntities) {
780
- informedEntityStmt.run(values);
781
817
  }
818
+ } catch (error) {
819
+ const errorMessage = error instanceof Error ? error.message : String(error);
820
+ errorCount++;
821
+ task.logWarning(`Alert processing error: ${errorMessage}`);
782
822
  }
783
- totalLineCount++;
784
- } catch (error) {
785
- task.logWarning(`Import error: ${error.message}`);
786
823
  }
787
- }
788
- task.log(
789
- `Importing - GTFS-Realtime service alerts - ${totalLineCount} entries imported\r`,
790
- true
791
- );
792
- })();
824
+ })();
825
+ return { recordCount, errorCount };
826
+ };
793
827
  }
794
- async function processRealtimeTripUpdates(db, gtfsRealtimeData, task) {
795
- let totalLineCount = 0;
796
- const tripUpdateStmt = db.prepare(
797
- `REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(
798
- ", "
799
- )}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
828
+ function createTripUpdatesProcessor(db, task) {
829
+ const tripUpdateStmt = createPreparedStatement(
830
+ db,
831
+ tripUpdates
800
832
  );
801
- const stopTimeStmt = db.prepare(
802
- `REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(
803
- ", "
804
- )}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
833
+ const stopTimeStmt = createPreparedStatement(
834
+ db,
835
+ stopTimeUpdates
805
836
  );
806
- db.transaction(() => {
807
- for (const entity of gtfsRealtimeData.entity) {
808
- try {
809
- const fieldValues = tripUpdates.schema.map(
810
- (column) => prepareRealtimeFieldValue(entity, column, task)
811
- );
812
- tripUpdateStmt.run(fieldValues);
813
- for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
814
- stopTimeUpdate.parent = entity;
815
- const values = stopTimeUpdates.schema.map(
816
- (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
817
- );
818
- stopTimeStmt.run(values);
837
+ return async (batch) => {
838
+ let recordCount = 0;
839
+ let errorCount = 0;
840
+ db.transaction(() => {
841
+ for (const entity of batch) {
842
+ try {
843
+ const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
844
+ tripUpdateStmt.run(tripUpdateValues);
845
+ recordCount++;
846
+ if (entity.tripUpdate?.stopTimeUpdate?.length) {
847
+ for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
848
+ stopTimeUpdate.parent = entity;
849
+ const stopTimeValues = stopTimeUpdates.schema.map(
850
+ (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
851
+ );
852
+ stopTimeStmt.run(stopTimeValues);
853
+ recordCount++;
854
+ }
855
+ }
856
+ } catch (error) {
857
+ const errorMessage = error instanceof Error ? error.message : String(error);
858
+ errorCount++;
859
+ task.logWarning(`Trip update processing error: ${errorMessage}`);
819
860
  }
820
- totalLineCount++;
821
- } catch (error) {
822
- task.logWarning(`Import error: ${error.message}`);
823
861
  }
824
- }
825
- task.log(
826
- `Importing - GTFS-Realtime trip updates - ${totalLineCount} entries imported\r`,
827
- true
828
- );
829
- })();
862
+ })();
863
+ return { recordCount, errorCount };
864
+ };
830
865
  }
831
- async function processRealtimeVehiclePositions(db, gtfsRealtimeData, task) {
832
- let totalLineCount = 0;
833
- const vehiclePositionStmt = db.prepare(
834
- `REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(
835
- ", "
836
- )}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
866
+ function createVehiclePositionsProcessor(db, task) {
867
+ const vehiclePositionStmt = createPreparedStatement(
868
+ db,
869
+ vehiclePositions
837
870
  );
838
- db.transaction(() => {
839
- for (const entity of gtfsRealtimeData.entity) {
840
- try {
841
- const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
842
- vehiclePositionStmt.run(fieldValues);
843
- totalLineCount++;
844
- } catch (error) {
845
- task.logWarning(`Import error: ${error.message}`);
871
+ return async (batch) => {
872
+ let recordCount = 0;
873
+ let errorCount = 0;
874
+ db.transaction(() => {
875
+ for (const entity of batch) {
876
+ try {
877
+ const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
878
+ vehiclePositionStmt.run(fieldValues);
879
+ recordCount++;
880
+ } catch (error) {
881
+ const errorMessage = error instanceof Error ? error.message : String(error);
882
+ errorCount++;
883
+ task.logWarning(`Vehicle position processing error: ${errorMessage}`);
884
+ }
846
885
  }
886
+ })();
887
+ return { recordCount, errorCount };
888
+ };
889
+ }
890
+ function removeExpiredRealtimeData(config) {
891
+ const db = openDb(config);
892
+ log(config)(`Removing expired GTFS-Realtime data`);
893
+ db.transaction(() => {
894
+ const tables = [
895
+ "vehicle_positions",
896
+ "trip_updates",
897
+ "stop_time_updates",
898
+ "service_alerts",
899
+ "service_alert_informed_entities"
900
+ ];
901
+ for (const table of tables) {
902
+ db.prepare(
903
+ `DELETE FROM ${table} WHERE expiration_timestamp <= strftime('%s','now')`
904
+ ).run();
847
905
  }
848
- task.log(
849
- `Importing - GTFS-Realtime vehicle positions - ${totalLineCount} entries imported\r`,
850
- true
851
- );
852
906
  })();
907
+ log(config)(`Removed expired GTFS-Realtime data\r`, true);
853
908
  }
854
909
  async function updateGtfsRealtimeData(task) {
855
- if (task.realtimeAlerts === void 0 && task.realtimeTripUpdates === void 0 && task.realtimeVehiclePositions === void 0) {
910
+ if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
856
911
  return;
857
912
  }
913
+ const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
914
+ [
915
+ task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
916
+ task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
917
+ task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
918
+ ]
919
+ );
858
920
  const db = openDb({ sqlitePath: task.sqlitePath });
859
- if (task.realtimeAlerts?.url) {
860
- try {
861
- const alertsData = await fetchGtfsRealtimeData(task.realtimeAlerts, task);
862
- if (alertsData?.entity) {
863
- await processRealtimeAlerts(db, alertsData, task);
864
- }
865
- } catch (error) {
866
- if (task.ignoreErrors) {
867
- task.logError(error.message);
868
- } else {
869
- throw error;
870
- }
871
- }
921
+ const recordCounts = {
922
+ alerts: 0,
923
+ tripupdates: 0,
924
+ vehiclepositions: 0
925
+ };
926
+ const processingPromises = [];
927
+ if (alertsData?.entity?.length) {
928
+ processingPromises.push(
929
+ processBatch(
930
+ alertsData.entity,
931
+ BATCH_SIZE,
932
+ createServiceAlertsProcessor(db, task)
933
+ ).then((result) => {
934
+ recordCounts.alerts = result.recordCount;
935
+ })
936
+ );
872
937
  }
873
- if (task.realtimeTripUpdates?.url) {
874
- try {
875
- const tripUpdatesData = await fetchGtfsRealtimeData(
876
- task.realtimeTripUpdates,
877
- task
878
- );
879
- if (tripUpdatesData?.entity) {
880
- await processRealtimeTripUpdates(db, tripUpdatesData, task);
881
- }
882
- } catch (error) {
883
- if (task.ignoreErrors) {
884
- task.logError(error.message);
885
- } else {
886
- throw error;
887
- }
888
- }
938
+ if (tripUpdatesData?.entity?.length) {
939
+ processingPromises.push(
940
+ processBatch(
941
+ tripUpdatesData.entity,
942
+ BATCH_SIZE,
943
+ createTripUpdatesProcessor(db, task)
944
+ ).then((result) => {
945
+ recordCounts.tripupdates = result.recordCount;
946
+ })
947
+ );
889
948
  }
890
- if (task.realtimeVehiclePositions?.url) {
891
- try {
892
- const vehiclePositionsData = await fetchGtfsRealtimeData(
893
- task.realtimeVehiclePositions,
894
- task
895
- );
896
- if (vehiclePositionsData?.entity) {
897
- await processRealtimeVehiclePositions(db, vehiclePositionsData, task);
898
- }
899
- } catch (error) {
900
- if (task.ignoreErrors) {
901
- task.logError(error.message);
902
- } else {
903
- throw error;
904
- }
905
- }
949
+ if (vehiclePositionsData?.entity?.length) {
950
+ processingPromises.push(
951
+ processBatch(
952
+ vehiclePositionsData.entity,
953
+ BATCH_SIZE,
954
+ createVehiclePositionsProcessor(db, task)
955
+ ).then((result) => {
956
+ recordCounts.vehiclepositions = result.recordCount;
957
+ })
958
+ );
906
959
  }
907
- task.log(`GTFS-Realtime data import complete`);
960
+ await Promise.all(processingPromises);
961
+ task.log(
962
+ `GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
963
+ );
908
964
  }
909
965
  async function updateGtfsRealtime(initialConfig) {
910
966
  const config = setDefaultConfig(initialConfig);
@@ -938,8 +994,9 @@ async function updateGtfsRealtime(initialConfig) {
938
994
  };
939
995
  await updateGtfsRealtimeData(task);
940
996
  } catch (error) {
997
+ const errorMessage = error instanceof Error ? error.message : String(error);
941
998
  if (config.ignoreErrors) {
942
- logError(config)(error.message);
999
+ logError(config)(errorMessage);
943
1000
  } else {
944
1001
  throw error;
945
1002
  }
@@ -954,7 +1011,7 @@ async function updateGtfsRealtime(initialConfig) {
954
1011
  `
955
1012
  );
956
1013
  } catch (error) {
957
- if (error?.code === "SQLITE_CANTOPEN") {
1014
+ if (error.code === "SQLITE_CANTOPEN") {
958
1015
  logError(config)(
959
1016
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
960
1017
  );