mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 02:30:55 +00:00
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import {
|
|
BOOKING_WINDOW_WS_EVENTS,
|
|
BOOKING_WINDOW_WS_NAMESPACE,
|
|
type BatchBoardChangedEvent,
|
|
type BookingWindowPhaseEvent,
|
|
} from '@edr/types';
|
|
import { Logger } from '@nestjs/common';
|
|
import {
|
|
OnGatewayConnection,
|
|
WebSocketGateway,
|
|
WebSocketServer,
|
|
} from '@nestjs/websockets';
|
|
import { Server, Socket } from 'socket.io';
|
|
|
|
import { WsAuthService } from '../notification-inbox/ws-auth.service';
|
|
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
|
|
|
/**
|
|
* Server → client push for booking-window state changes. Same handshake model
|
|
* as the notifications gateway: clients only listen, the token is verified on
|
|
* connect. Events are broadcast namespace-wide — window state is route-scoped
|
|
* public information for signed-in users, and clients filter/invalidate their
|
|
* own queries.
|
|
*/
|
|
@WebSocketGateway({
|
|
namespace: BOOKING_WINDOW_WS_NAMESPACE,
|
|
cors: { origin: true, credentials: true },
|
|
})
|
|
export class BookingWindowGateway implements OnGatewayConnection {
|
|
private readonly logger = new Logger(BookingWindowGateway.name);
|
|
|
|
@WebSocketServer()
|
|
private readonly server!: Server;
|
|
|
|
constructor(private readonly wsAuth: WsAuthService) {}
|
|
|
|
async handleConnection(socket: Socket): Promise<void> {
|
|
const userId = await this.wsAuth.resolveUserId(this.extractToken(socket));
|
|
if (!userId) {
|
|
this.logger.debug(`Rejected booking-window handshake ${socket.id}`);
|
|
socket.disconnect(true);
|
|
return;
|
|
}
|
|
socket.data.userId = userId;
|
|
// Log at info so "is anyone actually connected?" is answerable from the
|
|
// API log when diagnosing missing live updates.
|
|
this.logger.log(`Booking-window client connected (user ${userId})`);
|
|
}
|
|
|
|
/** Push a schedule's current window state to every connected client. */
|
|
emitPhase(schedule: TrainSchedule): void {
|
|
const payload: BookingWindowPhaseEvent = {
|
|
scheduleId: schedule.id,
|
|
originYardId: schedule.originStationId,
|
|
destinationYardId: schedule.destinationStationId,
|
|
direction: schedule.direction ?? null,
|
|
phase: (schedule.windowPhase ?? 'PRE_WINDOW') as BookingWindowPhaseEvent['phase'],
|
|
bookingWindowStatus: schedule.bookingWindowStatus ?? null,
|
|
bookingCycleNo: schedule.bookingCycleNo,
|
|
windowOpensAt: schedule.windowOpensAt?.toISOString() ?? null,
|
|
windowClosesAt: schedule.windowClosesAt?.toISOString() ?? null,
|
|
docReviewEndsAt: schedule.docReviewEndsAt?.toISOString() ?? null,
|
|
paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
|
|
scheduledDepartureDate: schedule.scheduledDepartureDate?.toISOString() ?? null,
|
|
};
|
|
this.server.emit(BOOKING_WINDOW_WS_EVENTS.PHASE, payload);
|
|
}
|
|
|
|
/**
|
|
* Announce that a schedule's batch-board data changed (payment, allocation,
|
|
* fill, expiry, …). Broadcast namespace-wide like PHASE — the payload carries
|
|
* no board data, only the scheduleId; clients holding that board refetch.
|
|
*/
|
|
emitBatchChanged(scheduleId: string, reason: string): void {
|
|
const payload: BatchBoardChangedEvent = {
|
|
scheduleId,
|
|
reason,
|
|
at: new Date().toISOString(),
|
|
};
|
|
this.server.emit(BOOKING_WINDOW_WS_EVENTS.BATCH_CHANGED, payload);
|
|
}
|
|
|
|
private extractToken(socket: Socket): string | undefined {
|
|
const authToken = socket.handshake.auth?.token as string | undefined;
|
|
if (authToken) return authToken;
|
|
|
|
const queryToken = socket.handshake.query?.token;
|
|
if (typeof queryToken === 'string') return queryToken;
|
|
|
|
const header = socket.handshake.headers?.authorization;
|
|
if (header?.startsWith('Bearer ')) return header.slice(7);
|
|
|
|
return undefined;
|
|
}
|
|
}
|