Skip to main content

Overview

Rodando uses Socket.io to deliver real-time updates during active trips:
  • Driver assignment notifications
  • Driver location tracking
  • Trip status changes (en route, started, completed)
  • Waiting time penalties
  • Trip cancellations
All event payloads are fully typed for type safety and auto-completion.

Event Types

The following events are emitted from the server to passengers:
EventStatus ChangeDescription
assigning_startedpendingassigningSystem started searching for drivers
no_drivers_foundassigningcancelledNo available drivers within timeout
driver_assignedassigningacceptedDriver accepted the trip (basic)
driver_accepted_enrichedassigningacceptedDriver accepted (includes profile/vehicle)
driver_en_routeaccepteden_routeDriver is heading to pickup
driver_arrived_pickupen_routearrivingDriver arrived at pickup location
trip_startedarrivingin_progressPassenger onboard, trip started
trip_completedin_progresscompletedTrip finished successfully
trip_cancelled*cancelledTrip cancelled by driver or system

Event Payloads

All payload types are defined in src/app/core/realtime/trip-realtime.ts:

assigning_started

interface AssigningStartedPayload {
  tripId: string;
  at: string;                       // ISO 8601 timestamp
  previousStatus: 'pending';
  currentStatus: 'assigning';
}
When emitted: Immediately after trip creation when the system begins searching for available drivers.

no_drivers_found

interface NoDriversFoundPayload {
  tripId: string;
  at: string;
  reason?: string | null;           // e.g., "No drivers within 5km"
}
When emitted: After the search timeout (typically 25-60 seconds) if no driver accepts.
The trip status changes to cancelled automatically. The passenger should be prompted to try again or adjust pickup location.

driver_assigned (Basic)

interface DriverAssignedPayloadForPassenger {
  tripId: string;
  driverId: string;
  vehicleId: string;
  at: string;
  currentStatus: 'accepted';
}
When emitted: When a driver accepts the trip (basic payload without profile data).

driver_accepted_enriched (Enriched)

interface DriverAcceptedEnrichedPayload {
  tripId: string;
  at: string;
  currentStatus: 'accepted';
  driver: DriverSlimForPassenger;
  vehicle: VehicleSlimForPassenger;
}

interface DriverSlimForPassenger {
  id: string;
  name: string;
  profilePictureUrl?: string | null;
  ratingAvg?: number | null;        // e.g., 4.8
  ratingCount?: number | null;      // e.g., 142
  phone?: string | null;
}

interface VehicleSlimForPassenger {
  id: string;
  plateNumber?: string | null;      // e.g., "ABC-1234"
  color?: string | null;            // e.g., "Blanco"
  make?: string | null;             // e.g., "Toyota"
  model?: string | null;            // e.g., "Corolla"
  year?: number | null;             // e.g., 2019
}
When emitted: Alternative to driver_assigned that includes driver profile and vehicle details in one payload. Example handling:
socket.on('driver_accepted_enriched', (payload: DriverAcceptedEnrichedPayload) => {
  console.log(`Driver ${payload.driver.name} accepted`);
  console.log(`Vehicle: ${payload.vehicle.make} ${payload.vehicle.model}`);
  console.log(`Plate: ${payload.vehicle.plateNumber}`);
  console.log(`Rating: ${payload.driver.ratingAvg}/5 (${payload.driver.ratingCount} trips)`);
  
  // Update UI with driver info
  this.store.setDriver(payload.driver);
  this.store.setVehicle(payload.vehicle);
});

driver_en_route

interface DriverEnRoutePayload {
  tripId: string;
  driverId: string;
  at: string;
  etaMinutes?: number | null;       // Estimated time to pickup
  driverPosition?: {                // Current driver location
    lat: number;
    lng: number;
  } | null;
}
When emitted: When the driver starts driving toward the pickup location. Example handling:
socket.on('driver_en_route', (payload: DriverEnRoutePayload) => {
  console.log(`Driver is ${payload.etaMinutes} minutes away`);
  
  if (payload.driverPosition) {
    // Update driver marker on map
    this.updateDriverMarker(payload.driverPosition);
  }
});
The driverPosition is updated periodically (every 5-10 seconds) via separate location events or included in subsequent driver_en_route events.

driver_arrived_pickup

interface DriverArrivedPickupPayload {
  tripId: string;
  driverId: string;
  at: string;
  currentStatus: 'arriving';
}
When emitted: When the driver is within ~50 meters of the pickup point.
Waiting time penalties may start accruing after a grace period (typically 2-3 minutes). Listen for separate waiting_penalty events.

trip_started

interface TripStartedPayload {
  tripId: string;
  driverId: string;
  at: string;
  currentStatus: 'in_progress';
}
When emitted: When the driver confirms the passenger is onboard and starts the trip. Example handling:
socket.on('trip_started', (payload: TripStartedPayload) => {
  console.log('Trip started at', payload.at);
  
  // Show "Trip in progress" UI
  this.router.navigate(['/trip-progress']);
});

trip_completed

interface TripCompletedPayload {
  tripId: string;
  driverId: string;
  at: string;
  currentStatus: 'completed';
  fareTotal: number;                // Final fare amount
  currency: string;                 // "CUP" or "USD"
}
When emitted: When the driver ends the trip at the destination. Example handling:
socket.on('trip_completed', (payload: TripCompletedPayload) => {
  console.log(`Trip completed. Total: ${payload.fareTotal} ${payload.currency}`);
  
  // Show rating modal
  this.modalCtrl.create({
    component: TripCompletedRatingModalComponent,
    componentProps: {
      tripId: payload.tripId,
      fare: payload.fareTotal,
      currency: payload.currency
    }
  }).then(modal => modal.present());
});

trip_cancelled

interface TripCancelledPayload {
  tripId: string;
  at: string;
  reason?: string | null;           // e.g., "Driver cancelled", "Passenger no-show"
  currentStatus: 'cancelled';
}
When emitted: When the trip is cancelled by driver, passenger, or system. Possible reasons:
  • "Driver cancelled" - Driver initiated cancellation
  • "Passenger no-show" - Passenger didn’t arrive within timeout
  • "System timeout" - System timeout (e.g., no response from passenger)
  • "Passenger cancelled" - Passenger cancelled the trip

Socket.io Setup

The app uses a dedicated Socket.io service for managing connections:
src/app/core/realtime/socket.service.ts
import { io, Socket } from 'socket.io-client';
import { environment } from '@/environments/environment';

export class SocketService {
  private socket?: Socket;
  
  connect(accessToken: string) {
    this.socket = io(environment.socketUrl, {
      auth: { token: accessToken },
      transports: ['websocket', 'polling'],
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 10
    });
    
    this.socket.on('connect', () => {
      console.log('Socket connected:', this.socket?.id);
    });
    
    this.socket.on('disconnect', (reason) => {
      console.warn('Socket disconnected:', reason);
    });
    
    this.socket.on('connect_error', (err) => {
      console.error('Socket connection error:', err.message);
    });
  }
  
  disconnect() {
    this.socket?.disconnect();
    this.socket = undefined;
  }
  
  on<T>(event: string, handler: (payload: T) => void) {
    this.socket?.on(event, handler);
  }
  
  off(event: string, handler?: Function) {
    this.socket?.off(event, handler);
  }
}

Subscribing to Trip Events

Use the TripActiveFacade to handle real-time updates automatically:
src/app/store/trips/trip-active.facade.ts
export class TripActiveFacade {
  private socket = inject(SocketService);
  
  subscribeToTrip(tripId: string) {
    // Join trip room
    this.socket.emit('join_trip', { tripId });
    
    // Listen for all trip events
    this.socket.on<DriverAcceptedEnrichedPayload>(
      'driver_accepted_enriched',
      (payload) => this.handleDriverAccepted(payload)
    );
    
    this.socket.on<DriverEnRoutePayload>(
      'driver_en_route',
      (payload) => this.handleDriverEnRoute(payload)
    );
    
    this.socket.on<TripStartedPayload>(
      'trip_started',
      (payload) => this.handleTripStarted(payload)
    );
    
    this.socket.on<TripCompletedPayload>(
      'trip_completed',
      (payload) => this.handleTripCompleted(payload)
    );
    
    this.socket.on<TripCancelledPayload>(
      'trip_cancelled',
      (payload) => this.handleTripCancelled(payload)
    );
  }
  
  private handleDriverAccepted(payload: DriverAcceptedEnrichedPayload) {
    this.store.setDriver(payload.driver);
    this.store.setVehicle(payload.vehicle);
    this.store.setStatus('accepted');
  }
  
  // ... other handlers
}

Complete Example

import { inject, Injectable } from '@angular/core';
import { SocketService } from '@/app/core/realtime/socket.service';
import {
  DriverAcceptedEnrichedPayload,
  DriverEnRoutePayload,
  TripStartedPayload,
  TripCompletedPayload,
  TripCancelledPayload
} from '@/app/core/realtime/trip-realtime';

@Injectable({ providedIn: 'root' })
export class TripActiveFacade {
  private socket = inject(SocketService);
  private store = inject(TripActiveStore);
  
  subscribeToTrip(tripId: string) {
    this.socket.on<DriverAcceptedEnrichedPayload>(
      'driver_accepted_enriched',
      (p) => {
        this.store.setDriver(p.driver);
        this.store.setVehicle(p.vehicle);
        this.store.setStatus('accepted');
      }
    );
    
    this.socket.on<DriverEnRoutePayload>(
      'driver_en_route',
      (p) => {
        this.store.setEta(p.etaMinutes);
        if (p.driverPosition) {
          this.store.setDriverPosition(p.driverPosition);
        }
      }
    );
    
    this.socket.on<TripStartedPayload>(
      'trip_started',
      (p) => {
        this.store.setStatus('in_progress');
        this.router.navigate(['/trip-progress']);
      }
    );
    
    this.socket.on<TripCompletedPayload>(
      'trip_completed',
      (p) => {
        this.store.setStatus('completed');
        this.store.setFare(p.fareTotal, p.currency);
        this.showRatingModal(p.tripId);
      }
    );
    
    this.socket.on<TripCancelledPayload>(
      'trip_cancelled',
      (p) => {
        this.store.setStatus('cancelled');
        this.showCancellationAlert(p.reason);
      }
    );
  }
  
  unsubscribeFromTrip(tripId: string) {
    this.socket.emit('leave_trip', { tripId });
    this.socket.off('driver_accepted_enriched');
    this.socket.off('driver_en_route');
    this.socket.off('trip_started');
    this.socket.off('trip_completed');
    this.socket.off('trip_cancelled');
  }
}

Best Practices

Always unsubscribe

Remove event listeners when leaving trip-related components to prevent memory leaks:
ngOnDestroy() {
  this.socket.off('driver_en_route');
  this.socket.off('trip_completed');
}

Handle reconnection

Socket.io reconnects automatically, but you may need to rejoin trip rooms:
this.socket.on('connect', () => {
  const tripId = this.facade.tripId();
  if (tripId) {
    this.socket.emit('join_trip', { tripId });
  }
});

Type all payloads

Always use typed payloads for auto-completion and type safety:
// Good
this.socket.on<TripStartedPayload>('trip_started', (p) => {
  console.log(p.at);  // Type-safe!
});

// Bad
this.socket.on('trip_started', (p: any) => {
  console.log(p.at);  // No type safety
});
Socket.io events are not queued. If the client is disconnected when an event is emitted, it will be lost. Always fetch trip state via REST API when reconnecting.
The at field in all payloads is an ISO 8601 timestamp (e.g., "2026-03-09T14:32:10.234Z"). Use new Date(payload.at) to parse it.

Build docs developers (and LLMs) love