From c90be4a981c25bc2aef6fe5a0e6d2447d67db49c Mon Sep 17 00:00:00 2001 From: Andrej Mickov Date: Fri, 13 Mar 2026 00:01:00 +0100 Subject: [PATCH] Saving GTFS-RT data to Parquet --- .env.example | 24 +- .gitignore | 4 +- API-DOCUMENTATION.md | 871 ------------ README.md | 235 +--- UPSTREAM-API-DOCUMENTATION.md | 556 -------- bun.lock | 270 +++- bus-tracker-json.ts | 347 ----- find-stops-routes.ts | 110 -- background-tracker.ts => index.ts | 94 +- infrastructure/compose.yml | 24 - lib/database.ts | 988 ++++++++------ package-lock.json | 2058 ----------------------------- package.json | 12 +- public/analytics.html | 500 ------- public/index.html | 642 --------- server.ts | 416 ------ setup-gtfs.ts | 86 -- tsconfig.json | 3 +- 18 files changed, 930 insertions(+), 6310 deletions(-) delete mode 100644 API-DOCUMENTATION.md delete mode 100644 UPSTREAM-API-DOCUMENTATION.md delete mode 100644 bus-tracker-json.ts delete mode 100644 find-stops-routes.ts rename background-tracker.ts => index.ts (80%) delete mode 100644 infrastructure/compose.yml delete mode 100644 package-lock.json delete mode 100644 public/analytics.html delete mode 100644 public/index.html delete mode 100644 server.ts delete mode 100644 setup-gtfs.ts diff --git a/.env.example b/.env.example index 05ce217..b96ff10 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,18 @@ -# Database Configuration (TimescaleDB) -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=iot_data -POSTGRES_USER=postgres -POSTGRES_PASSWORD=example +# Parquet storage +PARQUET_DIR=./data +PARQUET_ROLL_MINUTES=5 +SAVE_ALL_VEHICLE_SNAPSHOTS=true +SAVE_ALL_VEHICLE_POSITIONS=true + +# Object storage (S3-compatible) upload +S3_ENABLED=true +S3_BUCKET=test-bucket +S3_REGION=us-east-1 +S3_ENDPOINT=http://localhost:2600 +S3_PREFIX=parquet +S3_FORCE_PATH_STYLE=true +S3_DELETE_LOCAL_AFTER_UPLOAD=true +S3_UPLOAD_RETRIES=3 +S3_UPLOAD_RETRY_BASE_MS=1000 +S3_ACCESS_KEY_ID=test +S3_SECRET_ACCESS_KEY=test123! diff --git a/.gitignore b/.gitignore index b3eb616..230f6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ .env -dist/ \ No newline at end of file +dist/ +data/*.parquet +*.log \ No newline at end of file diff --git a/API-DOCUMENTATION.md b/API-DOCUMENTATION.md deleted file mode 100644 index 3460309..0000000 --- a/API-DOCUMENTATION.md +++ /dev/null @@ -1,871 +0,0 @@ -# GTFS API Documentation - -Comprehensive API documentation for the Skopje Bus Tracker GTFS-based API. - -## Table of Contents - -- [Overview](#overview) -- [OpenAPI Specification](#openapi-specification) -- [Base URL](#base-url) -- [GTFS Static Data Endpoints](#gtfs-static-data-endpoints) -- [Real-Time Data Endpoints](#real-time-data-endpoints) -- [Historical Data Endpoints](#historical-data-endpoints) -- [Statistics & Analytics Endpoints](#statistics--analytics-endpoints) -- [Data Models](#data-models) -- [Error Handling](#error-handling) -- [Rate Limiting](#rate-limiting) - ---- - -## Overview - -This API provides access to Skopje public transport data based on the **GTFS (General Transit Feed Specification)** standard. It combines static GTFS data with real-time information and historical analytics stored in TimescaleDB. - -### Key Features -- **GTFS-compliant** static data (stops, routes) -- **Real-time** vehicle positions and arrival predictions -- **Historical** time-series data for analytics -- **Statistical** analysis of delays and patterns - -### Upstream API - -This application consumes data from the **ModeShift GTFS API**. For documentation on the upstream API endpoints: -- See **[UPSTREAM-API-DOCUMENTATION.md](UPSTREAM-API-DOCUMENTATION.md)** - ---- - -## OpenAPI Specification - -A complete **OpenAPI 3.0** specification is available at [openapi.yaml](openapi.yaml). This machine-readable specification includes: - -- All API endpoints with parameters and responses -- Request/response schemas and data models -- Examples for all operations -- Error response formats - -### Using the OpenAPI Spec - -**Interactive Documentation (Local):** - -Once the server is running, visit: -``` -http://localhost:3000/api-docs.html -``` - -This provides an interactive Swagger UI where you can: -- Browse all API endpoints -- View request/response schemas -- Try out API calls directly from the browser -- Download the OpenAPI specification - -**Interactive Documentation with Swagger UI (Alternative):** -```bash -# Using Docker -docker run -p 8080:8080 -e SWAGGER_JSON=/openapi.yaml -v $(pwd)/openapi.yaml:/openapi.yaml swaggerapi/swagger-ui - -# Or use online editor -# Visit: https://editor.swagger.io/ -# Then paste the contents of openapi.yaml -``` - -**Generate Client Libraries:** -```bash -# Install OpenAPI Generator -npm install -g @openapitools/openapi-generator-cli - -# Generate TypeScript client -openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o ./client/typescript - -# Generate Python client -openapi-generator-cli generate -i openapi.yaml -g python -o ./client/python - -# Generate Go client -openapi-generator-cli generate -i openapi.yaml -g go -o ./client/go -``` - -**VS Code Integration:** -Install the "OpenAPI (Swagger) Editor" extension to view and edit the spec with validation and IntelliSense. - ---- - -## Base URL - -``` -http://localhost:3000 -``` - -For production, replace with your deployed server URL. - ---- - -## GTFS Static Data Endpoints - -### Get Configuration - -Retrieve configuration including default stop and route information. - -**Endpoint:** `GET /api/config` - -**Query Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `stopId` | string | No | Specific stop ID to query (defaults to configured default) | -| `routeId` | string | No | Specific route ID to query (defaults to configured default) | - -**Response:** -```json -{ - "stop": { - "id": "1563", - "code": "363", - "name": "KARPOS II - STIV NAUMOV", - "lat": 41.97964, - "lon": 21.41283 - }, - "route": { - "id": "125", - "shortName": "7", - "longName": "Centar - GTC - Karpos 4 - Karposh 2" - }, - "defaults": { - "stopId": "1563", - "routeId": "125" - } -} -``` - -**Example:** -```bash -curl "http://localhost:3000/api/config?stopId=1571&routeId=125" -``` - ---- - -### Get All Stops - -Retrieve all GTFS stops in the system. - -**Endpoint:** `GET /api/stops` - -**Query Parameters:** None - -**Response:** -```json -[ - { - "id": "1563", - "code": "363", - "name": "KARPOS II - STIV NAUMOV", - "lat": 41.97964, - "lon": 21.41283 - }, - { - "id": "1571", - "code": "371", - "name": "AMERICAN COLLEGE - VASIL GLAVINOV", - "lat": 42.00437, - "lon": 21.39687 - } -] -``` - -**Example:** -```bash -curl "http://localhost:3000/api/stops" -``` - -**Use Cases:** -- Populate stop selection dropdowns -- Build stop search functionality -- Generate maps with all stops - ---- - -### Get All Routes - -Retrieve all GTFS routes in the system. - -**Endpoint:** `GET /api/routes` - -**Query Parameters:** None - -**Response:** -```json -[ - { - "id": "125", - "shortName": "7", - "longName": "Centar - GTC - Karpos 4 - Karposh 2" - }, - { - "id": "128", - "shortName": "2", - "longName": "Kisela Voda - Centar - 11 Oktomvri - Avtokomanda" - } -] -``` - -**Example:** -```bash -curl "http://localhost:3000/api/routes" -``` - -**Use Cases:** -- Display available routes -- Filter by route number -- Route selection interfaces - ---- - -## Real-Time Data Endpoints - -### Get Real-Time Arrivals - -Get upcoming bus arrivals at a specific stop with real-time predictions. - -**Endpoint:** `GET /api/arrivals` - -**Query Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `stopId` | string | No | Stop ID to query (defaults to configured default) | -| `routeId` | string | No | Filter by specific route ID (defaults to configured default) | - -**Response:** -```json -[ - { - "arrivalTime": "2026-02-07T14:35:00.000Z", - "scheduledTime": "2026-02-07T14:33:00.000Z", - "minutesUntil": 8, - "delaySeconds": 120, - "headsign": "Karpos 2", - "isRealtime": true, - "realtimeState": "UPDATED" - }, - { - "arrivalTime": "2026-02-07T14:48:00.000Z", - "scheduledTime": "2026-02-07T14:48:00.000Z", - "minutesUntil": 21, - "delaySeconds": 0, - "headsign": "Centar", - "isRealtime": true, - "realtimeState": "SCHEDULED" - } -] -``` - -**Fields:** -- `arrivalTime`: Predicted arrival time (ISO 8601) -- `scheduledTime`: Originally scheduled time (ISO 8601) -- `minutesUntil`: Minutes until arrival (can be negative if bus passed) -- `delaySeconds`: Delay in seconds (positive = late, negative = early) -- `headsign`: Destination shown on bus -- `isRealtime`: Whether prediction is based on real-time data -- `realtimeState`: `SCHEDULED`, `UPDATED`, or `CANCELED` - -**Example:** -```bash -curl "http://localhost:3000/api/arrivals?stopId=1563&routeId=125" -``` - -**Notes:** -- Returns arrivals from 2 minutes ago up to configured minutes ahead -- Results are sorted by arrival time -- Data is also logged to database for historical analysis - ---- - -### Get Real-Time Vehicle Positions - -Get current positions of all vehicles on a specific route. - -**Endpoint:** `GET /api/vehicles` - -**Query Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `routeId` | string | No | Route ID to filter vehicles (defaults to configured default) | - -**Response:** -```json -[ - { - "id": 12345, - "vehicleId": "MK-SK-1234", - "label": "1234", - "lat": 41.9981, - "lon": 21.4254, - "bearing": 87.5, - "speed": 15.3, - "timestamp": "2026-02-07T14:27:15Z", - "tripId": "trip_789", - "currentStopSequence": 0, - "currentStatus": "IN_TRANSIT_TO" - } -] -``` - -**Fields:** -- `id`: Internal vehicle database ID -- `vehicleId`: Vehicle identification number (license plate) -- `label`: Bus inventory number (shown on bus) -- `lat`/`lon`: Current GPS coordinates -- `bearing`: Direction of travel (0-360 degrees, 0=North) -- `speed`: Current speed in km/h -- `timestamp`: Last position update time -- `tripId`: GTFS trip ID vehicle is currently serving -- `currentStatus`: `IN_TRANSIT_TO`, `STOPPED_AT`, or `UNKNOWN` - -**Example:** -```bash -curl "http://localhost:3000/api/vehicles?routeId=125" -``` - -**Notes:** -- Combines GTFS-RT trip updates with JSON vehicle API -- Vehicle positions are logged to database -- Updates typically every 10-30 seconds from source - ---- - -## Historical Data Endpoints - -### Get Database Statistics - -Get overview statistics about stored historical data. - -**Endpoint:** `GET /api/stats/db` - -**Query Parameters:** None - -**Response:** -```json -{ - "vehiclePositions": { - "totalRecords": 1523847, - "oldestRecord": "2026-01-15T08:00:00.000Z", - "newestRecord": "2026-02-07T14:27:30.000Z", - "dataRetentionDays": 90 - }, - "arrivalRecords": { - "totalRecords": 456231, - "oldestRecord": "2026-01-15T08:00:00.000Z", - "newestRecord": "2026-02-07T14:27:30.000Z", - "dataRetentionDays": 90 - }, - "databaseSize": "2.3 GB", - "status": "healthy" -} -``` - -**Example:** -```bash -curl "http://localhost:3000/api/stats/db" -``` - ---- - -### Get Vehicle Position History - -Retrieve historical GPS positions for a specific vehicle. - -**Endpoint:** `GET /api/history/vehicle/:vehicleId` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `vehicleId` | string | Yes | Vehicle identification number or label | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `hours` | integer | No | 24 | Number of hours to look back | - -**Response:** -```json -[ - { - "time": "2026-02-07T10:00:00.000Z", - "vehicleId": "MK-SK-1234", - "routeId": "125", - "tripId": "trip_789", - "latitude": 41.9981, - "longitude": 21.4254, - "speed": 15.3, - "bearing": 87.5, - "currentStatus": "IN_TRANSIT_TO" - } -] -``` - -**Example:** -```bash -curl "http://localhost:3000/api/history/vehicle/1234?hours=12" -``` - -**Use Cases:** -- Visualize vehicle path on map -- Analyze driving patterns -- Calculate actual travel times - ---- - -### Get Route Vehicle History - -Get historical positions of all vehicles that served a specific route. - -**Endpoint:** `GET /api/history/route/:routeId/vehicles` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `routeId` | string | Yes | GTFS route ID | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `hours` | integer | No | 24 | Number of hours to look back | - -**Response:** -```json -[ - { - "time": "2026-02-07T10:00:00.000Z", - "vehicleId": "1234", - "routeId": "125", - "tripId": "trip_789", - "latitude": 41.9981, - "longitude": 21.4254, - "speed": 15.3, - "bearing": 87.5 - } -] -``` - -**Example:** -```bash -curl "http://localhost:3000/api/history/route/125/vehicles?hours=6" -``` - -**Use Cases:** -- Service frequency analysis -- Route coverage visualization -- Fleet utilization tracking - ---- - -### Get Stop Arrival History - -Retrieve historical arrival records for a specific stop. - -**Endpoint:** `GET /api/history/stop/:stopId/arrivals` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `stopId` | string | Yes | GTFS stop ID | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `routeId` | string | No | (all) | Filter by specific route | -| `hours` | integer | No | 24 | Number of hours to look back | - -**Response:** -```json -[ - { - "time": "2026-02-07T10:15:00.000Z", - "stopId": "1563", - "routeId": "125", - "scheduledTime": "2026-02-07T10:15:00.000Z", - "predictedTime": "2026-02-07T10:17:30.000Z", - "delaySeconds": 150, - "isRealtime": true, - "headsign": "Karpos 2" - } -] -``` - -**Example:** -```bash -curl "http://localhost:3000/api/history/stop/1563/arrivals?routeId=125&hours=48" -``` - ---- - -## Statistics & Analytics Endpoints - -### Get Route Delay Statistics - -Get delay statistics and performance metrics for a route. - -**Endpoint:** `GET /api/stats/route/:routeId/delays` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `routeId` | string | Yes | GTFS route ID | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `hours` | integer | No | 24 | Time window for analysis | - -**Response:** -```json -{ - "routeId": "125", - "periodHours": 24, - "statistics": { - "totalArrivals": 287, - "avgDelaySeconds": 62, - "medianDelaySeconds": 45, - "minDelaySeconds": -30, - "maxDelaySeconds": 420, - "stdDeviation": 78.5, - "onTimePercentage": 68.3, - "earlyPercentage": 8.7, - "latePercentage": 23.0, - "veryLatePercentage": 4.2 - }, - "thresholds": { - "onTime": "±5 minutes", - "veryLate": ">10 minutes" - } -} -``` - -**Example:** -```bash -curl "http://localhost:3000/api/stats/route/125/delays?hours=72" -``` - -**Definitions:** -- **On-time**: Within ±5 minutes of schedule -- **Early**: More than 5 minutes early -- **Late**: 5-10 minutes late -- **Very Late**: More than 10 minutes late - ---- - -### Get Stop Delay Statistics - -Get delay statistics for a specific stop. - -**Endpoint:** `GET /api/stats/stop/:stopId/delays` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `stopId` | string | Yes | GTFS stop ID | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `hours` | integer | No | 24 | Time window for analysis | - -**Response:** -```json -{ - "stopId": "1563", - "periodHours": 24, - "statistics": { - "totalArrivals": 145, - "avgDelaySeconds": 72, - "medianDelaySeconds": 60, - "onTimePercentage": 62.1, - "routeBreakdown": [ - { - "routeId": "125", - "routeName": "7", - "arrivals": 89, - "avgDelaySeconds": 65 - }, - { - "routeId": "128", - "routeName": "2", - "arrivals": 56, - "avgDelaySeconds": 82 - } - ] - } -} -``` - -**Example:** -```bash -curl "http://localhost:3000/api/stats/stop/1563/delays?hours=48" -``` - -**Use Cases:** -- Compare reliability across stops -- Identify problematic locations -- Plan transfer reliability - ---- - -### Get Route Hourly Pattern - -Analyze delay patterns by hour of day for a route. - -**Endpoint:** `GET /api/stats/route/:routeId/hourly` - -**Path Parameters:** -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `routeId` | string | Yes | GTFS route ID | - -**Query Parameters:** -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `days` | integer | No | 7 | Number of days to analyze | - -**Response:** -```json -{ - "routeId": "125", - "periodDays": 7, - "hourlyPattern": [ - { - "hour": 6, - "arrivals": 42, - "avgDelaySeconds": 15, - "onTimePercentage": 85.7 - }, - { - "hour": 7, - "arrivals": 89, - "avgDelaySeconds": 45, - "onTimePercentage": 71.9 - }, - { - "hour": 8, - "arrivals": 124, - "avgDelaySeconds": 125, - "onTimePercentage": 52.4 - } - ] -} -``` - -**Example:** -```bash -curl "http://localhost:3000/api/stats/route/125/hourly?days=14" -``` - -**Use Cases:** -- Identify rush hour patterns -- Compare morning vs evening reliability -- Optimize schedules based on actual performance - ---- - -## Data Models - -### GTFS Stop -```typescript -interface GtfsStop { - stop_id: string; // Unique stop identifier - stop_code: string; // Public-facing stop code - stop_name: string; // Stop name - stop_lat: number; // Latitude (WGS84) - stop_lon: number; // Longitude (WGS84) -} -``` - -### GTFS Route -```typescript -interface GtfsRoute { - route_id: string; // Unique route identifier - route_short_name: string; // Public route number (e.g., "7") - route_long_name: string; // Full route name - route_type?: string; // GTFS route type (0=Tram, 3=Bus, etc.) -} -``` - -### Vehicle Position -```typescript -interface VehiclePosition { - time: Date; // Timestamp (ISO 8601) - vehicle_id: string; // Vehicle identifier - route_id: string; // Route being served - trip_id?: string; // Current trip ID - latitude: number; // GPS latitude - longitude: number; // GPS longitude - speed?: number; // Speed in km/h - bearing?: number; // Direction (0-360°) - current_status: string; // IN_TRANSIT_TO, STOPPED_AT, UNKNOWN -} -``` - -### Arrival Record -```typescript -interface ArrivalRecord { - time: Date; // Record timestamp - stop_id: string; // Stop ID - route_id: string; // Route ID - scheduled_time: Date; // Scheduled arrival time - predicted_time?: Date; // Real-time predicted time - actual_time?: Date; // Actual arrival time (if observed) - delay_seconds: number; // Delay in seconds - is_realtime: boolean; // Based on real-time data - headsign?: string; // Trip headsign -} -``` - ---- - -## Error Handling - -All endpoints return standard HTTP status codes: - -| Status Code | Description | -|-------------|-------------| -| `200` | Success | -| `404` | Resource not found (e.g., invalid stop/route ID) | -| `500` | Server error | - -**Error Response Format:** -```json -{ - "error": "Description of the error", - "details": "Additional error details (when available)" -} -``` - -**Example Error:** -```bash -curl "http://localhost:3000/api/config?stopId=invalid" -``` - -Response (404): -```json -{ - "error": "Stop invalid not found" -} -``` - ---- - -## Rate Limiting - -Currently, there is no rate limiting implemented. For production deployments, consider: - -- Implementing per-IP rate limits -- Using caching for frequently accessed endpoints -- Load balancing for high-traffic scenarios - -**Recommended Polling Intervals:** -- Real-time arrivals: 10-30 seconds -- Vehicle positions: 10-30 seconds -- Historical data: As needed (not time-sensitive) -- Statistics: 5-60 minutes (depending on use case) - ---- - -## GTFS Data Update - -The GTFS static data (stops, routes, schedules) is loaded from the `gtfs/` directory on server startup. - -**To update GTFS data:** - -```bash -bun run setup-gtfs.ts -``` - -This downloads the latest GTFS feed and extracts it. Restart the server to load the new data. - ---- - -## TimescaleDB Setup - -Historical data and analytics require TimescaleDB. See the [README.md](README.md) for setup instructions. - -**Quick start:** -```bash -cd infrastructure -docker compose up -d -``` - -The database automatically: -- Creates hypertables for time-series data -- Sets up 90-day data retention -- Builds continuous aggregates for hourly metrics -- Maintains indexes for fast queries - ---- - -## Example Use Cases - -### Building a Real-Time Tracker - -```javascript -// 1. Get all stops -const stops = await fetch('/api/stops').then(r => r.json()); - -// 2. Get all routes -const routes = await fetch('/api/routes').then(r => r.json()); - -// 3. Poll for arrivals every 15 seconds -setInterval(async () => { - const arrivals = await fetch('/api/arrivals?stopId=1563&routeId=125') - .then(r => r.json()); - updateUI(arrivals); -}, 15000); - -// 4. Poll for vehicle positions -setInterval(async () => { - const vehicles = await fetch('/api/vehicles?routeId=125') - .then(r => r.json()); - updateMap(vehicles); -}, 15000); -``` - -### Building Analytics Dashboard - -```javascript -// Get delay statistics for route -const stats = await fetch('/api/stats/route/125/delays?hours=168') - .then(r => r.json()); - -// Get hourly patterns -const pattern = await fetch('/api/stats/route/125/hourly?days=30') - .then(r => r.json()); - -// Get stop performance comparison -const stopStats = await fetch('/api/stats/stop/1563/delays?hours=168') - .then(r => r.json()); - -// Display charts with the data -renderDelayChart(stats); -renderHourlyPattern(pattern); -renderStopComparison(stopStats); -``` - ---- - -## Additional Resources - -- **OpenAPI Specification**: [openapi.yaml](openapi.yaml) - Machine-readable API spec -- **Upstream API Documentation**: [UPSTREAM-API-DOCUMENTATION.md](UPSTREAM-API-DOCUMENTATION.md) - ModeShift GTFS API docs -- **Swagger UI Demo**: https://editor.swagger.io/ (paste openapi.yaml content) -- **GTFS Specification**: https://gtfs.org/ -- **GTFS-RT Reference**: https://gtfs.org/realtime/ -- **TimescaleDB Documentation**: https://docs.timescale.com/ -- **Project README**: [README.md](README.md) - ---- - -## Support - -For issues or questions: -1. Check the [README.md](README.md) for setup instructions -2. Review this documentation -3. Check the source code in [server.ts](server.ts) and [lib/gtfs.ts](lib/gtfs.ts) - ---- - -**Last Updated:** February 7, 2026 -**API Version:** 1.0.0 diff --git a/README.md b/README.md index 12c1277..4154aa7 100644 --- a/README.md +++ b/README.md @@ -1,217 +1,70 @@ -# Skopje Bus Tracker +# OpenJSP Bus Tracker -Real-time bus tracking for Skopje public transport. Modular system supporting any stop and route. +Real-time Skopje public transport tracking with Bun, GTFS/GTFS-RT ingestion, parquet persistence, and optional S3-compatible segment upload. + +## What Is In This Repo + +- `bus-tracker-json.ts`: terminal tracker for one stop + one route. +- `background-tracker.ts`: continuous collector for multiple routes/stops. +- `lib/database.ts`: parquet write layer with rolling segments and optional S3 upload. +- `lib/gtfs.ts`: GTFS CSV loading helpers. +- `config.ts`: API base URL, defaults, and tracker timing. + +## Requirements + +- Bun 1.x+ +- Network access to the configured GTFS/JSON upstream APIs ## Quick Start ```bash -npm install -npm run setup-gtfs # Download latest GTFS data -npm run web +bun install +bun run typecheck ``` -Open **http://localhost:3000** - -Visit **http://localhost:3000/analytics.html** for historical data and performance analytics. - -## TimescaleDB Setup - -The application uses TimescaleDB for storing time-series data (vehicle positions, arrivals, delays). - -### Start the database: +Run single stop/route terminal tracker: ```bash -cd infrastructure -docker compose up -d +bun run tracker ``` -### Configure environment: - -Create a `.env` file (or use the defaults): +Run with custom stop and route IDs: ```bash -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=iot_data -POSTGRES_USER=postgres -POSTGRES_PASSWORD=example +bun run tracker -- --stop 1571 --route 125 ``` -The database will automatically: -- Create hypertables for efficient time-series queries -- Set up compression and retention policies (90 days) -- Build continuous aggregates for hourly metrics -- Index data for fast queries - -### Analytics Features: - -- **Vehicle Position History**: Track individual buses over time -- **Delay Analysis**: On-time performance, average delays, patterns -- **Hourly Patterns**: See when buses are typically late/early -- **Route Statistics**: Reliability scores, service quality metrics -- **Stop Performance**: Compare delays across different stops - -### Background Tracker: - -For continuous data collection without keeping the web interface open: +Run background collection pipeline: ```bash -npm run track +bun run track ``` -This automatically tracks these popular routes every 30 seconds: -- Routes: 2, 4, 5, 7, 15, 21, 22, 24 -- Private routes: 12П, 19П, 22П, 45П, 52П, 54П, 61П, 9П +## Environment -Data is stored in TimescaleDB for historical analysis. The tracker runs indefinitely until stopped with Ctrl+C. +Copy `.env.example` to `.env` and adjust values as needed. -## Features +Key variables: -- **Fully Modular Web Interface**: Select any stop and route via UI controls or URL parameters -- **Dynamic Tracking**: Change stops/routes without restarting the server -- Interactive map with live vehicle positions -- Real-time arrivals with delays -- **Time-Series Data Storage**: Historical tracking with TimescaleDB -- **Analytics Dashboard**: Delay statistics, hourly patterns, performance metrics -- 5-second auto-refresh (web), 10-second (terminal) -- CLI arguments for terminal tracker -- Configurable defaults via [config.ts](config.ts) -- Shareable URLs with stop/route parameters +- `PARQUET_DIR`: local output directory for parquet files. +- `PARQUET_ROLL_MINUTES`: segment rotation interval. +- `SAVE_ALL_VEHICLE_SNAPSHOTS`: save full raw vehicle feed snapshots. +- `SAVE_ALL_VEHICLE_POSITIONS`: persist all vehicle positions (not only route-matched). +- `S3_ENABLED`: enable object storage upload. +- `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, `S3_PREFIX`: object storage target. +- `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`: object storage credentials. +- `S3_DELETE_LOCAL_AFTER_UPLOAD`: delete local parquet after successful upload. +- `S3_UPLOAD_RETRIES`, `S3_UPLOAD_RETRY_BASE_MS`: upload retry behavior. -## Commands +## Scripts -```bash -npm run setup-gtfs # Download GTFS data -npm run find -- --stop "american" # Find stop IDs by name -npm run find -- --route "7" # Find route IDs by number/name -npm run web # Web interface at http://localhost:3000 -npm run tracker # Terminal interface (default) -npm run tracker -- --stop 1571 --route 125 # Custom stop/route -npm run track # Background tracker for popular routes (30s intervals) -npm start # Same as web -``` +- `bun run start`: alias for the terminal tracker. +- `bun run tracker`: terminal tracker. +- `bun run track`: background collector. +- `bun run typecheck`: TypeScript no-emit check. -### Finding Stop and Route IDs +## Notes -Not sure which Stop ID or Route ID to use? Use the find command: - -```bash -# Find stops by name (case-insensitive) -npm run find -- --stop "american" -npm run find -- --stop "центар" - -# Find routes by number or name -npm run find -- --route "7" -npm run find -- --route "линија" -``` - -### Web Interface Usage - -1. **Default tracking**: Open `http://localhost:3000` (loads default stop/route, can be changed in UI) -2. **Direct URL**: `http://localhost:3000?stopId=1571&routeId=125` (bookmarkable) -3. **Change tracking**: Use the controls at the top to enter different Stop ID and Route ID -4. **Share**: Copy URL after selecting a stop/route to share with others - -### CLI Arguments - -Terminal tracker supports custom stop and route: - -```bash -npm run tracker -- --stop --route -npm run tracker -- --help -``` - -### API Endpoints - -**This Application's API:** -- Complete docs: **[API-DOCUMENTATION.md](API-DOCUMENTATION.md)** -- Interactive docs: http://localhost:3000/api-docs.html (when server is running) -- OpenAPI spec: **[openapi.yaml](openapi.yaml)** - -**Upstream ModeShift GTFS API:** -- Documentation: **[UPSTREAM-API-DOCUMENTATION.md](UPSTREAM-API-DOCUMENTATION.md)** -- Provider: ModeShift (Skopje public transport data) - -#### Quick Reference - -Query parameters for custom tracking: - -``` -GET /api/config?stopId=1571&routeId=125 -GET /api/arrivals?stopId=1571&routeId=125 -GET /api/vehicles?routeId=125 -GET /api/stops # All stops -GET /api/routes # All routes - -# Historical Data APIs -GET /api/stats/db # Database statistics -GET /api/history/vehicle/:vehicleId?hours=24 -GET /api/history/route/:routeId/vehicles?hours=24 -GET /api/history/stop/:stopId/arrivals?routeId=125&hours=24 -GET /api/stats/route/:routeId/delays?hours=24 -GET /api/stats/stop/:stopId/delays?hours=24 -GET /api/stats/route/:routeId/hourly?days=7 -``` - -## Configuration - -Edit [config.ts](config.ts) to set defaults: - -```typescript -export const config: AppConfig = { - defaultStop: { - stopId: '1571', - name: 'АМЕРИКАН КОЛЕЏ-КОН ЦЕНТАР', - lat: 41.98057556152344, - lon: 21.457794189453125, - }, - defaultRoute: { - routeId: '125', - shortName: '7', - name: 'ЛИНИЈА 7', - }, - server: { - port: 3000, - }, - tracking: { - refreshInterval: { - web: 5000, // 5 seconds - terminal: 10000, // 10 seconds - }, - minutesAhead: 90, - }, + analytics) -├── bus-tracker-json.ts # Terminal tracker (CLI args) -├── lib/ -│ ├── gtfs.ts # GTFS loader -│ └── database.ts # TimescaleDB time-series storage -├── public/ -│ ├── index.html # Live tracker UI -│ └── analytics.html # Analytics dashboard -├── infrastructure/ -│ └── compose.yml # TimescaleDB Docker setup -└── gtfs/ ure - -``` -bus/ -├── config.ts # Configuration (stops, routes, timing) -├── setup-gtfs.ts # GTFS data downloader -├── find-stops-routes.ts # Helper to find Stop/Route IDs -├── server.ts # Web server (modular API) -├── bus-tracker-json.ts # Terminal tracker (CLI args) -├── lib/gtfs.ts # GTFS loader -├── public/index.html # Frontend (modular UI) -└─**TimescaleDB (PostgreSQL)** for time-series data -- Leaflet.js + OpenStreetMap -- Chart.js for analytics visualizations -- GTFS + GTFS-RT Protocol Buffers -- Docker Compose for database - -## Stack - -- Node.js + Express + TypeScript -- Leaflet.js + OpenStreetMap -- GTFS + GTFS-RT Protocol Buffers - -## License - -MIT +- Generated parquet files are intentionally ignored by git (`data/*.parquet`). +- The background tracker rotates segments and uploads each closed segment when S3 is enabled. +- On process shutdown (`SIGINT`/`SIGTERM`), writers are flushed so the current segment is finalized. diff --git a/UPSTREAM-API-DOCUMENTATION.md b/UPSTREAM-API-DOCUMENTATION.md deleted file mode 100644 index 156cea2..0000000 --- a/UPSTREAM-API-DOCUMENTATION.md +++ /dev/null @@ -1,556 +0,0 @@ -# ModeShift GTFS API Documentation - -Documentation for the upstream ModeShift GTFS API used by this Skopje Bus Tracker application. - -## Table of Contents - -- [Overview](#overview) -- [OpenAPI Specification](#openapi-specification) -- [Base URL](#base-url) -- [Authentication](#authentication) -- [Endpoints](#endpoints) - - [GTFS-RT Trip Updates](#gtfs-rt-trip-updates) - - [Public Vehicles (JSON)](#public-vehicles-json) - - [Nearby Stop Times](#nearby-stop-times) -- [Data Models](#data-models) -- [Usage Examples](#usage-examples) - ---- - -## Overview - -The ModeShift API provides real-time transit data for Skopje public transport based on the **GTFS (General Transit Feed Specification)** and **GTFS-Realtime** standards. - -**Provider:** ModeShift -**API Type:** GTFS + GTFS-Realtime -**Data Format:** JSON and Protocol Buffers -**Coverage:** Skopje, North Macedonia - ---- - -## OpenAPI Specification - -A complete **OpenAPI 3.0** specification for the ModeShift API is available at [upstream-openapi.yaml](upstream-openapi.yaml). - -This machine-readable specification includes: -- All upstream API endpoints -- Request/response schemas -- Parameter validation -- Examples for each operation - -You can view it interactively using: -- [Swagger Editor](https://editor.swagger.io/) - Paste the YAML content -- [Redoc](https://redocly.github.io/redoc/) - For a clean documentation view -- VS Code with OpenAPI extensions - ---- - -## Base URL - -``` -https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e -``` - -The base URL includes a tenant/agency identifier (`9814b106-2afe-47c8-919b-bdec6a5e521e`) specific to Skopje's transit system. - ---- - -## Authentication - -Currently, the API appears to be publicly accessible without authentication tokens. Requests can be made directly to the endpoints. - ---- - -## Endpoints - -### GTFS-RT Trip Updates - -Get real-time trip updates in GTFS-Realtime format (Protocol Buffers). - -**Endpoint:** `GET /transport/gtfsrt/tripupdates.pb` - -**Full URL:** -``` -https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb -``` - -**Response Format:** Protocol Buffers (binary) - -**Content-Type:** `application/octet-stream` - -**Description:** -Returns a GTFS-Realtime FeedMessage containing TripUpdate entities. This follows the official [GTFS-Realtime specification](https://gtfs.org/realtime/). - -**Response Structure (Protobuf):** -```protobuf -message FeedMessage { - FeedHeader header = 1; - repeated FeedEntity entity = 2; -} - -message FeedEntity { - required string id = 1; - optional TripUpdate trip_update = 3; -} - -message TripUpdate { - optional TripDescriptor trip = 1; - optional VehicleDescriptor vehicle = 3; - repeated StopTimeUpdate stop_time_update = 2; -} -``` - -**Example Usage (JavaScript):** -```javascript -import GtfsRealtimeBindings from 'gtfs-realtime-bindings'; - -const response = await fetch( - 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb' -); -const buffer = await response.arrayBuffer(); -const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( - new Uint8Array(buffer) -); - -for (const entity of feed.entity) { - if (entity.tripUpdate) { - console.log('Trip:', entity.tripUpdate.trip.tripId); - console.log('Route:', entity.tripUpdate.trip.routeId); - console.log('Vehicle:', entity.tripUpdate.vehicle?.id); - } -} -``` - -**Key Fields:** -- `trip.tripId`: GTFS trip identifier -- `trip.routeId`: GTFS route identifier -- `vehicle.id`: Vehicle identification number -- `vehicle.label`: Public-facing vehicle number -- `stopTimeUpdate[]`: Array of stop time predictions - ---- - -### Public Vehicles (JSON) - -Get current positions and status of all active vehicles in JSON format. - -**Endpoint:** `GET /transport/public/vehicles` - -**Full URL:** -``` -https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles -``` - -**Response Format:** JSON - -**Description:** -Returns an array of all currently active vehicles with their real-time GPS positions, speed, bearing, and status information. - -**Response Structure:** -```json -[ - { - "id": 12345, - "identificationNumber": "MK-SK-1234", - "inventoryNumber": 1234, - "positionLatitude": 41.9981, - "positionLongitude": 21.4254, - "positionBearing": 87.5, - "positionSpeed": 15.3, - "positionModifiedAt": "2026-02-07T14:27:15Z", - "status": 2 - }, - { - "id": 12346, - "identificationNumber": "MK-SK-5678", - "inventoryNumber": 5678, - "positionLatitude": 42.0012, - "positionLongitude": 21.4301, - "positionBearing": 180.0, - "positionSpeed": 0.0, - "positionModifiedAt": "2026-02-07T14:27:10Z", - "status": 1 - } -] -``` - -**Fields:** - -| Field | Type | Description | -|-------|------|-------------| -| `id` | integer | Internal database identifier for the vehicle | -| `identificationNumber` | string | Vehicle license plate or identification (e.g., "MK-SK-1234") | -| `inventoryNumber` | integer | Public-facing bus number shown on the vehicle | -| `positionLatitude` | number | Current GPS latitude (WGS84) | -| `positionLongitude` | number | Current GPS longitude (WGS84) | -| `positionBearing` | number | Direction of travel in degrees (0-360, 0=North, 90=East) | -| `positionSpeed` | number | Current speed in km/h | -| `positionModifiedAt` | string | ISO 8601 timestamp of last position update | -| `status` | integer | Vehicle status code (1=stopped, 2=in transit) | - -**Status Codes:** -- `1` - Vehicle is stopped -- `2` - Vehicle is in transit - -**Example Usage:** -```bash -curl "https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles" -``` - -**Use Cases:** -- Display all vehicles on a map -- Match vehicles to routes using trip update data -- Track real-time vehicle movements - ---- - -### Nearby Stop Times - -Get arrival predictions for stops near a specific location. - -**Endpoint:** `GET /transport/planner/stops/nearbyTimes` - -**Full URL:** -``` -https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes -``` - -**Query Parameters:** - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `latitude` | number | Yes | Search center latitude (WGS84) | -| `longitude` | number | Yes | Search center longitude (WGS84) | -| `radius` | integer | Yes | Search radius in meters | - -**Response Format:** JSON - -**Description:** -Returns stops within the specified radius along with their upcoming arrivals, including real-time predictions. - -**Response Structure:** -```json -[ - { - "id": 1563, - "code": "363", - "name": "KARPOS II - STIV NAUMOV", - "latitude": 41.97964, - "longitude": 21.41283, - "patterns": [ - { - "routeId": 125, - "routeShortName": "7", - "routeLongName": "Centar - GTC - Karpos 4 - Karposh 2", - "headsign": "Karpos 2", - "stopTimes": [ - { - "serviceDay": 1738972800, - "scheduledArrival": 52380, - "realtimeArrival": 52500, - "arrivalDelay": 120, - "realtime": true, - "realtimeState": "UPDATED", - "headsign": "Karpos 2" - }, - { - "serviceDay": 1738972800, - "scheduledArrival": 53280, - "realtimeArrival": 53280, - "arrivalDelay": 0, - "realtime": true, - "realtimeState": "SCHEDULED", - "headsign": "Karpos 2" - } - ] - } - ] - } -] -``` - -**Fields:** - -**Stop Level:** -| Field | Type | Description | -|-------|------|-------------| -| `id` | integer | GTFS stop ID | -| `code` | string | Public stop code | -| `name` | string | Stop name | -| `latitude` | number | Stop latitude | -| `longitude` | number | Stop longitude | -| `patterns[]` | array | Array of route patterns serving this stop | - -**Pattern Level:** -| Field | Type | Description | -|-------|------|-------------| -| `routeId` | integer | GTFS route ID | -| `routeShortName` | string | Public route number (e.g., "7") | -| `routeLongName` | string | Full route name | -| `headsign` | string | Direction/destination | -| `stopTimes[]` | array | Array of upcoming arrivals | - -**Stop Time Level:** -| Field | Type | Description | -|-------|------|-------------| -| `serviceDay` | integer | Unix timestamp (seconds) of service day start (midnight) | -| `scheduledArrival` | integer | Seconds since midnight for scheduled arrival time | -| `realtimeArrival` | integer | Seconds since midnight for predicted arrival time | -| `arrivalDelay` | integer | Delay in seconds (positive=late, negative=early) | -| `realtime` | boolean | Whether prediction is based on real-time data | -| `realtimeState` | string | State: `SCHEDULED`, `UPDATED`, `CANCELED` | -| `headsign` | string | Trip headsign (destination) | - -**Calculating Arrival Time:** -```javascript -const arrival = new Date( - (stopTime.serviceDay + stopTime.realtimeArrival) * 1000 -); -``` - -**Example Usage:** -```bash -# Get arrivals near American College -curl "https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes?latitude=42.00437&longitude=21.39687&radius=50" -``` - -**Use Cases:** -- Display upcoming arrivals at a stop -- Show real-time delay information -- Calculate minutes until arrival -- Filter by specific routes - ---- - -## Data Models - -### GTFS Stop -```typescript -interface Stop { - id: number; // GTFS stop_id (as integer) - code: string; // Public stop code - name: string; // Stop name - latitude: number; // WGS84 latitude - longitude: number; // WGS84 longitude - patterns: Pattern[]; // Routes serving this stop -} -``` - -### Route Pattern -```typescript -interface Pattern { - routeId: number; // GTFS route_id (as integer) - routeShortName: string; // Public route number - routeLongName: string; // Full route name - headsign: string; // Direction/destination - stopTimes: StopTime[]; // Upcoming arrivals -} -``` - -### Stop Time -```typescript -interface StopTime { - serviceDay: number; // Unix timestamp of service day start - scheduledArrival: number; // Seconds since midnight (scheduled) - realtimeArrival: number; // Seconds since midnight (predicted) - arrivalDelay: number; // Delay in seconds - realtime: boolean; // Based on real-time data - realtimeState: string; // SCHEDULED, UPDATED, CANCELED - headsign: string; // Trip headsign -} -``` - -### Vehicle -```typescript -interface Vehicle { - id: number; // Internal ID - identificationNumber: string; // License plate - inventoryNumber: number; // Bus number - positionLatitude: number; // GPS latitude - positionLongitude: number; // GPS longitude - positionBearing: number; // Direction (0-360°) - positionSpeed: number; // Speed in km/h - positionModifiedAt: string; // ISO 8601 timestamp - status: number; // 1=stopped, 2=in_transit -} -``` - ---- - -## Usage Examples - -### Complete Real-Time Tracker Flow - -```javascript -// 1. Get a specific stop's arrivals -const stopLat = 41.97964; -const stopLon = 21.41283; -const radius = 50; - -const nearbyResponse = await fetch( - `https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/planner/stops/nearbyTimes?latitude=${stopLat}&longitude=${stopLon}&radius=${radius}` -); -const stops = await nearbyResponse.json(); - -// 2. Process arrivals for a specific route -const targetRouteId = 125; -const now = new Date(); - -for (const stop of stops) { - for (const pattern of stop.patterns) { - if (pattern.routeId === targetRouteId) { - for (const stopTime of pattern.stopTimes) { - const arrivalTime = new Date( - (stopTime.serviceDay + stopTime.realtimeArrival) * 1000 - ); - const minutesUntil = Math.floor( - (arrivalTime.getTime() - now.getTime()) / 60000 - ); - - console.log(`Route ${pattern.routeShortName} - ${minutesUntil} min`); - console.log(`Delay: ${stopTime.arrivalDelay} seconds`); - } - } - } -} - -// 3. Get all vehicle positions -const vehiclesResponse = await fetch( - 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/public/vehicles' -); -const vehicles = await vehiclesResponse.json(); - -// 4. Get trip updates to match vehicles to routes -const tripUpdatesResponse = await fetch( - 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfsrt/tripupdates.pb' -); -const buffer = await tripUpdatesResponse.arrayBuffer(); -const feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( - new Uint8Array(buffer) -); - -// 5. Build vehicle-to-route mapping -const vehicleRouteMap = new Map(); -for (const entity of feed.entity) { - if (entity.tripUpdate?.vehicle?.id && entity.tripUpdate?.trip?.routeId) { - vehicleRouteMap.set( - entity.tripUpdate.vehicle.id, - entity.tripUpdate.trip.routeId - ); - } -} - -// 6. Filter vehicles by route -const routeVehicles = vehicles.filter(v => { - const vehicleKey = v.identificationNumber || v.inventoryNumber.toString(); - return vehicleRouteMap.get(vehicleKey) == targetRouteId; -}); - -console.log(`Found ${routeVehicles.length} vehicles on route ${targetRouteId}`); -``` - -### Polling for Updates - -```javascript -// Recommended polling intervals -const VEHICLE_UPDATE_INTERVAL = 10000; // 10 seconds -const ARRIVAL_UPDATE_INTERVAL = 15000; // 15 seconds -const TRIP_UPDATE_INTERVAL = 10000; // 10 seconds - -// Poll for vehicle positions -setInterval(async () => { - const vehicles = await fetch(vehiclesEndpoint).then(r => r.json()); - updateMap(vehicles); -}, VEHICLE_UPDATE_INTERVAL); - -// Poll for arrival predictions -setInterval(async () => { - const arrivals = await fetch(nearbyTimesEndpoint).then(r => r.json()); - updateArrivalBoard(arrivals); -}, ARRIVAL_UPDATE_INTERVAL); -``` - ---- - -## GTFS Static Data - -The API uses GTFS static data which should be downloaded separately. Static data includes: - -- **stops.txt**: All stop locations and names -- **routes.txt**: All route definitions -- **trips.txt**: Trip schedules -- **stop_times.txt**: Stop time schedules -- **calendar.txt**: Service calendar -- **shapes.txt**: Route shapes (geographic paths) - -**Note:** The static GTFS feed download URL should be obtained from ModeShift or the transit agency. - ---- - -## Rate Limiting & Best Practices - -1. **Polling Frequency**: - - Don't poll more frequently than every 10 seconds - - Cache responses when appropriate - - Use conditional requests when possible - -2. **Radius Parameter**: - - Use smallest radius that meets your needs (typically 50-100m) - - Larger radius = more data returned = slower response - -3. **Error Handling**: - - Handle network errors gracefully - - Implement exponential backoff for failures - - Parse protobuf data carefully (can be empty) - -4. **Data Volume**: - - Vehicle positions endpoint returns ALL vehicles (100+) - - Filter client-side rather than making multiple requests - - Consider caching vehicle positions - ---- - -## Common Issues & Solutions - -### Empty GTFS-RT Feed -**Problem:** `tripupdates.pb` returns empty or zero-length buffer -**Solution:** This can happen during off-peak hours or system maintenance. Handle gracefully and retry later. - -### Vehicle Matching -**Problem:** Vehicles in JSON don't match trip updates -**Solution:** Try matching by both `identificationNumber` and `inventoryNumber`. Some vehicles may not have active trips. - -### Time Zone Issues -**Problem:** Arrival times are incorrect -**Solution:** The API uses Europe/Skopje timezone. Service day timestamps are at midnight local time. - -### Delay Calculation -**Problem:** Delays seem wrong -**Solution:** Use `realtimeArrival - scheduledArrival` (already provided as `arrivalDelay` field). - ---- - -## Related Documentation - -- **GTFS Specification**: https://gtfs.org/ -- **GTFS-Realtime**: https://gtfs.org/realtime/ -- **Protocol Buffers**: https://protobuf.dev/ -- **gtfs-realtime-bindings**: https://www.npmjs.com/package/gtfs-realtime-bindings - ---- - -## Support - -For issues with the ModeShift API: -- Contact ModeShift support -- Check ModeShift documentation at https://www.modeshift.app/ - -For issues with this tracker application: -- See [README.md](README.md) -- Check [API-DOCUMENTATION.md](API-DOCUMENTATION.md) for the local API - ---- - -**Last Updated:** February 7, 2026 -**API Provider:** ModeShift -**Coverage:** Skopje, North Macedonia diff --git a/bun.lock b/bun.lock index 83d80cf..649ca08 100644 --- a/bun.lock +++ b/bun.lock @@ -5,16 +5,16 @@ "": { "name": "skopje-bus-api-client", "dependencies": { + "@aws-sdk/client-s3": "^3.888.0", "express": "^4.18.2", - "pg": "^8.18.0", + "hyparquet": "^1.25.1", + "hyparquet-writer": "^0.13.0", "protobufjs": "^7.5.4", }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.10.0", - "@types/pg": "^8.11.0", "protobufjs-cli": "^2.0.0", - "ts-node": "^10.9.1", "typescript": "^5.9.3", }, "optionalDependencies": { @@ -23,6 +23,86 @@ }, }, "packages": { + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1008.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/credential-provider-node": "^3.972.20", "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", "@aws-sdk/middleware-expect-continue": "^3.972.7", "@aws-sdk/middleware-flexible-checksums": "^3.973.5", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-location-constraint": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-sdk-s3": "^3.972.19", "@aws-sdk/middleware-ssec": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/signature-v4-multi-region": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.6", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-blob-browser": "^4.2.12", "@smithy/hash-node": "^4.2.11", "@smithy/hash-stream-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/md5-js": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-w/SIRD25v2zVMbkn8CYIxUsac8yf5Jghkhw5j7EsNWdJhl56m/nWpUX7t1etFUW1cnzpFjZV0lXt0dNFSnbXwA=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.973.19", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/credential-provider-env": "^3.972.17", "@aws-sdk/credential-provider-http": "^3.972.19", "@aws-sdk/credential-provider-login": "^3.972.19", "@aws-sdk/credential-provider-process": "^3.972.17", "@aws-sdk/credential-provider-sso": "^3.972.19", "@aws-sdk/credential-provider-web-identity": "^3.972.19", "@aws-sdk/nested-clients": "^3.996.9", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-pVJVjWqVrPqjpFq7o0mCmeZu1Y0c94OCHSYgivdCD2wfmYVtBbwQErakruhgOD8pcMcx9SCqRw1pzHKR7OGBcA=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.9", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-jOXdZ1o+CywQKr6gyxgxuUmnGwTTnY2Kxs1PM7fI6AYtDWDnmW/yKXayNqkF8KjP1unflqMWKVbVt5VgmE3L0g=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.20", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.17", "@aws-sdk/credential-provider-http": "^3.972.19", "@aws-sdk/credential-provider-ini": "^3.972.19", "@aws-sdk/credential-provider-process": "^3.972.17", "@aws-sdk/credential-provider-sso": "^3.972.19", "@aws-sdk/credential-provider-web-identity": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-0xHca2BnPY0kzjDYPH7vk8YbfdBPpWVS67rtqQMalYDQUCBYS37cZ55K6TuFxCoIyNZgSCFrVKr9PXC5BVvQQw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.9", "@aws-sdk/token-providers": "3.1008.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-kVjQsEU3b///q7EZGrUzol9wzwJFKbEzqJKSq82A9ShrUTEO7FNylTtby3sPV19ndADZh1H3FB3+5ZrvKtEEeg=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.9", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-BV1BlTFdG4w4tAihxN7iXDBoNcNewXD4q8uZlNQiUrnqxwGWUhKHODIQVSPlQGxXClEj+63m+cqZskw+ESmeZg=="], + + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA=="], + + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.973.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/crc64-nvme": "^3.972.4", "@aws-sdk/types": "^3.973.5", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Dp3hqE5W6hG8HQ3Uh+AINx9wjjqYmFHbxede54sGj3akx/haIQrkp85lNdTdC+ouNUcSYNiuGkzmyDREfHX1Gg=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.9", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-/CtOHHVFg4ZuN6CnLnYkrqWgVEnbOBC4kNiKa+4fldJ9cioDt3dD/f5vpq0cWLOXwmGL2zgVrVxNhjxWpxNMkg=="], + + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.9", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.9", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.19", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.6", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.9", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-retry": "^4.4.40", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.39", "@smithy/util-defaults-mode-node": "^4.2.42", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-+RpVtpmQbbtzFOKhMlsRcXM/3f1Z49qTOHaA8gEpHOYruERmog6f2AUtf/oTRLCWjR9H2b3roqryV/hI7QMW8w=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.7", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mYhh7FY+7OOqjkYkd6+6GgJOsXK1xBWmuR+c5mxJPj2kr5TBNeZq+nUvE9kANWAux5UxDVrNOSiEM/wlHzC3Lg=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1008.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.9", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-TulwlHQBWcJs668kNUDMZHN51DeLrDsYT59Ux4a/nbvr025gM6HjKJJ3LvnZccam7OS/ZKUVkWomCneRQKJbBg=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.6", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.20", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-iF7G0prk7AvmOK64FcLvc/fW+Ty1H+vttajL7PvJFReU8urMxfYmynTTuFKDTA76Wgpq3FzTPKwabMQIXQHiXQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -31,14 +111,6 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@jsdoc/salty": ["@jsdoc/salty@0.2.9", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -61,13 +133,107 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="], - "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], - "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw=="], + + "@smithy/core": ["@smithy/core@3.23.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.19", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-952rGf7hBRnhUIaeLp6q4MptKW8sPFe5VvkoZ5qIzFAtx6c/QZ/54FS3yootsyUSf9gJX/NBqEBNdNR7jMIlpQ=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="], + + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="], + + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + + "@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.25", "", { "dependencies": { "@smithy/core": "^3.23.11", "@smithy/middleware-serde": "^4.2.14", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-dqjLwZs2eBxIUG6Qtw8/YZ4DvzHGIf0DA18wrgtfP6a50UIO7e2nY0FPdcbv5tVJKqWCCU5BmGMOUwT7Puan+A=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.42", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.5", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vbwyqHRIpIZutNXZpLAozakzamcINaRCpEy1MYmK6xBeW3xN+TyPRA123GjXnuxZIjc9848MRRCugVMTXxC4Eg=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.14", "", { "dependencies": { "@smithy/core": "^3.23.11", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+CcaLoLa5apzSRtloOyG7lQvkUw2ZDml3hRh4QiG9WyEPfW5Ke/3tPOPiPjUneuT59Tpn8+c3RVaUvvkkwqZwg=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.16", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ULC8UCS/HivdCB3jhi+kLFYe4B5gxH2gi9vHBfEIiRrT2jfKiZNiETJSlzRtE6B26XbBHjPtc8iZKSNqMol9bw=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.5", "", { "dependencies": { "@smithy/core": "^3.23.11", "@smithy/middleware-endpoint": "^4.4.25", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" } }, "sha512-UqwYawyqSr/aog8mnLnfbPurS0gi4G7IYDcD28cUIBhsvWs1+rQcL2IwkUQ+QZ7dibaoRzhNF99fAQ9AUcO00w=="], + + "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.41", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.5", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-M1w1Ux0rSVvBOxIIiqbxvZvhnjQ+VUjJrugtORE90BbadSTH+jsQL279KRL3Hv0w69rE7EuYkV/4Lepz/NBW9g=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.44", "", { "dependencies": { "@smithy/config-resolver": "^4.4.11", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.5", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YPze3/lD1KmWuZsl9JlfhcgGLX7AXhSoaCDtiPntUjNW5/YY0lOHjkcgxyE9x/h5vvS1fzDifMGjzqnNlNiqOQ=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.19", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.4.16", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-v4sa+3xTweL1CLO2UP0p7tvIMH/Rq1X4KKOxd568mpe6LSLMQCnDHs4uv7m3ukpl3HvcN2JH6jiCS0SNRXKP/w=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], @@ -89,8 +255,6 @@ "@types/node": ["@types/node@20.19.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA=="], - "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], - "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], @@ -105,12 +269,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -121,6 +281,8 @@ "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -145,8 +307,6 @@ "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -155,8 +315,6 @@ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "diff": ["diff@4.0.4", "", {}, "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -193,6 +351,10 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-xml-builder": ["fast-xml-builder@1.1.2", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA=="], + + "fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -223,6 +385,10 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "hyparquet": ["hyparquet@1.25.1", "", {}, "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w=="], + + "hyparquet-writer": ["hyparquet-writer@0.13.0", "", { "dependencies": { "hyparquet": "1.25.1" } }, "sha512-4Vl2RxwQPjMhCOZ2mCNK/WY3lJiiy+NlUugdsqBPTTy9sw3Uu7sJPU3UMiy7+aRAbY+y2K5dRJuNGTmQ8PdXFQ=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], @@ -245,8 +411,6 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], - "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": "bin/markdown-it.mjs" }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], "markdown-it-anchor": ["markdown-it-anchor@8.6.7", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA=="], @@ -289,32 +453,10 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-expression-matcher": ["path-expression-matcher@1.1.3", "", {}, "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ=="], + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], - - "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - - "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], - - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - - "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], - - "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], - - "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - - "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - - "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - - "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], - - "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - - "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], @@ -355,19 +497,19 @@ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], @@ -387,8 +529,6 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -397,9 +537,11 @@ "xmlcreate": ["xmlcreate@2.0.4", "", {}, "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], @@ -408,5 +550,17 @@ "gtfs-realtime-bindings/protobufjs-cli": ["protobufjs-cli@1.2.0", "", { "dependencies": { "chalk": "^4.0.0", "escodegen": "^1.13.0", "espree": "^9.0.0", "estraverse": "^5.1.0", "glob": "^8.0.0", "jsdoc": "^4.0.0", "minimist": "^1.2.0", "semver": "^7.1.2", "tmp": "^0.2.1", "uglify-js": "^3.7.7" }, "peerDependencies": { "protobufjs": "^7.0.0" }, "bin": { "pbjs": "bin/pbjs", "pbts": "bin/pbts" } }, "sha512-+YvqJEmsmZHGzE5j0tvEzFeHm0sX7pzRFpyj7+GazhkS4Y0r+jgbioVvFxxSWIlPzUel/lxeOnLChBmV8NmyHA=="], "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], } } diff --git a/bus-tracker-json.ts b/bus-tracker-json.ts deleted file mode 100644 index 277761e..0000000 --- a/bus-tracker-json.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { loadGtfsStops, loadGtfsRoutes, GtfsStop, GtfsRoute } from './lib/gtfs'; -import { config } from './config'; - -// ============================================================================ -// CLI Arguments -// ============================================================================ - -function parseArgs() { - const args = process.argv.slice(2); - let stopId = config.defaultStop.stopId; - let routeId = config.defaultRoute.routeId; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--stop' && args[i + 1]) { - stopId = args[i + 1]; - i++; - } else if (args[i] === '--route' && args[i + 1]) { - routeId = args[i + 1]; - i++; - } else if (args[i] === '--help' || args[i] === '-h') { - console.log(` -Usage: npm run tracker [options] - -Options: - --stop Stop ID to track (default: ${config.defaultStop.stopId}) - --route Route ID to track (default: ${config.defaultRoute.routeId}) - --help, -h Show this help message - -Examples: - npm run tracker - npm run tracker -- --stop 1571 --route 125 -`); - process.exit(0); - } - } - - return { stopId, routeId }; -} - -const { stopId: TARGET_STOP_ID, routeId: TARGET_ROUTE_ID } = parseArgs(); - -// ============================================================================ -// Configuration -// ============================================================================ - -// ============================================================================ -// Additional Types -// ============================================================================ - -interface StopTime { - scheduledArrival: number; - scheduledDeparture: number; - realtimeArrival: number; - realtimeDeparture: number; - arrivalDelay: number; - departureDelay: number; - timepoint: boolean; - realtime: boolean; - realtimeState: string; - serviceDay: number; - headsign: string; -} - - -interface Pattern { - routeId: number; - index: number; - stopTimes: StopTime[]; -} - -interface StopArrivalData { - id: number; - patterns: Pattern[]; - distance: number; -} - -interface BusArrival { - tripId: string; - routeName: string; - stopName: string; - headsign: string; - arrivalTime: Date; - scheduledTime: Date; - delaySeconds: number; - minutesUntilArrival: number; - isApproaching: boolean; - realtimeState: string; - isRealtime: boolean; -} - -// ============================================================================ -// Real-time Data Fetching -// ============================================================================ - -async function getNextBuses( - stops: Map, - routes: Map -): Promise { - const targetStop = stops.get(TARGET_STOP_ID); - - if (!targetStop) { - throw new Error(`Stop ${TARGET_STOP_ID} not found in GTFS data`); - } - - // Fetch nearby arrivals using JSON API - const radius = 50; // 50 meters - const nearbyUrl = `${config.baseUrl}/transport/planner/stops/nearbyTimes?latitude=${targetStop.stop_lat}&longitude=${targetStop.stop_lon}&radius=${radius}`; - - const response = await fetch(nearbyUrl); - const nearbyData = await response.json() as StopArrivalData[]; - - const now = new Date(); - const maxTime = new Date(now.getTime() + config.tracking.minutesAhead * 60000); - - const arrivals: BusArrival[] = []; - - // Get route info - const targetRoute = routes.get(TARGET_ROUTE_ID); - - // Process the nearby data - for (const stopData of nearbyData) { - if (stopData.id.toString() !== TARGET_STOP_ID) continue; - - for (const pattern of stopData.patterns) { - // Filter by target route - if (pattern.routeId.toString() !== TARGET_ROUTE_ID) continue; - - const routeInfo = routes.get(pattern.routeId.toString()); - if (!routeInfo) continue; - - for (const stopTime of pattern.stopTimes) { - // Convert service day + seconds to actual timestamp - const serviceDay = new Date(stopTime.serviceDay * 1000); - const arrivalTime = new Date(serviceDay.getTime() + stopTime.realtimeArrival * 1000); - const scheduledTime = new Date(serviceDay.getTime() + stopTime.scheduledArrival * 1000); - - // Only include buses arriving within our time window - if (arrivalTime > maxTime || arrivalTime < now) continue; - - const minutesUntil = Math.floor((arrivalTime.getTime() - now.getTime()) / 60000); - - arrivals.push({ - tripId: `${pattern.routeId}.${pattern.index}`, - routeName: `${routeInfo.route_short_name} - ${routeInfo.route_long_name}`, - stopName: targetStop.stop_name, - headsign: stopTime.headsign, - arrivalTime: arrivalTime, - scheduledTime: scheduledTime, - delaySeconds: stopTime.arrivalDelay, - minutesUntilArrival: minutesUntil, - isApproaching: minutesUntil <= 5 && minutesUntil >= 0, - realtimeState: stopTime.realtimeState, - isRealtime: stopTime.realtime, - }); - } - } - } - - // Sort by arrival time - arrivals.sort((a, b) => a.arrivalTime.getTime() - b.arrivalTime.getTime()); - - return arrivals; -} - -// ============================================================================ -// Display Functions -// ============================================================================ - -function formatArrival(arrival: BusArrival, index: number): string { - const delayMin = Math.floor(arrival.delaySeconds / 60); - - let delayText = ""; - if (delayMin > 0) { - delayText = ` [${delayMin}min LATE]`; - } else if (delayMin < -3) { - delayText = ` [${Math.abs(delayMin)}min EARLY]`; - } else { - delayText = " [ON TIME]"; - } - - const timeStr = arrival.arrivalTime.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: false - }); - - let status = ""; - if (arrival.minutesUntilArrival <= 0) { - status = " >> ARRIVING NOW!"; - } else if (arrival.isApproaching) { - status = " >> APPROACHING"; - } - - const minutesText = arrival.minutesUntilArrival === 0 - ? "NOW" - : `${arrival.minutesUntilArrival} min`; - - const realtimeIndicator = arrival.isRealtime ? "[LIVE]" : "[SCHED]"; - - return ` ${index + 1}. ${timeStr} (in ${minutesText}) ${delayText} ${realtimeIndicator}${status}`; -} - -async function displayBusSchedule( - stops: Map, - routes: Map -) { - console.clear(); - - const targetStop = stops.get(TARGET_STOP_ID); - const targetRoute = routes.get(TARGET_ROUTE_ID); - - console.log("=".repeat(75)); - console.log(` BUS TRACKER - SKOPJE PUBLIC TRANSPORT`); - console.log("=".repeat(75)); - console.log(` Route: ${targetRoute?.route_short_name} - ${targetRoute?.route_long_name}`); - console.log(` Stop: ${targetStop?.stop_name} (Code: ${targetStop?.stop_code})`); - console.log(` Location: ${targetStop?.stop_lat.toFixed(5)}, ${targetStop?.stop_lon.toFixed(5)}`); - console.log("=".repeat(75)); - console.log(` Updated: ${new Date().toLocaleString('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - })}\n`); - - try { - const arrivals = await getNextBuses(stops, routes); - - if (arrivals.length === 0) { - console.log(` No buses scheduled in the next ${config.tracking.minutesAhead} minutes.\n`); - console.log(" This could mean:"); - console.log(" - No buses are currently running on this route"); - console.log(" - The next bus is more than 90 minutes away"); - console.log(" - Service has ended for the day\n"); - return; - } - - console.log(` Found ${arrivals.length} upcoming bus${arrivals.length > 1 ? 'es' : ''}:\n`); - - // Show all buses (or first 10 if too many) - const showCount = Math.min(arrivals.length, 10); - arrivals.slice(0, showCount).forEach((arrival, index) => { - console.log(formatArrival(arrival, index)); - if (arrival.headsign && index < 5) { - console.log(` Direction: ${arrival.headsign}`); - } - }); - - if (arrivals.length > showCount) { - console.log(`\n ... and ${arrivals.length - showCount} more later`); - } - - // Highlight the next bus - const nextBus = arrivals[0]; - if (nextBus) { - console.log("\n" + "-".repeat(75)); - console.log(" NEXT BUS:"); - console.log("-".repeat(75)); - - if (nextBus.minutesUntilArrival <= 0) { - console.log(" >> BUS IS ARRIVING NOW! HEAD TO THE STOP! <<"); - } else if (nextBus.minutesUntilArrival <= 2) { - console.log(` >> Bus arriving in ${nextBus.minutesUntilArrival} minute${nextBus.minutesUntilArrival > 1 ? 's' : ''}! Run! <<`); - } else if (nextBus.minutesUntilArrival <= 5) { - console.log(` >> Bus arriving in ${nextBus.minutesUntilArrival} minutes - Time to head to the stop <<`); - } else if (nextBus.minutesUntilArrival <= 15) { - console.log(` >> ${nextBus.minutesUntilArrival} minutes - Start getting ready <<`); - } else { - console.log(` >> ${nextBus.minutesUntilArrival} minutes - You have time to relax <<`); - } - - console.log(` Direction: ${nextBus.headsign}`); - console.log(` Data source: ${nextBus.isRealtime ? 'Real-time tracking' : 'Scheduled times'}`); - - const delayMin = Math.floor(nextBus.delaySeconds / 60); - if (delayMin > 5) { - console.log(` WARNING: Bus is running ${delayMin} minutes LATE`); - } else if (delayMin < -3) { - console.log(` NOTE: Bus is ${Math.abs(delayMin)} minutes EARLY`); - } - } - - } catch (error) { - console.log(`\n ERROR: ${error}\n`); - console.log(" Please check:"); - console.log(" - Your internet connection"); - console.log(" - The API is accessible"); - console.log(" - The GTFS files are present in ./gtfs/ directory\n"); - } - - console.log("\n" + "=".repeat(75)); - console.log(` Auto-refresh: Every ${config.tracking.refreshInterval.terminal / 1000} seconds`); - console.log(" Press Ctrl+C to stop"); - console.log("=".repeat(75)); -} - -// ============================================================================ -// Main Program -// ============================================================================ - -async function startMonitoring() { - console.log("Loading GTFS data...\n"); - - try { - const stops = loadGtfsStops(); - const routes = loadGtfsRoutes(); - - console.log(`Loaded ${stops.size} stops`); - console.log(`Loaded ${routes.size} routes\n`); - - // Verify target stop and route exist - const targetStop = stops.get(TARGET_STOP_ID); - const targetRoute = routes.get(TARGET_ROUTE_ID); - - if (!targetStop) { - console.error(`ERROR: Stop ${TARGET_STOP_ID} not found in GTFS data`); - process.exit(1); - } - - if (!targetRoute) { - console.error(`ERROR: Route ${TARGET_ROUTE_ID} not found in GTFS data`); - process.exit(1); - } - - console.log("Configuration validated"); - console.log("Starting bus monitor...\n"); - - // Initial display - await displayBusSchedule(stops, routes); - - // Set up periodic refresh - setInterval(async () => { - await displayBusSchedule(stops, routes); - }, config.tracking.refreshInterval.terminal); - - } catch (error) { - console.error(`Failed to start: ${error}`); - console.error("\nPlease ensure:"); - console.error(" 1. GTFS files exist in ./gtfs/ directory"); - console.error(" 2. Files include: stops.txt and routes.txt"); - console.error(" 3. Node.js has permission to read the files"); - process.exit(1); - } -} - -// Start the application -startMonitoring(); diff --git a/find-stops-routes.ts b/find-stops-routes.ts deleted file mode 100644 index 1b46da8..0000000 --- a/find-stops-routes.ts +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Helper script to find Stop IDs and Route IDs - * Usage: - * npm run find -- --stop "american" - * npm run find -- --route "7" - */ - -import { loadGtfsStops, loadGtfsRoutes } from './lib/gtfs'; - -function parseArgs() { - const args = process.argv.slice(2); - let searchStop = ''; - let searchRoute = ''; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--stop' && args[i + 1]) { - searchStop = args[i + 1].toLowerCase(); - i++; - } else if (args[i] === '--route' && args[i + 1]) { - searchRoute = args[i + 1].toLowerCase(); - i++; - } else if (args[i] === '--help' || args[i] === '-h') { - console.log(` -Usage: npm run find -- [options] - -Options: - --stop Search for stops by name (case-insensitive) - --route Search for routes by name or number - --help, -h Show this help message - -Examples: - npm run find -- --stop "american" - npm run find -- --route "7" - npm run find -- --stop "center" - npm run find -- --route "linija" -`); - process.exit(0); - } - } - - return { searchStop, searchRoute }; -} - -async function main() { - const { searchStop, searchRoute } = parseArgs(); - - if (!searchStop && !searchRoute) { - console.log('Please specify --stop or --route. Use --help for usage information.'); - process.exit(1); - } - - console.log('Loading GTFS data...\n'); - const stops = loadGtfsStops(); - const routes = loadGtfsRoutes(); - - if (searchStop) { - console.log(`=== Searching for stops matching "${searchStop}" ===\n`); - const matches = Array.from(stops.values()) - .filter(stop => stop.stop_name.toLowerCase().includes(searchStop)) - .slice(0, 20); // Limit to 20 results - - if (matches.length === 0) { - console.log('No stops found.'); - } else { - console.log(`Found ${matches.length} stop(s):\n`); - matches.forEach(stop => { - console.log(`Stop ID: ${stop.stop_id}`); - console.log(` Name: ${stop.stop_name}`); - console.log(` Code: ${stop.stop_code}`); - console.log(` Location: ${stop.stop_lat}, ${stop.stop_lon}`); - console.log(''); - }); - - if (matches.length === 20) { - console.log('(Showing first 20 results, refine your search for more specific results)\n'); - } - } - } - - if (searchRoute) { - console.log(`=== Searching for routes matching "${searchRoute}" ===\n`); - const matches = Array.from(routes.values()) - .filter(route => - route.route_short_name?.toLowerCase().includes(searchRoute) || - route.route_long_name?.toLowerCase().includes(searchRoute) - ) - .slice(0, 20); // Limit to 20 results - - if (matches.length === 0) { - console.log('No routes found.'); - } else { - console.log(`Found ${matches.length} route(s):\n`); - matches.forEach(route => { - console.log(`Route ID: ${route.route_id}`); - console.log(` Number: ${route.route_short_name}`); - console.log(` Name: ${route.route_long_name}`); - console.log(''); - }); - - if (matches.length === 20) { - console.log('(Showing first 20 results, refine your search for more specific results)\n'); - } - } - } - - console.log('Use these IDs in the web interface or terminal tracker.'); -} - -main(); diff --git a/background-tracker.ts b/index.ts similarity index 80% rename from background-tracker.ts rename to index.ts index 12914a0..27ebb59 100644 --- a/background-tracker.ts +++ b/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun /** * Background tracker for popular bus routes in Skopje - * Continuously monitors GTFS-RT feeds and stores data to TimescaleDB + * Continuously monitors GTFS-RT feeds and stores data as parquet segments */ import GtfsRealtimeBindings from 'gtfs-realtime-bindings'; @@ -10,7 +10,10 @@ import { GtfsRoute, GtfsStop, loadGtfsRoutes, loadGtfsStops } from './lib/gtfs'; import { initDatabase, logVehiclePositions, + logVehicleFeedSnapshot, logArrival, + closeDatabase, + VehicleFeedSnapshot, VehiclePosition } from './lib/database'; @@ -29,8 +32,6 @@ const TRACKED_ROUTES = [ { id: '204', name: 'Route 12 П' }, ]; -// Key stops to monitor for arrival predictions and delay tracking -// These are the busiest transit points in Skopje const MONITORED_STOPS = [ // Central area and major transit hubs '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', @@ -50,15 +51,22 @@ const MONITORED_STOPS = [ const REFRESH_INTERVAL = 30000; // 30 seconds const ARRIVAL_STOP_CAP = 150; // Max stops to query per cycle +const SAVE_ALL_VEHICLE_SNAPSHOTS = (process.env.SAVE_ALL_VEHICLE_SNAPSHOTS ?? 'true').toLowerCase() === 'true'; +const SAVE_ALL_VEHICLE_POSITIONS = (process.env.SAVE_ALL_VEHICLE_POSITIONS ?? 'true').toLowerCase() === 'true'; let stats = { cycles: 0, lastUpdate: new Date(), vehiclesTracked: 0, + vehicleSnapshots: 0, arrivalsLogged: 0, errors: 0, }; +function toOptionalNumber(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined; +} + // Load GTFS stops data const stops = loadGtfsStops(); const stopsByCode = new Map(); @@ -102,6 +110,26 @@ async function trackVehicles() { const allVehicles = await vehiclesResponse.json() as any[]; console.log(` Found ${allVehicles.length} total vehicles`); + + if (SAVE_ALL_VEHICLE_SNAPSHOTS && allVehicles.length > 0) { + const captureTime = Date.now(); + const snapshots: VehicleFeedSnapshot[] = allVehicles.map((vehicle) => ({ + timestamp: captureTime, + sourceTimestamp: toOptionalNumber(vehicle.positionModifiedAt), + vehicleId: vehicle.identificationNumber?.toString() || undefined, + inventoryNumber: vehicle.inventoryNumber?.toString() || undefined, + latitude: toOptionalNumber(vehicle.positionLatitude), + longitude: toOptionalNumber(vehicle.positionLongitude), + speed: toOptionalNumber(vehicle.positionSpeed), + bearing: toOptionalNumber(vehicle.positionBearing), + status: toOptionalNumber(vehicle.status), + rawJson: JSON.stringify(vehicle), + })); + + await logVehicleFeedSnapshot(snapshots); + stats.vehicleSnapshots += snapshots.length; + console.log(` [OK] Logged ${snapshots.length} full-vehicle snapshots`); + } // Fetch trip updates to match vehicles to routes const tripUpdatesResponse = await fetch(config.apiEndpoints.gtfsRtTripUpdates); @@ -129,9 +157,9 @@ async function trackVehicles() { return; } - // Build map of vehicle IDs to route IDs for tracked routes + // Build map of vehicle IDs to route IDs from GTFS trip updates. const vehicleRouteMap = new Map(); - const trackedRouteIds = new Set(TRACKED_ROUTES.map(r => r.id)); + const trackedRouteSet = new Set(TRACKED_ROUTES.map(r => r.id)); for (const entity of feed.entity) { if (!entity.tripUpdate) continue; @@ -139,8 +167,9 @@ async function trackVehicles() { const tripUpdate = entity.tripUpdate; const routeId = tripUpdate.trip?.routeId; - // Only process tracked routes - if (!routeId || !trackedRouteIds.has(routeId)) continue; + // In "all positions" mode keep every route; otherwise keep tracked routes only. + if (!routeId) continue; + if (!SAVE_ALL_VEHICLE_POSITIONS && !trackedRouteSet.has(routeId)) continue; // Get vehicle ID and label - store both as keys const vehicleId = tripUpdate.vehicle?.id; @@ -158,7 +187,7 @@ async function trackVehicles() { } } - console.log(` Matched ${vehicleRouteMap.size} vehicles to tracked routes`); + console.log(` Matched ${vehicleRouteMap.size} vehicles to GTFS routes`); // Debug: Show sample vehicle IDs from both sources if (vehicleRouteMap.size > 0) { @@ -173,7 +202,7 @@ async function trackVehicles() { console.log(` Sample JSON API vehicle IDs: ${sampleJsonIds.join(', ')}`); } - // Filter and prepare vehicle positions for tracked routes + // Prepare vehicle positions. const positions: VehiclePosition[] = []; const now = Date.now(); @@ -183,29 +212,29 @@ async function trackVehicles() { if (!identificationNumber && !inventoryNumber) continue; - // Try multiple matching strategies - let routeInfo = vehicleRouteMap.get(identificationNumber) || - vehicleRouteMap.get(inventoryNumber); - - if (routeInfo) { - positions.push({ - timestamp: now, - vehicleId: identificationNumber || inventoryNumber, - routeId: routeInfo.routeId, - tripId: routeInfo.tripId, - latitude: vehicle.positionLatitude, - longitude: vehicle.positionLongitude, - speed: vehicle.positionSpeed, - bearing: vehicle.positionBearing, - currentStatus: vehicle.status === 2 ? 'IN_TRANSIT_TO' : 'UNKNOWN', - }); - } + // Try multiple matching strategies. + const routeInfo = vehicleRouteMap.get(identificationNumber) || vehicleRouteMap.get(inventoryNumber); + const includeVehicle = SAVE_ALL_VEHICLE_POSITIONS || !!routeInfo; + + if (!includeVehicle) continue; + + positions.push({ + timestamp: now, + vehicleId: identificationNumber || inventoryNumber, + routeId: routeInfo?.routeId || 'UNKNOWN', + tripId: routeInfo?.tripId, + latitude: vehicle.positionLatitude, + longitude: vehicle.positionLongitude, + speed: vehicle.positionSpeed, + bearing: vehicle.positionBearing, + currentStatus: vehicle.status === 2 ? 'IN_TRANSIT_TO' : 'UNKNOWN', + }); } // Log to database if (positions.length > 0) { await logVehiclePositions(positions); - console.log(` [OK] Logged ${positions.length} vehicle positions`); + console.log(` [OK] Logged ${positions.length} vehicle positions${SAVE_ALL_VEHICLE_POSITIONS ? ' (all vehicles mode)' : ''}`); stats.vehiclesTracked += positions.length; } else { console.log(` [WARN] No vehicles found for tracked routes`); @@ -358,6 +387,7 @@ function printStats() { console.log(`Monitoring up to ${ARRIVAL_STOP_CAP} key stops for arrival data`); console.log(`\nCycles completed: ${stats.cycles}`); console.log(`Vehicle positions tracked: ${stats.vehiclesTracked}`); + console.log(`Full vehicle snapshots logged: ${stats.vehicleSnapshots}`); console.log(`Arrival predictions logged: ${stats.arrivalsLogged}`); console.log(`Errors: ${stats.errors}`); console.log(`Last update: ${stats.lastUpdate.toLocaleString()}`); @@ -396,9 +426,17 @@ async function main() { }, 5 * 60 * 1000); // Handle graceful shutdown - process.on('SIGINT', () => { + process.on('SIGINT', async () => { console.log('\n\nShutting down tracker...'); printStats(); + await closeDatabase(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + console.log('\n\nReceived SIGTERM, closing tracker...'); + printStats(); + await closeDatabase(); process.exit(0); }); } diff --git a/infrastructure/compose.yml b/infrastructure/compose.yml deleted file mode 100644 index 08b6a79..0000000 --- a/infrastructure/compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -services: - timescaledb: - image: timescale/timescaledb:latest-pg17 - environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-example} - POSTGRES_DB: ${POSTGRES_DB:-iot_data} - ports: - - "5432:5432" - volumes: - - timescaledb-data:/var/lib/postgresql/data - restart: unless-stopped - - redis: - image: redis:latest - ports: - - "6379:6379" - restart: unless-stopped - volumes: - - redis-data:/data - -volumes: - timescaledb-data: - redis-data: \ No newline at end of file diff --git a/lib/database.ts b/lib/database.ts index 2d4d07a..29ee757 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -1,196 +1,133 @@ -import { Pool, PoolClient, QueryResult } from 'pg'; +import { promises as fs } from 'fs'; +import * as fsSync from 'fs'; +import * as path from 'path'; +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { ByteWriter, ParquetWriter } from 'hyparquet-writer'; +import type { SchemaElement } from 'hyparquet'; +import type { Writer } from 'hyparquet-writer'; -// PostgreSQL connection pool -const pool = new Pool({ - host: process.env.POSTGRES_HOST || 'localhost', - port: parseInt(process.env.POSTGRES_PORT || '5432'), - database: process.env.POSTGRES_DB || 'iot_data', - user: process.env.POSTGRES_USER || 'postgres', - password: process.env.POSTGRES_PASSWORD || 'example', - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, -}); +const env = process.env; +const DATA_DIR = env.PARQUET_DIR ?? path.join(process.cwd(), 'data'); +const ROLL_MINUTES = Number.parseInt(env.PARQUET_ROLL_MINUTES ?? '5', 10); +const ROLL_INTERVAL_MS = Math.max(1, Number.isNaN(ROLL_MINUTES) ? 5 : ROLL_MINUTES) * 60_000; -// Test connection -pool.on('error', (err) => { - console.error('Unexpected error on idle client', err); -}); +const OBJECT_STORAGE_BUCKET = env.S3_BUCKET; +const OBJECT_STORAGE_REGION = env.S3_REGION ?? 'us-east-1'; +const OBJECT_STORAGE_ENDPOINT = env.S3_ENDPOINT; +const OBJECT_STORAGE_PREFIX = (env.S3_PREFIX ?? 'parquet').replace(/^\/+|\/+$/g, ''); +const OBJECT_STORAGE_FORCE_PATH_STYLE = (env.S3_FORCE_PATH_STYLE ?? 'true').toLowerCase() === 'true'; +const OBJECT_STORAGE_ACCESS_KEY = env.S3_ACCESS_KEY_ID; +const OBJECT_STORAGE_SECRET_KEY = env.S3_SECRET_ACCESS_KEY; +const OBJECT_STORAGE_ENABLED = + (env.S3_ENABLED ?? 'false').toLowerCase() === 'true' && + !!OBJECT_STORAGE_BUCKET && + !!OBJECT_STORAGE_ACCESS_KEY && + !!OBJECT_STORAGE_SECRET_KEY; +const DELETE_LOCAL_AFTER_UPLOAD = (env.S3_DELETE_LOCAL_AFTER_UPLOAD ?? 'false').toLowerCase() === 'true'; +const OBJECT_STORAGE_UPLOAD_RETRIES = Math.max(1, Number.parseInt(env.S3_UPLOAD_RETRIES ?? '3', 10) || 3); +const OBJECT_STORAGE_UPLOAD_RETRY_BASE_MS = Math.max(100, Number.parseInt(env.S3_UPLOAD_RETRY_BASE_MS ?? '1000', 10) || 1000); -// Create tables with TimescaleDB hypertables -export async function initDatabase() { - const client = await pool.connect(); - try { - // Enable TimescaleDB extension - await client.query('CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE'); - - // Vehicle positions time-series - await client.query(` - CREATE TABLE IF NOT EXISTS vehicle_positions ( - time TIMESTAMPTZ NOT NULL, - vehicle_id TEXT NOT NULL, - route_id TEXT NOT NULL, - trip_id TEXT, - latitude DOUBLE PRECISION NOT NULL, - longitude DOUBLE PRECISION NOT NULL, - speed DOUBLE PRECISION, - bearing DOUBLE PRECISION, - current_status TEXT, - PRIMARY KEY (time, vehicle_id) - ) - `); - - // Convert to hypertable if not already - try { - await client.query(` - SELECT create_hypertable('vehicle_positions', 'time', - if_not_exists => TRUE, - chunk_time_interval => INTERVAL '1 day' - ) - `); - } catch (e: any) { - if (!e.message?.includes('already a hypertable')) { - console.warn('Note: vehicle_positions hypertable setup:', e.message); - } - } - - // Create indexes - await client.query(` - CREATE INDEX IF NOT EXISTS idx_vehicle_positions_vehicle - ON vehicle_positions(vehicle_id, time DESC) - `); - await client.query(` - CREATE INDEX IF NOT EXISTS idx_vehicle_positions_route - ON vehicle_positions(route_id, time DESC) - `); +type BasicType = 'BOOLEAN' | 'INT32' | 'INT64' | 'FLOAT' | 'DOUBLE' | 'STRING' | 'TIMESTAMP'; - // Arrival records time-series - await client.query(` - CREATE TABLE IF NOT EXISTS arrival_records ( - time TIMESTAMPTZ NOT NULL, - stop_id TEXT NOT NULL, - route_id TEXT NOT NULL, - trip_id TEXT, - scheduled_time TIMESTAMPTZ NOT NULL, - predicted_time TIMESTAMPTZ, - actual_time TIMESTAMPTZ, - delay_seconds INTEGER, - is_realtime BOOLEAN, - headsign TEXT, - PRIMARY KEY (time, stop_id, route_id, scheduled_time) - ) - `); - - // Convert to hypertable - try { - await client.query(` - SELECT create_hypertable('arrival_records', 'time', - if_not_exists => TRUE, - chunk_time_interval => INTERVAL '1 day' - ) - `); - } catch (e: any) { - if (!e.message?.includes('already a hypertable')) { - console.warn('Note: arrival_records hypertable setup:', e.message); - } - } - - // Create indexes - await client.query(` - CREATE INDEX IF NOT EXISTS idx_arrival_records_stop - ON arrival_records(stop_id, time DESC) - `); - await client.query(` - CREATE INDEX IF NOT EXISTS idx_arrival_records_route - ON arrival_records(route_id, time DESC) - `); +type ColumnSource = { + name: string; + data: Array; + type: BasicType; + nullable?: boolean; +}; - // Service metrics (aggregated hourly) - await client.query(` - CREATE TABLE IF NOT EXISTS service_metrics ( - hour_timestamp TIMESTAMPTZ NOT NULL, - route_id TEXT NOT NULL, - stop_id TEXT NOT NULL DEFAULT '', - total_arrivals INTEGER DEFAULT 0, - on_time_arrivals INTEGER DEFAULT 0, - late_arrivals INTEGER DEFAULT 0, - avg_delay_seconds DOUBLE PRECISION DEFAULT 0, - max_delay_seconds INTEGER DEFAULT 0, - active_vehicles INTEGER DEFAULT 0, - PRIMARY KEY (hour_timestamp, route_id, stop_id) - ) - `); +type IncrementalWriter = { + write: (options: { columnData: ColumnSource[]; rowGroupSize?: number | number[]; pageSize?: number }) => void; + finish: () => void; +}; - // Route performance summary - await client.query(` - CREATE TABLE IF NOT EXISTS route_stats ( - route_id TEXT PRIMARY KEY, - route_name TEXT, - last_updated TIMESTAMPTZ, - total_trips INTEGER DEFAULT 0, - avg_delay_seconds DOUBLE PRECISION DEFAULT 0, - reliability_score DOUBLE PRECISION DEFAULT 0 - ) - `); +type SegmentMeta = { + segmentId: string; + vehicleFile: string; + arrivalFile: string; + snapshotFile: string; + vehicleRows: number; + arrivalRows: number; + snapshotRows: number; +}; - // Create continuous aggregates for better performance - try { - await client.query(` - CREATE MATERIALIZED VIEW IF NOT EXISTS hourly_route_delays - WITH (timescaledb.continuous) AS - SELECT - time_bucket('1 hour', time) AS bucket, - route_id, - COUNT(*) as arrival_count, - AVG(delay_seconds) as avg_delay, - MAX(delay_seconds) as max_delay, - COUNT(*) FILTER (WHERE ABS(delay_seconds) <= 120) as on_time_count - FROM arrival_records - WHERE delay_seconds IS NOT NULL - GROUP BY bucket, route_id - WITH NO DATA - `); - - // Refresh policy - await client.query(` - SELECT add_continuous_aggregate_policy('hourly_route_delays', - start_offset => INTERVAL '3 hours', - end_offset => INTERVAL '1 hour', - schedule_interval => INTERVAL '1 hour', - if_not_exists => TRUE - ) - `); - } catch (e: any) { - if (!e.message?.includes('already exists')) { - console.warn('Note: continuous aggregate setup:', e.message); - } - } +const VEHICLE_SCHEMA: SchemaElement[] = [ + { name: 'root', num_children: 9 }, + { name: 'timestamp', type: 'INT64', repetition_type: 'REQUIRED' }, + { name: 'vehicleId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'REQUIRED' }, + { name: 'routeId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'REQUIRED' }, + { name: 'tripId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, + { name: 'latitude', type: 'DOUBLE', repetition_type: 'REQUIRED' }, + { name: 'longitude', type: 'DOUBLE', repetition_type: 'REQUIRED' }, + { name: 'speed', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'bearing', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'currentStatus', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, +]; - // Set up retention policy (keep 90 days) - try { - await client.query(` - SELECT add_retention_policy('vehicle_positions', - INTERVAL '90 days', - if_not_exists => TRUE - ) - `); - await client.query(` - SELECT add_retention_policy('arrival_records', - INTERVAL '90 days', - if_not_exists => TRUE - ) - `); - } catch (e: any) { - console.warn('Note: retention policy setup:', e.message); - } +const ARRIVAL_SCHEMA: SchemaElement[] = [ + { name: 'root', num_children: 10 }, + { name: 'timestamp', type: 'INT64', repetition_type: 'REQUIRED' }, + { name: 'stopId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'REQUIRED' }, + { name: 'routeId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'REQUIRED' }, + { name: 'tripId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, + { name: 'scheduledTime', type: 'INT64', repetition_type: 'REQUIRED' }, + { name: 'predictedTime', type: 'INT64', repetition_type: 'OPTIONAL' }, + { name: 'actualTime', type: 'INT64', repetition_type: 'OPTIONAL' }, + { name: 'delaySeconds', type: 'INT32', repetition_type: 'OPTIONAL' }, + { name: 'isRealtime', type: 'BOOLEAN', repetition_type: 'REQUIRED' }, + { name: 'headsign', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, +]; - console.log('[OK] TimescaleDB initialized successfully'); - } finally { - client.release(); - } -} +const SNAPSHOT_SCHEMA: SchemaElement[] = [ + { name: 'root', num_children: 10 }, + { name: 'timestamp', type: 'INT64', repetition_type: 'REQUIRED' }, + { name: 'sourceTimestamp', type: 'INT64', repetition_type: 'OPTIONAL' }, + { name: 'vehicleId', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, + { name: 'inventoryNumber', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'OPTIONAL' }, + { name: 'latitude', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'longitude', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'speed', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'bearing', type: 'DOUBLE', repetition_type: 'OPTIONAL' }, + { name: 'status', type: 'INT32', repetition_type: 'OPTIONAL' }, + { name: 'rawJson', type: 'BYTE_ARRAY', converted_type: 'UTF8', repetition_type: 'REQUIRED' }, +]; + +let initialized = false; +let vehicleWriter: IncrementalWriter | undefined; +let arrivalWriter: IncrementalWriter | undefined; +let snapshotWriter: IncrementalWriter | undefined; +let vehicleWriteQueue: Promise = Promise.resolve(); +let arrivalWriteQueue: Promise = Promise.resolve(); +let snapshotWriteQueue: Promise = Promise.resolve(); +let rotationQueue: Promise = Promise.resolve(); +let rollTimer: ReturnType | undefined; + +let currentSegmentId = ''; +let currentVehicleFile = ''; +let currentArrivalFile = ''; +let currentSnapshotFile = ''; +let currentSegmentRows = { vehicle: 0, arrival: 0, snapshot: 0 }; + +const writes = { + vehiclePositions: 0, + arrivalRecords: 0, + vehicleSnapshots: 0, +}; + +const s3Client = OBJECT_STORAGE_ENABLED + ? new S3Client({ + region: OBJECT_STORAGE_REGION, + endpoint: OBJECT_STORAGE_ENDPOINT, + forcePathStyle: OBJECT_STORAGE_FORCE_PATH_STYLE, + requestChecksumCalculation: 'WHEN_REQUIRED', + responseChecksumValidation: 'WHEN_REQUIRED', + credentials: { + accessKeyId: OBJECT_STORAGE_ACCESS_KEY as string, + secretAccessKey: OBJECT_STORAGE_SECRET_KEY as string, + }, + }) + : undefined; -// Insert vehicle position export interface VehiclePosition { timestamp: number; vehicleId: string; @@ -203,66 +140,6 @@ export interface VehiclePosition { currentStatus?: string; } -export async function logVehiclePosition(position: VehiclePosition) { - try { - await pool.query(` - INSERT INTO vehicle_positions - (time, vehicle_id, route_id, trip_id, latitude, longitude, speed, bearing, current_status) - VALUES (to_timestamp($1::double precision / 1000), $2, $3, $4, $5, $6, $7, $8, $9) - ON CONFLICT (time, vehicle_id) DO NOTHING - `, [ - position.timestamp, - position.vehicleId, - position.routeId, - position.tripId || null, - position.latitude, - position.longitude, - position.speed || null, - position.bearing || null, - position.currentStatus || null - ]); - } catch (error) { - console.error('Failed to log vehicle position:', error); - } -} - -// Batch insert vehicle positions -export async function logVehiclePositions(positions: VehiclePosition[]) { - if (positions.length === 0) return; - - const client = await pool.connect(); - try { - await client.query('BEGIN'); - - for (const pos of positions) { - await client.query(` - INSERT INTO vehicle_positions - (time, vehicle_id, route_id, trip_id, latitude, longitude, speed, bearing, current_status) - VALUES (to_timestamp($1::double precision / 1000), $2, $3, $4, $5, $6, $7, $8, $9) - ON CONFLICT (time, vehicle_id) DO NOTHING - `, [ - pos.timestamp, - pos.vehicleId, - pos.routeId, - pos.tripId || null, - pos.latitude, - pos.longitude, - pos.speed || null, - pos.bearing || null, - pos.currentStatus || null - ]); - } - - await client.query('COMMIT'); - } catch (error) { - await client.query('ROLLBACK'); - console.error('Failed to batch log vehicle positions:', error); - } finally { - client.release(); - } -} - -// Insert arrival record export interface ArrivalRecord { timestamp: number; stopId: string; @@ -276,189 +153,480 @@ export interface ArrivalRecord { headsign?: string; } -export async function logArrival(arrival: ArrivalRecord) { - try { - await pool.query(` - INSERT INTO arrival_records - (time, stop_id, route_id, trip_id, scheduled_time, predicted_time, actual_time, delay_seconds, is_realtime, headsign) - VALUES ( - to_timestamp($1::double precision / 1000), - $2, $3, $4, - to_timestamp($5::double precision / 1000), - to_timestamp($6::double precision / 1000), - to_timestamp($7::double precision / 1000), - $8, $9, $10 - ) - ON CONFLICT (time, stop_id, route_id, scheduled_time) DO UPDATE SET - predicted_time = EXCLUDED.predicted_time, - delay_seconds = EXCLUDED.delay_seconds, - is_realtime = EXCLUDED.is_realtime - `, [ - arrival.timestamp, - arrival.stopId, - arrival.routeId, - arrival.tripId || null, - arrival.scheduledTime, - arrival.predictedTime || null, - arrival.actualTime || null, - arrival.delaySeconds || null, - arrival.isRealtime, - arrival.headsign || null - ]); - } catch (error) { - console.error('Failed to log arrival:', error); +export interface VehicleFeedSnapshot { + timestamp: number; + sourceTimestamp?: number; + vehicleId?: string; + inventoryNumber?: string; + latitude?: number; + longitude?: number; + speed?: number; + bearing?: number; + status?: number; + rawJson: string; +} + +function segmentId(): string { + return new Date().toISOString().replace(/[.:]/g, '-'); +} + +function buildVehicleFile(id: string): string { + return path.join(DATA_DIR, `vehicle_positions-${id}.parquet`); +} + +function buildArrivalFile(id: string): string { + return path.join(DATA_DIR, `arrival_records-${id}.parquet`); +} + +function buildSnapshotFile(id: string): string { + return path.join(DATA_DIR, `vehicle_snapshots-${id}.parquet`); +} + +function createFileWriter(filename: string): Writer { + const writer = new ByteWriter() as unknown as Writer & { index: number }; + const chunkSize = 1_000_000; + + fsSync.writeFileSync(filename, '', { flag: 'w' }); + + const flush = () => { + const chunk = new Uint8Array(writer.buffer, 0, writer.index); + fsSync.writeFileSync(filename, chunk, { flag: 'a' }); + writer.index = 0; + }; + + writer.ensure = (size: number) => { + if (writer.index > chunkSize) { + flush(); + } + if (writer.index + size > writer.buffer.byteLength) { + const newSize = Math.max(writer.buffer.byteLength * 2, writer.index + size); + const newBuffer = new ArrayBuffer(newSize); + new Uint8Array(newBuffer).set(new Uint8Array(writer.buffer)); + writer.buffer = newBuffer; + writer.view = new DataView(writer.buffer); + } + }; + + writer.getBuffer = () => { + throw new Error('getBuffer not supported for file writer'); + }; + writer.getBytes = () => { + throw new Error('getBytes not supported for file writer'); + }; + writer.finish = () => { + flush(); + }; + + return writer; +} + +function createParquetWriter(filename: string, schema: SchemaElement[]): IncrementalWriter { + return new ParquetWriter({ writer: createFileWriter(filename), schema }) as unknown as IncrementalWriter; +} + +function queuedWrite(queue: Promise, task: () => Promise): Promise { + return queue.then(task).catch((error) => { + console.error('Parquet write failed:', error); + }); +} + +function toInt64(value: number): bigint { + return BigInt(Math.trunc(value)); +} + +function toNullableInt64(value: number | undefined): bigint | null { + return value == null ? null : toInt64(value); +} + +function toVehicleColumns(positions: VehiclePosition[]): ColumnSource[] { + return [ + { name: 'timestamp', data: positions.map((p) => toInt64(p.timestamp)), type: 'INT64', nullable: false }, + { name: 'vehicleId', data: positions.map((p) => p.vehicleId), type: 'STRING', nullable: false }, + { name: 'routeId', data: positions.map((p) => p.routeId), type: 'STRING', nullable: false }, + { name: 'tripId', data: positions.map((p) => p.tripId ?? null), type: 'STRING' }, + { name: 'latitude', data: positions.map((p) => p.latitude), type: 'DOUBLE', nullable: false }, + { name: 'longitude', data: positions.map((p) => p.longitude), type: 'DOUBLE', nullable: false }, + { name: 'speed', data: positions.map((p) => p.speed ?? null), type: 'DOUBLE' }, + { name: 'bearing', data: positions.map((p) => p.bearing ?? null), type: 'DOUBLE' }, + { name: 'currentStatus', data: positions.map((p) => p.currentStatus ?? null), type: 'STRING' }, + ]; +} + +function toArrivalColumns(arrivals: ArrivalRecord[]): ColumnSource[] { + return [ + { name: 'timestamp', data: arrivals.map((a) => toInt64(a.timestamp)), type: 'INT64', nullable: false }, + { name: 'stopId', data: arrivals.map((a) => a.stopId), type: 'STRING', nullable: false }, + { name: 'routeId', data: arrivals.map((a) => a.routeId), type: 'STRING', nullable: false }, + { name: 'tripId', data: arrivals.map((a) => a.tripId ?? null), type: 'STRING' }, + { name: 'scheduledTime', data: arrivals.map((a) => toInt64(a.scheduledTime)), type: 'INT64', nullable: false }, + { name: 'predictedTime', data: arrivals.map((a) => toNullableInt64(a.predictedTime)), type: 'INT64' }, + { name: 'actualTime', data: arrivals.map((a) => toNullableInt64(a.actualTime)), type: 'INT64' }, + { name: 'delaySeconds', data: arrivals.map((a) => (a.delaySeconds == null ? null : Math.trunc(a.delaySeconds))), type: 'INT32' }, + { name: 'isRealtime', data: arrivals.map((a) => a.isRealtime), type: 'BOOLEAN', nullable: false }, + { name: 'headsign', data: arrivals.map((a) => a.headsign ?? null), type: 'STRING' }, + ]; +} + +function toSnapshotColumns(snapshots: VehicleFeedSnapshot[]): ColumnSource[] { + return [ + { name: 'timestamp', data: snapshots.map((s) => toInt64(s.timestamp)), type: 'INT64', nullable: false }, + { name: 'sourceTimestamp', data: snapshots.map((s) => toNullableInt64(s.sourceTimestamp)), type: 'INT64' }, + { name: 'vehicleId', data: snapshots.map((s) => s.vehicleId ?? null), type: 'STRING' }, + { name: 'inventoryNumber', data: snapshots.map((s) => s.inventoryNumber ?? null), type: 'STRING' }, + { name: 'latitude', data: snapshots.map((s) => (s.latitude == null ? null : s.latitude)), type: 'DOUBLE' }, + { name: 'longitude', data: snapshots.map((s) => (s.longitude == null ? null : s.longitude)), type: 'DOUBLE' }, + { name: 'speed', data: snapshots.map((s) => (s.speed == null ? null : s.speed)), type: 'DOUBLE' }, + { name: 'bearing', data: snapshots.map((s) => (s.bearing == null ? null : s.bearing)), type: 'DOUBLE' }, + { name: 'status', data: snapshots.map((s) => (s.status == null ? null : Math.trunc(s.status))), type: 'INT32' }, + { name: 'rawJson', data: snapshots.map((s) => s.rawJson), type: 'STRING', nullable: false }, + ]; +} + +function startNewSegment(id: string): void { + currentSegmentId = id; + currentVehicleFile = buildVehicleFile(id); + currentArrivalFile = buildArrivalFile(id); + currentSnapshotFile = buildSnapshotFile(id); + currentSegmentRows = { vehicle: 0, arrival: 0, snapshot: 0 }; + vehicleWriter = createParquetWriter(currentVehicleFile, VEHICLE_SCHEMA); + arrivalWriter = createParquetWriter(currentArrivalFile, ARRIVAL_SCHEMA); + snapshotWriter = createParquetWriter(currentSnapshotFile, SNAPSHOT_SCHEMA); +} + +async function uploadFileToObjectStorage(filePath: string): Promise { + if (!OBJECT_STORAGE_ENABLED || !s3Client || !OBJECT_STORAGE_BUCKET) { + return true; } + + const keyPrefix = OBJECT_STORAGE_PREFIX ? `${OBJECT_STORAGE_PREFIX}/` : ''; + const key = `${keyPrefix}${path.basename(filePath)}`; + + const body = await fs.readFile(filePath); + + for (let attempt = 1; attempt <= OBJECT_STORAGE_UPLOAD_RETRIES; attempt += 1) { + try { + await s3Client.send( + new PutObjectCommand({ + Bucket: OBJECT_STORAGE_BUCKET, + Key: key, + Body: body, + ContentLength: body.byteLength, + ContentType: 'application/octet-stream', + }) + ); + + console.log(`[OK] Uploaded parquet to object storage: s3://${OBJECT_STORAGE_BUCKET}/${key}`); + return true; + } catch (error) { + const status = (error as { $metadata?: { httpStatusCode?: number } })?.$metadata?.httpStatusCode; + const shouldRetry = !!status && status >= 500; + const canRetry = shouldRetry && attempt < OBJECT_STORAGE_UPLOAD_RETRIES; + + console.error( + `[ERROR] Upload failed for ${filePath} (attempt ${attempt}/${OBJECT_STORAGE_UPLOAD_RETRIES}):`, + error + ); + + if (!canRetry) { + break; + } + + const backoffMs = OBJECT_STORAGE_UPLOAD_RETRY_BASE_MS * attempt; + await new Promise((resolve) => setTimeout(resolve, backoffMs)); + } + } + + return false; } -// Query historical vehicle positions -export async function getVehicleHistory(vehicleId: string, startTime: number, endTime: number) { - const result = await pool.query(` - SELECT - EXTRACT(EPOCH FROM time) * 1000 as timestamp, - latitude, longitude, speed, bearing, current_status - FROM vehicle_positions - WHERE vehicle_id = $1 - AND time BETWEEN to_timestamp($2::double precision / 1000) AND to_timestamp($3::double precision / 1000) - ORDER BY time ASC - `, [vehicleId, startTime, endTime]); - - return result.rows; +async function deleteLocalFile(filePath: string): Promise { + await fs.unlink(filePath).catch((error: unknown) => { + const code = (error as { code?: string }).code; + if (code !== 'ENOENT') { + console.warn(`[WARN] Failed to delete local parquet file ${filePath}:`, error); + } + }); } -// Query route vehicle positions in time range -export async function getRouteVehiclePositions(routeId: string, startTime: number, endTime: number) { - const result = await pool.query(` - SELECT - EXTRACT(EPOCH FROM time) * 1000 as timestamp, - vehicle_id, latitude, longitude, speed, bearing, current_status - FROM vehicle_positions - WHERE route_id = $1 - AND time BETWEEN to_timestamp($2::double precision / 1000) AND to_timestamp($3::double precision / 1000) - ORDER BY time ASC - `, [routeId, startTime, endTime]); - - return result.rows; -} +async function finalizeSegment(reason: string, reopen: boolean): Promise { + if (!initialized) { + return; + } -// Query arrival history for a stop -export async function getStopArrivalHistory(stopId: string, routeId: string, startTime: number, endTime: number) { - const result = await pool.query(` - SELECT - EXTRACT(EPOCH FROM time) * 1000 as timestamp, - EXTRACT(EPOCH FROM scheduled_time) * 1000 as scheduled_time, - EXTRACT(EPOCH FROM predicted_time) * 1000 as predicted_time, - EXTRACT(EPOCH FROM actual_time) * 1000 as actual_time, - delay_seconds, is_realtime, headsign - FROM arrival_records - WHERE stop_id = $1 AND route_id = $2 - AND time BETWEEN to_timestamp($3::double precision / 1000) AND to_timestamp($4::double precision / 1000) - ORDER BY time ASC - `, [stopId, routeId, startTime, endTime]); - - return result.rows; -} + await Promise.all([vehicleWriteQueue, arrivalWriteQueue, snapshotWriteQueue]); -// Get route delay statistics -export async function getRouteDelayStats(routeId: string, hours: number = 24) { - const result = await pool.query(` - SELECT - COUNT(*) as total_arrivals, - AVG(delay_seconds) as avg_delay, - MIN(delay_seconds) as min_delay, - MAX(delay_seconds) as max_delay, - COUNT(*) FILTER (WHERE ABS(delay_seconds) <= 120) as on_time_count, - COUNT(*) FILTER (WHERE delay_seconds > 120) as late_count, - COUNT(*) FILTER (WHERE delay_seconds < -120) as early_count - FROM arrival_records - WHERE route_id = $1 - AND time >= NOW() - INTERVAL '1 hour' * $2 - `, [routeId, hours]); - - return result.rows[0]; -} + const meta: SegmentMeta = { + segmentId: currentSegmentId, + vehicleFile: currentVehicleFile, + arrivalFile: currentArrivalFile, + snapshotFile: currentSnapshotFile, + vehicleRows: currentSegmentRows.vehicle, + arrivalRows: currentSegmentRows.arrival, + snapshotRows: currentSegmentRows.snapshot, + }; -// Get stop delay statistics -export async function getStopDelayStats(stopId: string, hours: number = 24) { - const result = await pool.query(` - SELECT - route_id, - COUNT(*) as total_arrivals, - AVG(delay_seconds) as avg_delay, - COUNT(*) FILTER (WHERE ABS(delay_seconds) <= 120) as on_time_count - FROM arrival_records - WHERE stop_id = $1 - AND time >= NOW() - INTERVAL '1 hour' * $2 - AND delay_seconds IS NOT NULL - GROUP BY route_id - `, [stopId, hours]); - - return result.rows; -} + if (vehicleWriter) { + vehicleWriter.finish(); + vehicleWriter = undefined; + } + if (arrivalWriter) { + arrivalWriter.finish(); + arrivalWriter = undefined; + } + if (snapshotWriter) { + snapshotWriter.finish(); + snapshotWriter = undefined; + } -// Get hourly pattern for a route -export async function getRouteHourlyPattern(routeId: string, days: number = 7) { - const result = await pool.query(` - SELECT - EXTRACT(HOUR FROM ar.time) as hour, - COUNT(*) as arrival_count, - AVG(ar.delay_seconds) as avg_delay, - COUNT(DISTINCT vp.vehicle_id) as unique_vehicles - FROM arrival_records ar - LEFT JOIN vehicle_positions vp ON ar.trip_id = vp.trip_id - AND ABS(EXTRACT(EPOCH FROM (ar.time - vp.time))) < 60 - WHERE ar.route_id = $1 - AND ar.time >= NOW() - INTERVAL '1 day' * $2 - GROUP BY hour - ORDER BY hour - `, [routeId, days]); - - return result.rows; -} + const hasRows = meta.vehicleRows > 0 || meta.arrivalRows > 0 || meta.snapshotRows > 0; -// Cleanup old data (already handled by retention policy, but keep for manual cleanup) -export async function cleanupOldData(daysToKeep: number = 90) { - const client = await pool.connect(); try { - const result1 = await client.query(` - DELETE FROM vehicle_positions - WHERE time < NOW() - INTERVAL '1 day' * $1 - `, [daysToKeep]); - - const result2 = await client.query(` - DELETE FROM arrival_records - WHERE time < NOW() - INTERVAL '1 day' * $1 - `, [daysToKeep]); - - console.log(`Cleaned up old data: ${result1.rowCount} positions, ${result2.rowCount} arrivals`); + if (hasRows) { + const uploadResults = await Promise.all([ + uploadFileToObjectStorage(meta.vehicleFile), + uploadFileToObjectStorage(meta.arrivalFile), + uploadFileToObjectStorage(meta.snapshotFile), + ]); + const allUploaded = uploadResults.every(Boolean); + + if (OBJECT_STORAGE_ENABLED && DELETE_LOCAL_AFTER_UPLOAD && allUploaded) { + await Promise.all([ + deleteLocalFile(meta.vehicleFile), + deleteLocalFile(meta.arrivalFile), + deleteLocalFile(meta.snapshotFile), + ]); + console.log(`[OK] Deleted local parquet segment after upload: ${meta.segmentId}`); + } else if (OBJECT_STORAGE_ENABLED && DELETE_LOCAL_AFTER_UPLOAD && !allUploaded) { + console.warn(`[WARN] Kept local parquet segment due to upload errors: ${meta.segmentId}`); + } + } else if (OBJECT_STORAGE_ENABLED && DELETE_LOCAL_AFTER_UPLOAD) { + await Promise.all([ + deleteLocalFile(meta.vehicleFile), + deleteLocalFile(meta.arrivalFile), + deleteLocalFile(meta.snapshotFile), + ]); + } } finally { - client.release(); + if (reopen) { + startNewSegment(segmentId()); + console.log(`[OK] Rotated parquet segment (${reason}) from ${meta.segmentId}`); + } } } -// Get database statistics +function enqueueRotation(reason: string, reopen: boolean): Promise { + rotationQueue = rotationQueue.then(() => finalizeSegment(reason, reopen)).catch((error) => { + console.error('Parquet rotation failed:', error); + }); + return rotationQueue; +} + +function startRollTimer(): void { + if (rollTimer) { + clearInterval(rollTimer); + } + + rollTimer = setInterval(() => { + void enqueueRotation('interval', true); + }, ROLL_INTERVAL_MS); +} + +async function ensureInitialized(): Promise { + if (initialized) { + return; + } + + await fs.mkdir(DATA_DIR, { recursive: true }); + startNewSegment(segmentId()); + startRollTimer(); + initialized = true; + + const storageInfo = OBJECT_STORAGE_ENABLED + ? `, object storage enabled (bucket=${OBJECT_STORAGE_BUCKET}, deleteLocal=${DELETE_LOCAL_AFTER_UPLOAD})` + : ', object storage disabled'; + + console.log(`[OK] Hyparquet storage ready at ${DATA_DIR}, roll=${ROLL_INTERVAL_MS / 60000}m${storageInfo}`); +} + +export async function initDatabase(): Promise { + await ensureInitialized(); +} + +export async function logVehiclePosition(position: VehiclePosition): Promise { + await logVehiclePositions([position]); +} + +export async function logVehiclePositions(positions: VehiclePosition[]): Promise { + if (positions.length === 0) { + return; + } + + await ensureInitialized(); + await rotationQueue; + + vehicleWriteQueue = queuedWrite(vehicleWriteQueue, async () => { + const writer = vehicleWriter; + if (!writer) { + return; + } + + writer.write({ + columnData: toVehicleColumns(positions), + rowGroupSize: positions.length, + }); + + currentSegmentRows.vehicle += positions.length; + writes.vehiclePositions += positions.length; + }); + + await vehicleWriteQueue; +} + +export async function logArrival(arrival: ArrivalRecord): Promise { + await ensureInitialized(); + await rotationQueue; + + arrivalWriteQueue = queuedWrite(arrivalWriteQueue, async () => { + const writer = arrivalWriter; + if (!writer) { + return; + } + + writer.write({ + columnData: toArrivalColumns([arrival]), + rowGroupSize: 1, + }); + + currentSegmentRows.arrival += 1; + writes.arrivalRecords += 1; + }); + + await arrivalWriteQueue; +} + +export async function logVehicleFeedSnapshot(snapshots: VehicleFeedSnapshot[]): Promise { + if (snapshots.length === 0) { + return; + } + + await ensureInitialized(); + await rotationQueue; + + snapshotWriteQueue = queuedWrite(snapshotWriteQueue, async () => { + const writer = snapshotWriter; + if (!writer) { + return; + } + + writer.write({ + columnData: toSnapshotColumns(snapshots), + rowGroupSize: snapshots.length, + }); + + currentSegmentRows.snapshot += snapshots.length; + writes.vehicleSnapshots += snapshots.length; + }); + + await snapshotWriteQueue; +} + +export async function getVehicleHistory(_vehicleId: string, _startTime: number, _endTime: number) { + return []; +} + +export async function getRouteVehiclePositions(_routeId: string, _startTime: number, _endTime: number) { + return []; +} + +export async function getStopArrivalHistory(_stopId: string, _routeId: string, _startTime: number, _endTime: number) { + return []; +} + +export async function getRouteDelayStats(_routeId: string, _hours: number = 24) { + return { + total_arrivals: 0, + avg_delay: null, + min_delay: null, + max_delay: null, + on_time_count: 0, + late_count: 0, + early_count: 0, + }; +} + +export async function getStopDelayStats(_stopId: string, _hours: number = 24) { + return []; +} + +export async function getRouteHourlyPattern(_routeId: string, _days: number = 7) { + return []; +} + +export async function cleanupOldData(_daysToKeep: number = 90): Promise { + console.log('cleanupOldData skipped: parquet segment mode'); +} + export async function getDatabaseStats() { - const client = await pool.connect(); - try { - const positionCount = await client.query('SELECT COUNT(*) as count FROM vehicle_positions'); - const arrivalCount = await client.query('SELECT COUNT(*) as count FROM arrival_records'); - const oldestPosition = await client.query('SELECT MIN(time) as oldest FROM vehicle_positions'); - const newestPosition = await client.query('SELECT MAX(time) as newest FROM vehicle_positions'); - - return { - vehiclePositions: parseInt(positionCount.rows[0].count), - arrivalRecords: parseInt(arrivalCount.rows[0].count), - oldestRecord: oldestPosition.rows[0].oldest, - newestRecord: newestPosition.rows[0].newest, - dbType: 'TimescaleDB', - host: pool.options.host, - database: pool.options.database - }; - } finally { - client.release(); - } + await ensureInitialized(); + + const [vehicleInfo, arrivalInfo, snapshotInfo] = await Promise.all([ + fs.stat(currentVehicleFile).catch(() => null), + fs.stat(currentArrivalFile).catch(() => null), + fs.stat(currentSnapshotFile).catch(() => null), + ]); + + return { + vehiclePositions: writes.vehiclePositions, + arrivalRecords: writes.arrivalRecords, + vehicleSnapshots: writes.vehicleSnapshots, + oldestRecord: null, + newestRecord: null, + dbType: 'hyparquet(rolling-write)', + host: OBJECT_STORAGE_ENABLED ? 'object-storage+local' : 'local-filesystem', + database: DATA_DIR, + rolling: { + minutes: ROLL_INTERVAL_MS / 60000, + currentSegmentId, + }, + objectStorage: { + enabled: OBJECT_STORAGE_ENABLED, + bucket: OBJECT_STORAGE_BUCKET ?? null, + region: OBJECT_STORAGE_REGION, + endpoint: OBJECT_STORAGE_ENDPOINT ?? null, + prefix: OBJECT_STORAGE_PREFIX, + deleteLocalAfterUpload: DELETE_LOCAL_AFTER_UPLOAD, + }, + files: { + vehicle: { + path: currentVehicleFile, + bytes: vehicleInfo?.size ?? 0, + modifiedAt: vehicleInfo?.mtime ?? null, + }, + arrivals: { + path: currentArrivalFile, + bytes: arrivalInfo?.size ?? 0, + modifiedAt: arrivalInfo?.mtime ?? null, + }, + snapshots: { + path: currentSnapshotFile, + bytes: snapshotInfo?.size ?? 0, + modifiedAt: snapshotInfo?.mtime ?? null, + }, + }, + }; } -// Close database -export async function closeDatabase() { - await pool.end(); +export async function closeDatabase(): Promise { + if (!initialized) { + return; + } + + if (rollTimer) { + clearInterval(rollTimer); + rollTimer = undefined; + } + + await Promise.all([vehicleWriteQueue, arrivalWriteQueue, snapshotWriteQueue]); + await enqueueRotation('shutdown', false); + initialized = false; } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6c458b2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2058 +0,0 @@ -{ - "name": "skopje-bus-tracker", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "skopje-bus-tracker", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "express": "^4.18.2", - "protobufjs": "^7.5.4" - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^20.10.0", - "protobufjs-cli": "^2.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.9.3" - }, - "optionalDependencies": { - "gtfs-realtime-bindings": "^1.1.1" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@jsdoc/salty": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", - "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v12.0.0" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", - "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "devOptional": true, - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "devOptional": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/gtfs-realtime-bindings": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/gtfs-realtime-bindings/-/gtfs-realtime-bindings-1.1.1.tgz", - "integrity": "sha512-+k8+/MmiBmUUWlASs4CeTkV+Qyz/FgbZxXdg9rDU62XRfJOpRaRe+nKWCGKse965jffVZ0tIu1K+R7hRvjSLfQ==", - "optional": true, - "dependencies": { - "protobufjs": "^7.1.2", - "protobufjs-cli": "^1.0.2" - } - }, - "node_modules/gtfs-realtime-bindings/node_modules/protobufjs-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.2.0.tgz", - "integrity": "sha512-+YvqJEmsmZHGzE5j0tvEzFeHm0sX7pzRFpyj7+GazhkS4Y0r+jgbioVvFxxSWIlPzUel/lxeOnLChBmV8NmyHA==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^4.0.0", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "protobufjs": "^7.0.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, - "node_modules/jsdoc": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", - "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/parser": "^7.20.15", - "@jsdoc/salty": "^0.2.1", - "@types/markdown-it": "^14.1.1", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^14.1.0", - "markdown-it-anchor": "^8.6.7", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.7", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", - "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", - "devOptional": true, - "license": "Unlicense", - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "devOptional": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "devOptional": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "devOptional": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "devOptional": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protobufjs-cli": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-2.0.0.tgz", - "integrity": "sha512-/QGByFXSLY9Was5Oq8yKn5lu1pMywGMHgE0fDR9b4nZFTvaWJonHiJxT3HHu2ZuOQAxd7ufYyQtW8Q1ZgW+gPg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "chalk": "^4.0.0", - "escodegen": "^1.13.0", - "espree": "^9.0.0", - "estraverse": "^5.1.0", - "glob": "^8.0.0", - "jsdoc": "^4.0.0", - "minimist": "^1.2.0", - "semver": "^7.1.2", - "tmp": "^0.2.1", - "uglify-js": "^3.7.7" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "protobufjs": "^7.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/requizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", - "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "devOptional": true, - "license": "BSD-2-Clause", - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/package.json b/package.json index 118f9b8..4a92eca 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,10 @@ "description": "Real-time bus tracking for Skopje public transport", "main": "server.ts", "scripts": { - "start": "bun run server.ts", - "track": "bun run background-tracker.ts" + "start": "bun run bus-tracker-json.ts", + "tracker": "bun run bus-tracker-json.ts", + "track": "bun run background-tracker.ts", + "typecheck": "bunx tsc --noEmit" }, "keywords": [ "skopje", @@ -19,8 +21,10 @@ "author": "", "license": "MIT", "dependencies": { + "@aws-sdk/client-s3": "^3.888.0", "express": "^4.18.2", - "pg": "^8.18.0", + "hyparquet": "^1.25.1", + "hyparquet-writer": "^0.13.0", "protobufjs": "^7.5.4" }, "optionalDependencies": { @@ -29,9 +33,7 @@ "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.10.0", - "@types/pg": "^8.11.0", "protobufjs-cli": "^2.0.0", - "ts-node": "^10.9.1", "typescript": "^5.9.3" } } diff --git a/public/analytics.html b/public/analytics.html deleted file mode 100644 index 5a0ebf5..0000000 --- a/public/analytics.html +++ /dev/null @@ -1,500 +0,0 @@ - - - - - - Bus Analytics - Skopje - - - - -
-

Bus Analytics Dashboard

-
Historical performance and delay analysis
-
- - - -
-
- - -
- -
- -
- - -
- -
- -
-
-

Delay Distribution

- -
- -
-

Hourly Service Pattern (Last 7 Days)

- -
- -
-

On-Time Performance

- -
-
-
- - - - diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 306fcaa..0000000 --- a/public/index.html +++ /dev/null @@ -1,642 +0,0 @@ - - - - - - Skopje Bus Tracker - - - - - - - -
-
-
-

Skopje Bus Tracker

-
Select stop and route to track
-
- View Analytics -
-
- -
-
- - -
-
- - -
- -
- -
- - -
-
-
-
- - - - - - - diff --git a/server.ts b/server.ts deleted file mode 100644 index 3e8143c..0000000 --- a/server.ts +++ /dev/null @@ -1,416 +0,0 @@ -import express, { Request, Response } from 'express'; -import GtfsRealtimeBindings from 'gtfs-realtime-bindings'; -import { loadGtfsStops, loadGtfsRoutes } from './lib/gtfs'; -import { config, StopConfig, RouteConfig } from './config'; -import { - initDatabase, - logVehiclePosition, - logVehiclePositions, - logArrival, - getDatabaseStats, - cleanupOldData, - getVehicleHistory, - getRouteVehiclePositions, - getStopArrivalHistory, - getRouteDelayStats, - getStopDelayStats, - getRouteHourlyPattern -} from './lib/database'; - -const app = express(); - -// Initialize database -try { - initDatabase(); - console.log('Database initialized successfully'); -} catch (error) { - console.error('Failed to initialize database:', error); - console.log('Continuing without data logging...'); -} - -// Load GTFS data -const stops = loadGtfsStops(); -const routes = loadGtfsRoutes(); - -// Serve static files -import * as path from 'path'; -app.use(express.static(path.join(__dirname, 'public'))); - -// Serve OpenAPI specification -app.get('/openapi.yaml', (req: Request, res: Response) => { - res.sendFile(path.join(__dirname, 'openapi.yaml')); -}); - -// API Endpoints -app.get('/api/config', (req: Request, res: Response) => { - const stopId = (req.query.stopId as string) || config.defaultStop.stopId; - const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; - - const stop = stops.get(stopId); - const route = routes.get(routeId); - - res.json({ - stop: { - id: stopId, - code: stop?.stop_code, - name: stop?.stop_name, - lat: stop?.stop_lat, - lon: stop?.stop_lon, - }, - route: { - id: routeId, - shortName: route?.route_short_name, - longName: route?.route_long_name, - }, - defaults: { - stopId: config.defaultStop.stopId, - routeId: config.defaultRoute.routeId, - } - }); -}); - -app.get('/api/stops', (req: Request, res: Response) => { - const stopsList = Array.from(stops.values()).map(stop => ({ - id: stop.stop_id, - code: stop.stop_code, - name: stop.stop_name, - lat: stop.stop_lat, - lon: stop.stop_lon, - })); - res.json(stopsList); -}); - -app.get('/api/routes', (req: Request, res: Response) => { - const routesList = Array.from(routes.values()).map(route => ({ - id: route.route_id, - shortName: route.route_short_name, - longName: route.route_long_name, - })); - res.json(routesList); -}); - -app.get('/api/arrivals', async (req: Request, res: Response) => { - try { - const stopId = (req.query.stopId as string) || config.defaultStop.stopId; - const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; - - const stop = stops.get(stopId); - if (!stop) { - return res.status(404).json({ error: `Stop ${stopId} not found` }); - } - - const radius = 50; - const nearbyUrl = `${config.baseUrl}/transport/planner/stops/nearbyTimes?latitude=${stop.stop_lat}&longitude=${stop.stop_lon}&radius=${radius}`; - - const response = await fetch(nearbyUrl); - const nearbyData = await response.json() as any[]; - - const now = new Date(); - const arrivals: any[] = []; - - for (const stopData of nearbyData) { - if (stopData.id.toString() !== stopId) continue; - - for (const pattern of stopData.patterns) { - if (pattern.routeId.toString() !== routeId) continue; - - const routeInfo = routes.get(pattern.routeId.toString()); - - for (const stopTime of pattern.stopTimes) { - const serviceDay = new Date(stopTime.serviceDay * 1000); - const arrivalTime = new Date(serviceDay.getTime() + stopTime.realtimeArrival * 1000); - const scheduledTime = new Date(serviceDay.getTime() + stopTime.scheduledArrival * 1000); - - const minutesUntil = Math.floor((arrivalTime.getTime() - now.getTime()) / 60000); - - if (minutesUntil >= -2 && minutesUntil <= config.tracking.minutesAhead) { - arrivals.push({ - arrivalTime: arrivalTime.toISOString(), - scheduledTime: scheduledTime.toISOString(), - minutesUntil: minutesUntil, - delaySeconds: stopTime.arrivalDelay, - headsign: stopTime.headsign, - isRealtime: stopTime.realtime, - realtimeState: stopTime.realtimeState, - }); - - // Log arrival to database (use scheduled time as primary timestamp to avoid duplicates) - try { - logArrival({ - timestamp: scheduledTime.getTime(), - stopId, - routeId, - scheduledTime: scheduledTime.getTime(), - predictedTime: arrivalTime.getTime(), - delaySeconds: stopTime.arrivalDelay, - isRealtime: stopTime.realtime, - headsign: stopTime.headsign, - }); - } catch (dbError) { - // Fail silently if database is not available - } - } - } - } - } - - arrivals.sort((a, b) => new Date(a.arrivalTime).getTime() - new Date(b.arrivalTime).getTime()); - - res.json(arrivals); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch arrivals' }); - } -}); - -app.get('/api/vehicles', async (req: Request, res: Response) => { - try { - const routeId = (req.query.routeId as string) || config.defaultRoute.routeId; - - // Fetch all vehicles from JSON API - const vehiclesResponse = await fetch(config.apiEndpoints.vehiclesJson); - - if (!vehiclesResponse.ok) { - throw new Error(`HTTP error! status: ${vehiclesResponse.status}`); - } - - const allVehicles = await vehiclesResponse.json() as any[]; - - // Fetch trip updates to find which vehicles are on our route - const tripUpdatesResponse = await fetch(config.apiEndpoints.gtfsRtTripUpdates); - - if (!tripUpdatesResponse.ok) { - console.warn('Could not fetch trip updates, returning all vehicles'); - // Return all vehicles with basic info - res.json(allVehicles.slice(0, 20).map((v: any) => ({ - id: v.id, - vehicleId: v.identificationNumber, - label: v.inventoryNumber, - lat: v.positionLatitude, - lon: v.positionLongitude, - bearing: v.positionBearing, - speed: v.positionSpeed, - timestamp: v.positionModifiedAt, - tripId: '', - currentStopSequence: 0, - currentStatus: 'UNKNOWN', - }))); - return; - } - - const buffer = await tripUpdatesResponse.arrayBuffer(); - - if (buffer.byteLength === 0) { - console.warn('Empty trip updates feed'); - res.json([]); - return; - } - - let feed; - try { - feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode( - new Uint8Array(buffer) - ); - } catch (decodeError) { - console.error('Failed to decode GTFS-RT feed:', decodeError); - res.json([]); - return; - } - - // Find trip IDs and vehicle IDs for our route - const routeVehicleIds = new Set(); - const vehicleTripMap = new Map(); - - for (const entity of feed.entity) { - if (!entity.tripUpdate) continue; - - const tripUpdate = entity.tripUpdate; - - // Filter by target route - if (tripUpdate.trip?.routeId !== routeId) continue; - - // Get vehicle ID if available - if (tripUpdate.vehicle?.id) { - routeVehicleIds.add(tripUpdate.vehicle.id); - vehicleTripMap.set(tripUpdate.vehicle.id, { - tripId: tripUpdate.trip.tripId, - routeId: tripUpdate.trip.routeId, - }); - } - - // Also try vehicle label - if (tripUpdate.vehicle?.label) { - routeVehicleIds.add(tripUpdate.vehicle.label); - vehicleTripMap.set(tripUpdate.vehicle.label, { - tripId: tripUpdate.trip.tripId, - routeId: tripUpdate.trip.routeId, - }); - } - } - - // Filter vehicles by route - const activeVehicles: any[] = []; - - for (const vehicle of allVehicles) { - // Try to match by identification number or inventory number - const vehicleId = vehicle.identificationNumber || vehicle.inventoryNumber?.toString(); - - if (vehicleId && (routeVehicleIds.has(vehicleId) || routeVehicleIds.has(vehicle.inventoryNumber?.toString()))) { - const tripInfo = vehicleTripMap.get(vehicleId) || vehicleTripMap.get(vehicle.inventoryNumber?.toString()); - - activeVehicles.push({ - id: vehicle.id, - vehicleId: vehicle.identificationNumber, - label: vehicle.inventoryNumber, - lat: vehicle.positionLatitude, - lon: vehicle.positionLongitude, - bearing: vehicle.positionBearing, - speed: vehicle.positionSpeed, - timestamp: vehicle.positionModifiedAt, - tripId: tripInfo?.tripId || '', - currentStopSequence: 0, - currentStatus: vehicle.status === 2 ? 'IN_TRANSIT_TO' : 'UNKNOWN', - }); - } - } - - console.log(`Found ${routeVehicleIds.size} vehicle IDs for route ${routeId}`); - console.log(`Matched ${activeVehicles.length} vehicles from JSON API`); - - // Log vehicle positions to database - try { - const positions = activeVehicles.map(v => ({ - timestamp: Date.now(), - vehicleId: v.vehicleId || v.label, - routeId, - tripId: v.tripId, - latitude: v.lat, - longitude: v.lon, - speed: v.speed, - bearing: v.bearing, - currentStatus: v.currentStatus, - })); - logVehiclePositions(positions); - } catch (dbError) { - // Fail silently if database is not available - } - - res.json(activeVehicles); - } catch (error) { - console.error('Error fetching vehicles:', error); - res.status(500).json({ error: 'Failed to fetch vehicles', details: String(error) }); - } -}); - -// Historical Data API Endpoints - -app.get('/api/stats/db', async (req: Request, res: Response) => { - try { - const stats = await getDatabaseStats(); - res.json(stats); - } catch (error) { - res.status(500).json({ error: 'Database not available' }); - } -}); - -app.get('/api/history/vehicle/:vehicleId', async (req: Request, res: Response) => { - try { - const vehicleId = req.params.vehicleId; - const hours = parseInt(req.query.hours as string) || 24; - const endTime = Date.now(); - const startTime = endTime - (hours * 60 * 60 * 1000); - - const history = await getVehicleHistory(vehicleId, startTime, endTime); - res.json(history); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch vehicle history' }); - } -}); - -app.get('/api/history/route/:routeId/vehicles', async (req: Request, res: Response) => { - try { - const routeId = req.params.routeId; - const hours = parseInt(req.query.hours as string) || 24; - const endTime = Date.now(); - const startTime = endTime - (hours * 60 * 60 * 1000); - - const positions = await getRouteVehiclePositions(routeId, startTime, endTime); - res.json(positions); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch route vehicle history' }); - } -}); - -app.get('/api/history/stop/:stopId/arrivals', async (req: Request, res: Response) => { - try { - const stopId = req.params.stopId; - const routeId = (req.query.routeId as string) || ''; - const hours = parseInt(req.query.hours as string) || 24; - const endTime = Date.now(); - const startTime = endTime - (hours * 60 * 60 * 1000); - - const arrivals = await getStopArrivalHistory(stopId, routeId, startTime, endTime); - res.json(arrivals); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch arrival history' }); - } -}); - -app.get('/api/stats/route/:routeId/delays', async (req: Request, res: Response) => { - try { - const routeId = req.params.routeId; - const hours = parseInt(req.query.hours as string) || 24; - - const stats = await getRouteDelayStats(routeId, hours); - res.json(stats); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch delay stats' }); - } -}); - -app.get('/api/stats/stop/:stopId/delays', async (req: Request, res: Response) => { - try { - const stopId = req.params.stopId; - const hours = parseInt(req.query.hours as string) || 24; - - const stats = await getStopDelayStats(stopId, hours); - res.json(stats); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch delay stats' }); - } -}); - -app.get('/api/stats/route/:routeId/hourly', async (req: Request, res: Response) => { - try { - const routeId = req.params.routeId; - const days = parseInt(req.query.days as string) || 7; - - const pattern = await getRouteHourlyPattern(routeId, days); - res.json(pattern); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch hourly pattern' }); - } -}); - -// Start server -app.listen(config.server.port, () => { - console.log(` -=========================================================================== - BUS TRACKER WEB APP -=========================================================================== - Server running at: http://localhost:${config.server.port} - - Pages: - - Main Tracker: http://localhost:${config.server.port} - - Analytics: http://localhost:${config.server.port}/analytics.html - - API Docs: http://localhost:${config.server.port}/api-docs.html - - Features: - - Real-time bus arrivals and vehicle tracking - - Interactive map interface - - Historical analytics and delay statistics - - Complete REST API with OpenAPI specification - - Press Ctrl+C to stop the server -=========================================================================== - `); -}); diff --git a/setup-gtfs.ts b/setup-gtfs.ts deleted file mode 100644 index fef8e7b..0000000 --- a/setup-gtfs.ts +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Downloads and extracts GTFS static data - * Run: npm run setup-gtfs - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as https from 'https'; -import { execSync } from 'child_process'; - -const GTFS_ZIP_URL = 'https://www.modeshift.app/api/v1/9814b106-2afe-47c8-919b-bdec6a5e521e/transport/gtfs/gtfs.zip'; -const GTFS_DIR = path.join(__dirname, 'gtfs'); -const ZIP_FILE = path.join(__dirname, 'gtfs.zip'); - -async function downloadFile(url: string, dest: string): Promise { - return new Promise((resolve, reject) => { - console.log(`Downloading GTFS data from ${url}...`); - const file = fs.createWriteStream(dest); - - https.get(url, (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - // Handle redirect - if (response.headers.location) { - https.get(response.headers.location, (redirectResponse) => { - redirectResponse.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', reject); - } - } else { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - } - }).on('error', (err) => { - fs.unlink(dest, () => reject(err)); - }); - }); -} - -async function extractZip(zipPath: string, destDir: string): Promise { - console.log(`Extracting GTFS data to ${destDir}...`); - - // Create directory if it doesn't exist - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - // Use unzip command (available on most Linux/Mac systems) - try { - execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' }); - } catch (error) { - throw new Error('Failed to extract zip. Make sure unzip is installed.'); - } -} - -async function main() { - try { - // Download GTFS zip - await downloadFile(GTFS_ZIP_URL, ZIP_FILE); - console.log('✓ Download complete'); - - // Extract zip - await extractZip(ZIP_FILE, GTFS_DIR); - console.log('✓ Extraction complete'); - - // Clean up zip file - fs.unlinkSync(ZIP_FILE); - console.log('✓ Cleanup complete'); - - // List extracted files - const files = fs.readdirSync(GTFS_DIR); - console.log(`\n✓ GTFS data ready! Files extracted:\n${files.map(f => ` - ${f}`).join('\n')}`); - - } catch (error) { - console.error('Error setting up GTFS data:', error); - process.exit(1); - } -} - -main(); diff --git a/tsconfig.json b/tsconfig.json index 427b79f..2967dac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], + "types": ["node"], "outDir": "./dist", "rootDir": "./", "strict": true, @@ -16,6 +17,6 @@ "noImplicitAny": true, "strictNullChecks": true }, - "include": ["*.ts"], + "include": ["**/*.ts"], "exclude": ["node_modules", "dist"] }