feat: introduce routing status tracking for approved roads and telemetry batch queue limits

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:26 +03:00
parent a8470eb76c
commit 91e9e71ce4
9 changed files with 234 additions and 129 deletions
@@ -50,6 +50,11 @@ export class DriverTelemetryDto {
return val != null ? Number(val) : 0;
})
elevation?: number;
@ApiPropertyOptional({ description: 'Client GPS capture time as ISO-8601', example: '2026-09-16T10:20:30.000Z' })
@IsOptional()
@IsString()
timestamp?: string;
}
export class DriverTelemetryBatchDto {
@@ -25,14 +25,17 @@ export class TelemetryController {
@Post('batch')
@ApiOperation({
summary: 'Batch ingest driver telemetry points 📦',
description: 'Receives an array of telemetry points for offline-buffered sync or high-frequency traces.',
summary: 'Queue navigation telemetry batch 📦',
description: 'Acknowledges a navigation batch immediately; the server persists it from Redis in the background.',
})
async ingestBatch(@Body() body: DriverTelemetryBatchDto) {
if (!body || !Array.isArray(body.points)) {
throw new HttpException('Invalid payload: expected { points: [...] }', HttpStatus.BAD_REQUEST);
}
return this.telemetryService.ingestBatch(body.points);
if (body.points.length > 100) {
throw new HttpException('A telemetry batch may contain at most 100 points', HttpStatus.BAD_REQUEST);
}
return this.telemetryService.enqueueBatch(body.points);
}
@Get('nearby')
+57 -4
View File
@@ -1,4 +1,4 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { TelemetryLog } from './telemetry.entity';
@@ -6,8 +6,11 @@ import { DriverTelemetryDto } from './dto/driver-telemetry.dto';
import { RedisService } from '../common/redis.service';
@Injectable()
export class TelemetryService implements OnModuleInit {
export class TelemetryService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(TelemetryService.name);
private readonly queueKey = 'telemetry:batch:queue';
private draining = false;
private queueTimer?: NodeJS.Timeout;
constructor(
@InjectRepository(TelemetryLog)
@@ -43,6 +46,50 @@ export class TelemetryService implements OnModuleInit {
} catch (err: any) {
this.logger.warn(`Telemetry DB auto-migration check note: ${err.message}`);
}
// The request path only queues data. A small background worker persists it
// in batches, keeping navigation uploads fast even when PostGIS is busy.
this.queueTimer = setInterval(() => void this.drainQueue(), 5_000);
void this.drainQueue();
}
onModuleDestroy() {
if (this.queueTimer) clearInterval(this.queueTimer);
}
async enqueueBatch(points: DriverTelemetryDto[]) {
if (!points?.length) return { success: true, accepted: 0, queued: 0 };
if (points.length > 100) {
throw new Error('A telemetry batch may contain at most 100 points.');
}
await this.redisService.getClient().lPush(this.queueKey, JSON.stringify(points));
const queuedBatches = await this.redisService.getClient().lLen(this.queueKey);
void this.drainQueue();
return { success: true, accepted: points.length, queuedBatches };
}
private async drainQueue() {
if (this.draining) return;
this.draining = true;
try {
// Limit a turn so requests and other Redis work stay responsive.
for (let i = 0; i < 20; i++) {
const raw = await this.redisService.getClient().rPop(this.queueKey);
if (!raw) break;
try {
const points = JSON.parse(raw) as DriverTelemetryDto[];
await this.ingestBatch(points);
} catch (error: any) {
// Put the original payload back at the head for a later retry.
await this.redisService.getClient().rPush(this.queueKey, raw);
this.logger.error(`Telemetry queue persistence failed: ${error.message}`);
break;
}
}
} catch (error: any) {
this.logger.error(`Telemetry queue worker failed: ${error.message}`);
} finally {
this.draining = false;
}
}
/**
@@ -64,7 +111,7 @@ export class TelemetryService implements OnModuleInit {
heading,
distance,
elevation,
timestamp: new Date(),
timestamp: this.captureTime(data.timestamp),
location: {
type: 'Point',
coordinates: [lng, lat],
@@ -127,7 +174,7 @@ export class TelemetryService implements OnModuleInit {
heading,
distance,
elevation,
timestamp: new Date(),
timestamp: this.captureTime(p.timestamp),
location: {
type: 'Point',
coordinates: [lng, lat],
@@ -168,6 +215,12 @@ export class TelemetryService implements OnModuleInit {
};
}
private captureTime(value?: string): Date {
if (!value) return new Date();
const date = new Date(value);
return Number.isNaN(date.getTime()) ? new Date() : date;
}
/**
* Find nearby active drivers using PostGIS spatial geography search
*/