gtfs 4.17.7 → 4.18.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 +9 -9
- package/dist/bin/gtfs-export.js +11 -4
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.js +413 -343
- package/dist/bin/gtfs-import.js.map +1 -1
- package/dist/bin/gtfsrealtime-update.js +240 -183
- package/dist/bin/gtfsrealtime-update.js.map +1 -1
- package/dist/index.d.ts +145 -7
- package/dist/index.js +459 -377
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
|
@@ -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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
true
|
|
791
|
-
);
|
|
792
|
-
})();
|
|
824
|
+
})();
|
|
825
|
+
return { recordCount, errorCount };
|
|
826
|
+
};
|
|
793
827
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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 =
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
)}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
|
|
833
|
+
const stopTimeStmt = createPreparedStatement(
|
|
834
|
+
db,
|
|
835
|
+
stopTimeUpdates
|
|
805
836
|
);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
(
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
true
|
|
828
|
-
);
|
|
829
|
-
})();
|
|
862
|
+
})();
|
|
863
|
+
return { recordCount, errorCount };
|
|
864
|
+
};
|
|
830
865
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
", "
|
|
836
|
-
)}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
|
|
866
|
+
function createVehiclePositionsProcessor(db, task) {
|
|
867
|
+
const vehiclePositionStmt = createPreparedStatement(
|
|
868
|
+
db,
|
|
869
|
+
vehiclePositions
|
|
837
870
|
);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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 (
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
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 (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
|
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
|
);
|