gtfs 4.17.6 → 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.
@@ -13,10 +13,10 @@ import PrettyError from "pretty-error";
13
13
  // src/lib/file-utils.ts
14
14
  import path from "path";
15
15
  import { existsSync } from "fs";
16
+ import { homedir } from "os";
16
17
  import { mkdir, readFile, rm } from "fs/promises";
17
18
  import { omit, snakeCase } from "lodash-es";
18
19
  import sanitize from "sanitize-filename";
19
- import untildify from "untildify";
20
20
  import StreamZip from "node-stream-zip";
21
21
 
22
22
  // src/lib/log-utils.ts
@@ -70,6 +70,7 @@ function formatError(error) {
70
70
  }
71
71
 
72
72
  // src/lib/file-utils.ts
73
+ var homeDirectory = homedir();
73
74
  async function getConfig(argv2) {
74
75
  let config;
75
76
  let data;
@@ -117,17 +118,17 @@ async function unzip(zipfilePath, exportPath) {
117
118
  );
118
119
  }
119
120
  }
121
+ function untildify(pathWithTilde) {
122
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
123
+ }
120
124
 
121
125
  // src/lib/import-gtfs.ts
122
126
  import path2 from "path";
123
127
  import { createReadStream, existsSync as existsSync2, lstatSync } from "fs";
124
128
  import { cp, readdir, rename, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
125
129
  import { parse } from "csv-parse";
126
- import pluralize2 from "pluralize";
127
130
  import stripBomStream from "strip-bom-stream";
128
131
  import { temporaryDirectory } from "tempy";
129
- import Timer from "timer-machine";
130
- import untildify3 from "untildify";
131
132
  import mapSeries2 from "promise-map-series";
132
133
 
133
134
  // src/models/models.ts
@@ -4089,10 +4090,9 @@ var vehicles = {
4089
4090
 
4090
4091
  // src/lib/db.ts
4091
4092
  import Database from "better-sqlite3";
4092
- import untildify2 from "untildify";
4093
4093
  var dbs = {};
4094
4094
  function setupDb(sqlitePath) {
4095
- const db = new Database(untildify2(sqlitePath));
4095
+ const db = new Database(untildify(sqlitePath));
4096
4096
  db.pragma("journal_mode = OFF");
4097
4097
  db.pragma("synchronous = OFF");
4098
4098
  db.pragma("temp_store = MEMORY");
@@ -4158,13 +4158,12 @@ function isValidJSON(string) {
4158
4158
  try {
4159
4159
  JSON.parse(string);
4160
4160
  return true;
4161
- } catch (error) {
4161
+ } catch {
4162
4162
  return false;
4163
4163
  }
4164
4164
  }
4165
4165
 
4166
4166
  // src/lib/import-gtfs-realtime.ts
4167
- import pluralize from "pluralize";
4168
4167
  import GtfsRealtimeBindings from "gtfs-realtime-bindings";
4169
4168
  import mapSeries from "promise-map-series";
4170
4169
  import { get } from "lodash-es";
@@ -4191,7 +4190,8 @@ function setDefaultConfig(initialConfig) {
4191
4190
  ignoreDuplicates: false,
4192
4191
  ignoreErrors: false,
4193
4192
  gtfsRealtimeExpirationSeconds: 0,
4194
- verbose: true
4193
+ verbose: true,
4194
+ downloadTimeout: 3e4
4195
4195
  };
4196
4196
  return {
4197
4197
  ...defaults,
@@ -4220,38 +4220,14 @@ function applyPrefixToValue(value, columnShouldBePrefixed, prefix) {
4220
4220
  }
4221
4221
  return `${prefix}${value}`;
4222
4222
  }
4223
+ function pluralize(singularWord, pluralWord, count) {
4224
+ return count === 1 ? singularWord : pluralWord;
4225
+ }
4223
4226
 
4224
4227
  // src/lib/import-gtfs-realtime.ts
4225
- async function fetchGtfsRealtimeData(urlConfig, task) {
4226
- task.log(`Downloading GTFS-Realtime from ${urlConfig.url}`);
4227
- const response = await fetch(urlConfig.url, {
4228
- method: "GET",
4229
- headers: {
4230
- ...urlConfig.headers ?? {},
4231
- "Accept-Encoding": "gzip"
4232
- },
4233
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4234
- });
4235
- if (response.status !== 200) {
4236
- task.logWarning(
4237
- `Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
4238
- );
4239
- return null;
4240
- }
4241
- const buffer = await response.arrayBuffer();
4242
- const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
4243
- new Uint8Array(buffer)
4244
- );
4245
- return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
4246
- enums: String,
4247
- longs: String,
4248
- bytes: String,
4249
- defaults: false,
4250
- arrays: true,
4251
- objects: true,
4252
- oneofs: true
4253
- });
4254
- }
4228
+ var BATCH_SIZE = 1e3;
4229
+ var MAX_RETRIES = 3;
4230
+ var RETRY_DELAY = 1e3;
4255
4231
  function prepareRealtimeFieldValue(entity, column, task) {
4256
4232
  if (column.name === "created_timestamp") {
4257
4233
  return task.currentTimestamp;
@@ -4268,163 +4244,246 @@ function prepareRealtimeFieldValue(entity, column, task) {
4268
4244
  );
4269
4245
  return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
4270
4246
  }
4271
- async function processRealtimeAlerts(db, gtfsRealtimeData, task) {
4272
- const alertStmt = db.prepare(
4273
- `REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(
4274
- ", "
4275
- )}) VALUES (${serviceAlerts.schema.map(() => "?").join(", ")})`
4276
- );
4277
- const informedEntityStmt = db.prepare(
4278
- `REPLACE INTO ${serviceAlertInformedEntities.filenameBase} (${serviceAlertInformedEntities.schema.map((column) => column.name).join(
4279
- ", "
4280
- )}) VALUES (${serviceAlertInformedEntities.schema.map(() => "?").join(", ")})`
4247
+ function createPreparedStatement(db, model) {
4248
+ const columns = model.schema.map((column) => column.name);
4249
+ const placeholders = model.schema.map(() => "?").join(", ");
4250
+ return db.prepare(
4251
+ `REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
4281
4252
  );
4282
- let totalLineCount = 0;
4283
- db.transaction(() => {
4284
- for (const entity of gtfsRealtimeData.entity) {
4285
- const fieldValues = serviceAlerts.schema.map(
4286
- (column) => prepareRealtimeFieldValue(entity, column, task)
4253
+ }
4254
+ async function processBatch(items, batchSize, processor) {
4255
+ let totalRecordCount = 0;
4256
+ let totalErrorCount = 0;
4257
+ for (let i = 0; i < items.length; i += batchSize) {
4258
+ const batch = items.slice(i, i + batchSize);
4259
+ try {
4260
+ const result = await processor(batch);
4261
+ totalRecordCount += result.recordCount;
4262
+ totalErrorCount += result.errorCount;
4263
+ } catch (error) {
4264
+ const errorMessage = error instanceof Error ? error.message : String(error);
4265
+ totalErrorCount += batch.length;
4266
+ console.error(`Batch processing error: ${errorMessage}`);
4267
+ }
4268
+ }
4269
+ return { recordCount: totalRecordCount, errorCount: totalErrorCount };
4270
+ }
4271
+ async function fetchGtfsRealtimeData(type, task) {
4272
+ const urlConfig = getUrlConfig(type, task);
4273
+ if (!urlConfig) {
4274
+ return null;
4275
+ }
4276
+ task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
4277
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
4278
+ try {
4279
+ const response = await fetch(urlConfig.url, {
4280
+ method: "GET",
4281
+ headers: {
4282
+ ...urlConfig.headers ?? {},
4283
+ "Accept-Encoding": "gzip"
4284
+ },
4285
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
4286
+ });
4287
+ if (response.status !== 200) {
4288
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
4289
+ }
4290
+ const buffer = await response.arrayBuffer();
4291
+ const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
4292
+ new Uint8Array(buffer)
4287
4293
  );
4288
- try {
4289
- alertStmt.run(fieldValues);
4290
- if (entity.alert.informedEntity?.length) {
4291
- const informedEntities = entity.alert.informedEntity.map(
4292
- (informedEntity) => {
4294
+ const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
4295
+ enums: String,
4296
+ longs: String,
4297
+ bytes: String,
4298
+ defaults: false,
4299
+ arrays: true,
4300
+ objects: true,
4301
+ oneofs: true
4302
+ });
4303
+ return feedMessage;
4304
+ } catch (error) {
4305
+ const errorMessage = error instanceof Error ? error.message : String(error);
4306
+ if (attempt === MAX_RETRIES) {
4307
+ if (task.ignoreErrors) {
4308
+ task.logError(
4309
+ `Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
4310
+ );
4311
+ return null;
4312
+ }
4313
+ throw error;
4314
+ }
4315
+ task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
4316
+ await new Promise(
4317
+ (resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
4318
+ );
4319
+ }
4320
+ }
4321
+ return null;
4322
+ }
4323
+ function getUrlConfig(type, task) {
4324
+ switch (type) {
4325
+ case "alerts":
4326
+ return task.realtimeAlerts;
4327
+ case "tripupdates":
4328
+ return task.realtimeTripUpdates;
4329
+ case "vehiclepositions":
4330
+ return task.realtimeVehiclePositions;
4331
+ default:
4332
+ return void 0;
4333
+ }
4334
+ }
4335
+ function createServiceAlertsProcessor(db, task) {
4336
+ const alertStmt = createPreparedStatement(db, serviceAlerts);
4337
+ const informedEntityStmt = createPreparedStatement(
4338
+ db,
4339
+ serviceAlertInformedEntities
4340
+ );
4341
+ return async (batch) => {
4342
+ let recordCount = 0;
4343
+ let errorCount = 0;
4344
+ db.transaction(() => {
4345
+ for (const entity of batch) {
4346
+ try {
4347
+ const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4348
+ alertStmt.run(alertValues);
4349
+ recordCount++;
4350
+ if (entity.alert?.informedEntity?.length) {
4351
+ for (const informedEntity of entity.alert.informedEntity) {
4293
4352
  informedEntity.parent = entity;
4294
- return serviceAlertInformedEntities.schema.map(
4353
+ const entityValues = serviceAlertInformedEntities.schema.map(
4295
4354
  (column) => prepareRealtimeFieldValue(informedEntity, column, task)
4296
4355
  );
4356
+ informedEntityStmt.run(entityValues);
4357
+ recordCount++;
4297
4358
  }
4298
- );
4299
- for (const values of informedEntities) {
4300
- informedEntityStmt.run(values);
4301
4359
  }
4360
+ } catch (error) {
4361
+ const errorMessage = error instanceof Error ? error.message : String(error);
4362
+ errorCount++;
4363
+ task.logWarning(`Alert processing error: ${errorMessage}`);
4302
4364
  }
4303
- totalLineCount++;
4304
- } catch (error) {
4305
- task.logWarning(`Import error: ${error.message}`);
4306
4365
  }
4307
- }
4308
- task.log(
4309
- `Importing - GTFS-Realtime service alerts - ${totalLineCount} entries imported\r`,
4310
- true
4311
- );
4312
- })();
4366
+ })();
4367
+ return { recordCount, errorCount };
4368
+ };
4313
4369
  }
4314
- async function processRealtimeTripUpdates(db, gtfsRealtimeData, task) {
4315
- let totalLineCount = 0;
4316
- const tripUpdateStmt = db.prepare(
4317
- `REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(
4318
- ", "
4319
- )}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
4370
+ function createTripUpdatesProcessor(db, task) {
4371
+ const tripUpdateStmt = createPreparedStatement(
4372
+ db,
4373
+ tripUpdates
4320
4374
  );
4321
- const stopTimeStmt = db.prepare(
4322
- `REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(
4323
- ", "
4324
- )}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
4375
+ const stopTimeStmt = createPreparedStatement(
4376
+ db,
4377
+ stopTimeUpdates
4325
4378
  );
4326
- db.transaction(() => {
4327
- for (const entity of gtfsRealtimeData.entity) {
4328
- try {
4329
- const fieldValues = tripUpdates.schema.map(
4330
- (column) => prepareRealtimeFieldValue(entity, column, task)
4331
- );
4332
- tripUpdateStmt.run(fieldValues);
4333
- for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
4334
- stopTimeUpdate.parent = entity;
4335
- const values = stopTimeUpdates.schema.map(
4336
- (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
4337
- );
4338
- stopTimeStmt.run(values);
4379
+ return async (batch) => {
4380
+ let recordCount = 0;
4381
+ let errorCount = 0;
4382
+ db.transaction(() => {
4383
+ for (const entity of batch) {
4384
+ try {
4385
+ const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4386
+ tripUpdateStmt.run(tripUpdateValues);
4387
+ recordCount++;
4388
+ if (entity.tripUpdate?.stopTimeUpdate?.length) {
4389
+ for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
4390
+ stopTimeUpdate.parent = entity;
4391
+ const stopTimeValues = stopTimeUpdates.schema.map(
4392
+ (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
4393
+ );
4394
+ stopTimeStmt.run(stopTimeValues);
4395
+ recordCount++;
4396
+ }
4397
+ }
4398
+ } catch (error) {
4399
+ const errorMessage = error instanceof Error ? error.message : String(error);
4400
+ errorCount++;
4401
+ task.logWarning(`Trip update processing error: ${errorMessage}`);
4339
4402
  }
4340
- totalLineCount++;
4341
- } catch (error) {
4342
- task.logWarning(`Import error: ${error.message}`);
4343
4403
  }
4344
- }
4345
- task.log(
4346
- `Importing - GTFS-Realtime trip updates - ${totalLineCount} entries imported\r`,
4347
- true
4348
- );
4349
- })();
4404
+ })();
4405
+ return { recordCount, errorCount };
4406
+ };
4350
4407
  }
4351
- async function processRealtimeVehiclePositions(db, gtfsRealtimeData, task) {
4352
- let totalLineCount = 0;
4353
- const vehiclePositionStmt = db.prepare(
4354
- `REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(
4355
- ", "
4356
- )}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
4408
+ function createVehiclePositionsProcessor(db, task) {
4409
+ const vehiclePositionStmt = createPreparedStatement(
4410
+ db,
4411
+ vehiclePositions
4357
4412
  );
4358
- db.transaction(() => {
4359
- for (const entity of gtfsRealtimeData.entity) {
4360
- try {
4361
- const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4362
- vehiclePositionStmt.run(fieldValues);
4363
- totalLineCount++;
4364
- } catch (error) {
4365
- task.logWarning(`Import error: ${error.message}`);
4413
+ return async (batch) => {
4414
+ let recordCount = 0;
4415
+ let errorCount = 0;
4416
+ db.transaction(() => {
4417
+ for (const entity of batch) {
4418
+ try {
4419
+ const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
4420
+ vehiclePositionStmt.run(fieldValues);
4421
+ recordCount++;
4422
+ } catch (error) {
4423
+ const errorMessage = error instanceof Error ? error.message : String(error);
4424
+ errorCount++;
4425
+ task.logWarning(`Vehicle position processing error: ${errorMessage}`);
4426
+ }
4366
4427
  }
4367
- }
4368
- task.log(
4369
- `Importing - GTFS-Realtime vehicle positions - ${totalLineCount} entries imported\r`,
4370
- true
4371
- );
4372
- })();
4428
+ })();
4429
+ return { recordCount, errorCount };
4430
+ };
4373
4431
  }
4374
4432
  async function updateGtfsRealtimeData(task) {
4375
- if (task.realtimeAlerts === void 0 && task.realtimeTripUpdates === void 0 && task.realtimeVehiclePositions === void 0) {
4433
+ if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
4376
4434
  return;
4377
4435
  }
4436
+ const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
4437
+ [
4438
+ task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
4439
+ task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
4440
+ task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
4441
+ ]
4442
+ );
4378
4443
  const db = openDb({ sqlitePath: task.sqlitePath });
4379
- if (task.realtimeAlerts?.url) {
4380
- try {
4381
- const alertsData = await fetchGtfsRealtimeData(task.realtimeAlerts, task);
4382
- if (alertsData?.entity) {
4383
- await processRealtimeAlerts(db, alertsData, task);
4384
- }
4385
- } catch (error) {
4386
- if (task.ignoreErrors) {
4387
- task.logError(error.message);
4388
- } else {
4389
- throw error;
4390
- }
4391
- }
4444
+ const recordCounts = {
4445
+ alerts: 0,
4446
+ tripupdates: 0,
4447
+ vehiclepositions: 0
4448
+ };
4449
+ const processingPromises = [];
4450
+ if (alertsData?.entity?.length) {
4451
+ processingPromises.push(
4452
+ processBatch(
4453
+ alertsData.entity,
4454
+ BATCH_SIZE,
4455
+ createServiceAlertsProcessor(db, task)
4456
+ ).then((result) => {
4457
+ recordCounts.alerts = result.recordCount;
4458
+ })
4459
+ );
4392
4460
  }
4393
- if (task.realtimeTripUpdates?.url) {
4394
- try {
4395
- const tripUpdatesData = await fetchGtfsRealtimeData(
4396
- task.realtimeTripUpdates,
4397
- task
4398
- );
4399
- if (tripUpdatesData?.entity) {
4400
- await processRealtimeTripUpdates(db, tripUpdatesData, task);
4401
- }
4402
- } catch (error) {
4403
- if (task.ignoreErrors) {
4404
- task.logError(error.message);
4405
- } else {
4406
- throw error;
4407
- }
4408
- }
4461
+ if (tripUpdatesData?.entity?.length) {
4462
+ processingPromises.push(
4463
+ processBatch(
4464
+ tripUpdatesData.entity,
4465
+ BATCH_SIZE,
4466
+ createTripUpdatesProcessor(db, task)
4467
+ ).then((result) => {
4468
+ recordCounts.tripupdates = result.recordCount;
4469
+ })
4470
+ );
4409
4471
  }
4410
- if (task.realtimeVehiclePositions?.url) {
4411
- try {
4412
- const vehiclePositionsData = await fetchGtfsRealtimeData(
4413
- task.realtimeVehiclePositions,
4414
- task
4415
- );
4416
- if (vehiclePositionsData?.entity) {
4417
- await processRealtimeVehiclePositions(db, vehiclePositionsData, task);
4418
- }
4419
- } catch (error) {
4420
- if (task.ignoreErrors) {
4421
- task.logError(error.message);
4422
- } else {
4423
- throw error;
4424
- }
4425
- }
4472
+ if (vehiclePositionsData?.entity?.length) {
4473
+ processingPromises.push(
4474
+ processBatch(
4475
+ vehiclePositionsData.entity,
4476
+ BATCH_SIZE,
4477
+ createVehiclePositionsProcessor(db, task)
4478
+ ).then((result) => {
4479
+ recordCounts.vehiclepositions = result.recordCount;
4480
+ })
4481
+ );
4426
4482
  }
4427
- task.log(`GTFS-Realtime data import complete`);
4483
+ await Promise.all(processingPromises);
4484
+ task.log(
4485
+ `GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
4486
+ );
4428
4487
  }
4429
4488
 
4430
4489
  // src/lib/import-gtfs.ts
@@ -4456,8 +4515,8 @@ var extractGtfsFiles = async (task) => {
4456
4515
  if (!task.path) {
4457
4516
  throw new Error("No `path` specified in config");
4458
4517
  }
4459
- const gtfsPath = untildify3(task.path);
4460
- task.log(`Importing GTFS from ${task.path}\r`);
4518
+ const gtfsPath = untildify(task.path);
4519
+ task.log(`Importing static GTFS from ${task.path}\r`);
4461
4520
  if (path2.extname(gtfsPath) === ".zip") {
4462
4521
  try {
4463
4522
  await unzip(gtfsPath, task.downloadDir);
@@ -4540,11 +4599,11 @@ var createGtfsTables = (db) => {
4540
4599
  if (column.type === "time") {
4541
4600
  sqlColumnCreateStatements.push(
4542
4601
  `${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
4543
- CASE
4544
- WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
4602
+ CASE
4603
+ WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
4545
4604
  ELSE CAST(
4546
- substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
4547
- substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
4605
+ substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
4606
+ substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
4548
4607
  substr(${column.name}, -2) AS INTEGER
4549
4608
  )
4550
4609
  END
@@ -4601,7 +4660,7 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4601
4660
  continue;
4602
4661
  }
4603
4662
  if (type === "date") {
4604
- value = value.replace(/-/g, "");
4663
+ value = value?.toString().replace(/-/g, "");
4605
4664
  if (value.length !== 8) {
4606
4665
  throw new Error(
4607
4666
  `Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
@@ -4617,155 +4676,221 @@ var formatGtfsLine = (line, model, totalLineCount) => {
4617
4676
  }
4618
4677
  return formattedLine;
4619
4678
  };
4620
- var BATCH_SIZE = 1e5;
4621
- var importGtfsFiles = (db, task) => mapSeries2(
4622
- Object.values(models_exports),
4623
- (model) => new Promise((resolve, reject) => {
4624
- let totalLineCount = 0;
4625
- const filename = `${model.filenameBase}.${model.filenameExtension}`;
4626
- if (task.exclude && task.exclude.includes(model.filenameBase)) {
4627
- task.log(`Skipping - ${filename}\r`);
4628
- resolve();
4629
- return;
4630
- }
4631
- if (model.extension === "gtfs-realtime") {
4632
- resolve();
4633
- return;
4634
- }
4635
- const filepath = path2.join(task.downloadDir, `${filename}`);
4636
- if (!existsSync2(filepath)) {
4637
- if (!model.nonstandard) {
4638
- task.log(`Importing - ${filename} - No file found\r`);
4679
+ var BATCH_SIZE2 = 1e5;
4680
+ var importGtfsFiles = async (db, task) => {
4681
+ await mapSeries2(
4682
+ Object.values(models_exports),
4683
+ (model) => new Promise((resolve, reject) => {
4684
+ let totalLineCount = 0;
4685
+ const filename = `${model.filenameBase}.${model.filenameExtension}`;
4686
+ if (task.exclude && task.exclude.includes(model.filenameBase)) {
4687
+ task.log(`Skipping - ${filename}\r`);
4688
+ resolve();
4689
+ return;
4639
4690
  }
4640
- resolve();
4641
- return;
4642
- }
4643
- task.log(`Importing - ${filename}\r`);
4644
- const columns = model.schema;
4645
- const prefixedColumns = new Set(
4646
- columns.filter((column) => column.prefix).map((column) => column.name)
4647
- );
4648
- const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
4649
- const insert = db.prepare(prepareStatement);
4650
- const insertLines = db.transaction((lines) => {
4651
- for (const [rowNumber, line] of Object.entries(lines)) {
4652
- try {
4653
- if (task.prefix === void 0) {
4654
- insert.run(line);
4655
- } else {
4656
- const prefixedLine = Object.fromEntries(
4657
- Object.entries(
4658
- line
4659
- ).map(([columnName, value]) => [
4660
- columnName,
4661
- applyPrefixToValue(
4662
- value,
4663
- prefixedColumns.has(columnName),
4664
- task.prefix
4665
- )
4666
- ])
4667
- );
4668
- insert.run(prefixedLine);
4669
- }
4670
- } catch (error) {
4671
- if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
4672
- const primaryColumns = columns.filter(
4673
- (column) => column.primary
4674
- );
4675
- task.logWarning(
4676
- `Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${filename}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`
4677
- );
4678
- }
4679
- task.logWarning(
4680
- `Check ${filename} for invalid data on line ${rowNumber + 1}.`
4681
- );
4682
- throw error;
4691
+ if (model.extension === "gtfs-realtime") {
4692
+ resolve();
4693
+ return;
4694
+ }
4695
+ const filepath = path2.join(task.downloadDir, `${filename}`);
4696
+ if (!existsSync2(filepath)) {
4697
+ if (!model.nonstandard) {
4698
+ task.log(`Importing - ${filename} - No file found\r`);
4683
4699
  }
4700
+ resolve();
4701
+ return;
4684
4702
  }
4685
- });
4686
- if (model.filenameExtension === "txt") {
4687
- const parser = parse({
4688
- columns: true,
4689
- relax_quotes: true,
4690
- trim: true,
4691
- skip_empty_lines: true,
4692
- ...task.csvOptions
4693
- });
4694
- let lines = [];
4695
- parser.on("readable", () => {
4696
- try {
4697
- let record;
4698
- while (record = parser.read()) {
4699
- totalLineCount += 1;
4700
- lines.push(formatGtfsLine(record, model, totalLineCount));
4701
- if (lines.length >= BATCH_SIZE) {
4702
- insertLines(lines);
4703
- lines = [];
4704
- task.log(
4705
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4706
- true
4703
+ task.log(`Importing - ${filename}\r`);
4704
+ const columns = model.schema;
4705
+ const prefixedColumns = new Set(
4706
+ columns.filter((column) => column.prefix).map((column) => column.name)
4707
+ );
4708
+ const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
4709
+ const insert = db.prepare(prepareStatement);
4710
+ const insertLines = db.transaction((lines) => {
4711
+ for (const [rowNumber, line] of Object.entries(lines)) {
4712
+ try {
4713
+ if (task.prefix === void 0) {
4714
+ insert.run(line);
4715
+ } else {
4716
+ const prefixedLine = Object.fromEntries(
4717
+ Object.entries(
4718
+ line
4719
+ ).map(([columnName, value]) => [
4720
+ columnName,
4721
+ applyPrefixToValue(
4722
+ value,
4723
+ prefixedColumns.has(columnName),
4724
+ task.prefix
4725
+ )
4726
+ ])
4707
4727
  );
4728
+ insert.run(prefixedLine);
4708
4729
  }
4730
+ } catch (error) {
4731
+ if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
4732
+ const primaryColumns = columns.filter(
4733
+ (column) => column.primary
4734
+ );
4735
+ task.logWarning(
4736
+ `Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${filename}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`
4737
+ );
4738
+ }
4739
+ task.logWarning(
4740
+ `Check ${filename} for invalid data on line ${rowNumber + 1}.`
4741
+ );
4742
+ throw error;
4709
4743
  }
4710
- } catch (error) {
4711
- reject(error);
4712
4744
  }
4713
4745
  });
4714
- parser.on("end", () => {
4715
- try {
4716
- if (lines.length > 0) {
4717
- try {
4718
- insertLines(lines);
4719
- } catch (error) {
4746
+ if (model.filenameExtension === "txt") {
4747
+ const parser = parse({
4748
+ columns: true,
4749
+ relax_quotes: true,
4750
+ trim: true,
4751
+ skip_empty_lines: true,
4752
+ ...task.csvOptions
4753
+ });
4754
+ let lines = [];
4755
+ parser.on("readable", () => {
4756
+ try {
4757
+ let record;
4758
+ while (record = parser.read()) {
4759
+ totalLineCount += 1;
4760
+ lines.push(formatGtfsLine(record, model, totalLineCount));
4761
+ if (lines.length >= BATCH_SIZE2) {
4762
+ insertLines(lines);
4763
+ lines = [];
4764
+ task.log(
4765
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4766
+ true
4767
+ );
4768
+ }
4769
+ }
4770
+ } catch (error) {
4771
+ if (task.ignoreErrors) {
4772
+ const errorMessage = error instanceof Error ? error.message : String(error);
4773
+ task.logError(`Error processing ${filename}: ${errorMessage}`);
4774
+ resolve();
4775
+ } else {
4720
4776
  reject(error);
4721
4777
  }
4722
4778
  }
4723
- task.log(
4724
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4725
- true
4779
+ });
4780
+ parser.on("end", () => {
4781
+ try {
4782
+ if (lines.length > 0) {
4783
+ try {
4784
+ insertLines(lines);
4785
+ } catch (error) {
4786
+ if (task.ignoreErrors) {
4787
+ const errorMessage = error instanceof Error ? error.message : String(error);
4788
+ task.logError(
4789
+ `Error inserting data for ${filename}: ${errorMessage}`
4790
+ );
4791
+ resolve();
4792
+ return;
4793
+ } else {
4794
+ reject(error);
4795
+ return;
4796
+ }
4797
+ }
4798
+ }
4799
+ task.log(
4800
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4801
+ true
4802
+ );
4803
+ resolve();
4804
+ } catch (error) {
4805
+ if (task.ignoreErrors) {
4806
+ const errorMessage = error instanceof Error ? error.message : String(error);
4807
+ task.logError(`Error finalizing ${filename}: ${errorMessage}`);
4808
+ resolve();
4809
+ } else {
4810
+ reject(error);
4811
+ }
4812
+ }
4813
+ });
4814
+ parser.on("error", (error) => {
4815
+ if (task.ignoreErrors) {
4816
+ const errorMessage = error instanceof Error ? error.message : String(error);
4817
+ task.logError(`Parser error for ${filename}: ${errorMessage}`);
4818
+ resolve();
4819
+ } else {
4820
+ reject(error);
4821
+ }
4822
+ });
4823
+ createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
4824
+ } else if (model.filenameExtension === "geojson") {
4825
+ readFile2(filepath, "utf8").then((data) => {
4826
+ if (isValidJSON(data) === false) {
4827
+ if (task.ignoreErrors) {
4828
+ task.logError(`Invalid JSON in ${filename}`);
4829
+ resolve();
4830
+ return;
4831
+ } else {
4832
+ reject(new Error(`Invalid JSON in ${filename}`));
4833
+ return;
4834
+ }
4835
+ }
4836
+ totalLineCount += 1;
4837
+ const line = formatGtfsLine(
4838
+ { geojson: data },
4839
+ model,
4840
+ totalLineCount
4841
+ );
4842
+ try {
4843
+ insertLines([line]);
4844
+ task.log(
4845
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4846
+ true
4847
+ );
4848
+ resolve();
4849
+ } catch (error) {
4850
+ if (task.ignoreErrors) {
4851
+ const errorMessage = error instanceof Error ? error.message : String(error);
4852
+ task.logError(
4853
+ `Error inserting data for ${filename}: ${errorMessage}`
4854
+ );
4855
+ resolve();
4856
+ } else {
4857
+ reject(error);
4858
+ }
4859
+ }
4860
+ }).catch((error) => {
4861
+ if (task.ignoreErrors) {
4862
+ const errorMessage = error instanceof Error ? error.message : String(error);
4863
+ task.logError(`Error reading ${filename}: ${errorMessage}`);
4864
+ resolve();
4865
+ } else {
4866
+ reject(error);
4867
+ }
4868
+ });
4869
+ } else {
4870
+ if (task.ignoreErrors) {
4871
+ task.logError(
4872
+ `Unsupported file type: ${model.filenameExtension} for ${filename}`
4726
4873
  );
4727
4874
  resolve();
4728
- } catch (error) {
4729
- reject(error);
4730
- }
4731
- });
4732
- parser.on("error", reject);
4733
- createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
4734
- } else if (model.filenameExtension === "geojson") {
4735
- readFile2(filepath, "utf8").then((data) => {
4736
- if (isValidJSON(data) === false) {
4737
- reject(new Error(`Invalid JSON in ${filename}`));
4875
+ } else {
4876
+ reject(
4877
+ new Error(`Unsupported file type: ${model.filenameExtension}`)
4878
+ );
4738
4879
  }
4739
- totalLineCount += 1;
4740
- const line = formatGtfsLine(
4741
- { geojson: data },
4742
- model,
4743
- totalLineCount
4744
- );
4745
- insertLines([line]);
4746
- task.log(
4747
- `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4748
- true
4749
- );
4750
- resolve();
4751
- }).catch(reject);
4752
- } else {
4753
- reject(
4754
- new Error(`Unsupported file type: ${model.filenameExtension}`)
4755
- );
4756
- }
4757
- })
4758
- );
4880
+ }
4881
+ })
4882
+ );
4883
+ task.log(`Static GTFS import complete`);
4884
+ };
4759
4885
  async function importGtfs(initialConfig) {
4760
- const timer = new Timer();
4761
- timer.start();
4886
+ const startTime = process.hrtime.bigint();
4762
4887
  const config = setDefaultConfig(initialConfig);
4763
4888
  validateConfigForImport(config);
4764
4889
  try {
4765
4890
  const db = openDb(config);
4766
4891
  const agencyCount = config.agencies.length;
4767
4892
  log(config)(
4768
- `Starting GTFS import for ${pluralize2("file", agencyCount, true)} using SQLite database at ${config.sqlitePath}`
4893
+ `Starting GTFS import for ${pluralize("file", "files", agencyCount)} using SQLite database at ${config.sqlitePath}`
4769
4894
  );
4770
4895
  createGtfsTables(db);
4771
4896
  await mapSeries2(config.agencies, async (agency2) => {
@@ -4773,7 +4898,6 @@ async function importGtfs(initialConfig) {
4773
4898
  const tempPath = temporaryDirectory();
4774
4899
  const task = {
4775
4900
  exclude: agency2.exclude,
4776
- url: agency2.url,
4777
4901
  headers: agency2.headers,
4778
4902
  realtimeAlerts: agency2.realtimeAlerts,
4779
4903
  realtimeTripUpdates: agency2.realtimeTripUpdates,
@@ -4781,7 +4905,6 @@ async function importGtfs(initialConfig) {
4781
4905
  downloadDir: tempPath,
4782
4906
  downloadTimeout: config.downloadTimeout,
4783
4907
  gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
4784
- path: agency2.path,
4785
4908
  csvOptions: config.csvOptions || {},
4786
4909
  ignoreDuplicates: config.ignoreDuplicates,
4787
4910
  ignoreErrors: config.ignoreErrors,
@@ -4792,8 +4915,13 @@ async function importGtfs(initialConfig) {
4792
4915
  logWarning: logWarning(config),
4793
4916
  logError: logError(config)
4794
4917
  };
4795
- if (task.url) {
4918
+ if ("url" in agency2) {
4919
+ Object.assign(task, { url: agency2.url });
4796
4920
  await downloadGtfsFiles(task);
4921
+ } else {
4922
+ Object.assign(task, {
4923
+ path: agency2.path
4924
+ });
4797
4925
  }
4798
4926
  await extractGtfsFiles(task);
4799
4927
  await importGtfsFiles(db, task);
@@ -4801,7 +4929,8 @@ async function importGtfs(initialConfig) {
4801
4929
  await rm2(tempPath, { recursive: true });
4802
4930
  } catch (error) {
4803
4931
  if (config.ignoreErrors) {
4804
- logError(config)(error.message);
4932
+ const errorMessage = error instanceof Error ? error.message : String(error);
4933
+ logError(config)(errorMessage);
4805
4934
  } else {
4806
4935
  throw error;
4807
4936
  }
@@ -4809,14 +4938,14 @@ async function importGtfs(initialConfig) {
4809
4938
  });
4810
4939
  log(config)(`Creating DB indexes`);
4811
4940
  createGtfsIndexes(db);
4812
- const seconds = Math.round(timer.time() / 1e3);
4813
- timer.stop();
4941
+ const endTime = process.hrtime.bigint();
4942
+ const elapsedSeconds = Number(endTime - startTime) / 1e9;
4814
4943
  log(config)(
4815
- `Completed GTFS import for ${pluralize2("agency", agencyCount, true)} in ${seconds} seconds
4944
+ `Completed GTFS import in ${elapsedSeconds.toFixed(1)} seconds
4816
4945
  `
4817
4946
  );
4818
4947
  } catch (error) {
4819
- if (error?.code === "SQLITE_CANTOPEN") {
4948
+ if (error.code === "SQLITE_CANTOPEN") {
4820
4949
  logError(config)(
4821
4950
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4822
4951
  );
@@ -4829,11 +4958,9 @@ async function importGtfs(initialConfig) {
4829
4958
  import path3 from "path";
4830
4959
  import { writeFile as writeFile2 } from "fs/promises";
4831
4960
  import { without, compact as compact2 } from "lodash-es";
4832
- import pluralize3 from "pluralize";
4833
4961
  import { stringify } from "csv-stringify";
4834
4962
  import sqlString2 from "sqlstring-sqlite";
4835
4963
  import mapSeries3 from "promise-map-series";
4836
- import untildify4 from "untildify";
4837
4964
 
4838
4965
  // src/lib/advancedQuery.ts
4839
4966
  import sqlString3 from "sqlstring-sqlite";