Skip to content

vikasjadhav-dev/pathfinder-live

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 

Repository files navigation

PathFinder Live 🗺️

A Complete Guide to Real-Time Location Tracking in Web Apps

Learning Goal: By reading this project you should understand exactly how Uber, Zomato, Swiggy, and similar apps show a driver's live location to a customer. Every concept used in production is demonstrated here with real, working code.


What You'll Learn

  1. How Live Location Tracking Works
  2. The WebSocket Protocol — Why Not HTTP?
  3. The Server Registry — Who Is Online?
  4. The Fan-Out Pattern — Broadcasting Positions
  5. The Ride System — Peer-to-Peer Tracking
  6. Common Bugs and How We Fixed Them
  7. Production Differences (Uber/Zomato Scale)
  8. Running the Project

1. How Live Location Tracking Works

The basic flow — same in every food-delivery / rideshare app:

DRIVER BROWSER                SERVER                 CUSTOMER BROWSER
      │                          │                          │
      │── GPS update (lat,lng) ──▶│                          │
      │                          │── fan-out to all ────────▶│
      │                          │   other clients           │
      │                          │                          │ map marker moves
      │── GPS update (lat,lng) ──▶│                          │
      │                          │── fan-out ───────────────▶│
  1. Driver's browser calls the Geolocation API (navigator.geolocation.watchPosition) which fires every time the GPS moves. See: client/src/hooks/useLocation.js

  2. Driver sends the coordinates to the server over a persistent WebSocket connection. See: client/src/services/SocketService.jssendLocation()

  3. Server receives the update, stores the latest position, and fans out the message to every other connected client. See: server/index.jsfanOut()

  4. Customer's browser receives the message and updates the map marker. See: client/src/hooks/useRealtimeUsers.jsLOCATION_UPDATE handler

  5. Map marker moves with a smooth animation. See: client/src/components/map-engines/GoogleMapEngine.jsxanimateMarker()


2. The WebSocket Protocol

Why not just poll with fetch every second?

Approach Latency Server load Real-time?
HTTP polling every 1s ~1000ms High (N clients × 1 req/s) No
HTTP long-polling ~100ms Medium Approximate
WebSocket < 10ms Low (one persistent connection) Yes
Server-Sent Events ~10ms Low One-direction only

A WebSocket is a persistent, bidirectional TCP connection. Once established, either side can push data at any time with no HTTP overhead.

Client                    Server
  │── HTTP Upgrade req ──▶│    (standard HTTP handshake)
  │◀─ 101 Switching ───────│    (server agrees to upgrade)
  │                        │
  │◀══ WebSocket frame ════│    (now both sides can push anytime)
  │══ WebSocket frame ════▶│
  │◀══ WebSocket frame ════│

In this project:

  • Server creates a WebSocket server on the same port as Express: server/index.js
  • Client connects and manages reconnection: client/src/services/SocketService.js
  • Vite proxies /ws to the backend in development: client/vite.config.js

3. The Server Registry

The server maintains a single Map called registry:

// server/index.js
const registry = new Map();
// Key:   userId (string, e.g. "a3f2-bc19...")
// Value: { ws, name, lastPosition: {lat,lng} | null, joinedAt }

Rule: A user is ONLINE if and only if they are in this Map.

When a user disconnects (tab closed, network drop), the WebSocket close event fires and we registry.delete(userId). We then broadcast the updated user list. No timers. No "stale" flags. The socket state is the truth.

This is exactly how Uber's presence system works at its core — the only production difference is Redis instead of a JavaScript Map, so it works across multiple servers.

User connects    → registry.set(userId, ...)  → broadcast USER_LIST
User disconnects → registry.delete(userId)    → broadcast USER_LIST
User moves       → registry.get(userId).lastPosition = {lat,lng}
                                              → fan-out LOCATION_UPDATE

When a new user joins, the server sends them the complete USER_LIST which includes every other user's lastPosition. This means the map is fully populated immediately — no waiting for everyone to move.


4. The Fan-Out Pattern

When driver A moves, we want ALL other connected customers to see it:

// server/index.js
function fanOut(excludeId, payload) {
  registry.forEach(({ ws }, id) => {
    if (id !== excludeId && ws.readyState === WebSocket.OPEN) {
      ws.send(payload);
    }
  });
}

This is called a fan-out: one message in → N messages out.

At Zomato/Uber scale this runs through a message broker (Redis Pub/Sub, Kafka) so that the fan-out can happen across multiple server instances:

Driver App  →  Server 1 (Redis PUBLISH)
                    ↓
               Redis Pub/Sub
                    ↓
             Server 1 (sends to customers on Server 1)
             Server 2 (sends to customers on Server 2)
             Server 3 (sends to customers on Server 3)

5. The Ride System

The Ride feature teaches peer-to-peer messaging — when two specific users want to share location with each other (like a delivery boy and the customer who ordered).

Message flow:

User A (customer)             Server              User B (delivery boy)
      │                          │                          │
      │── RIDE_REQUEST ─────────▶│                          │
      │                          │── RIDE_REQUEST ─────────▶│
      │                          │                          │ (popup appears)
      │                          │                          │
      │                          │◀── RIDE_ACCEPT ──────────│
      │◀── RIDE_ACCEPT ──────────│                          │
      │ (map switches to         │                          │
      │  tracking mode)          │                          │
      │                          │                          │
      │◀══ LOCATION_UPDATEs ═════╪═══ LOCATION_UPDATEs ════│
      │ (path polyline draws)    │   (normal broadcasts)    │

The server is just a router for ride messages — it doesn't store ride state. Each client tracks their own ride state using useRide.js.

Ride states:

IDLE → REQUESTING (sent request, waiting) → ACTIVE (tracking live)
     ↑                                           │
     └─────────────── IDLE ◀────────────── cancel/end

6. Common Bugs and Fixes

Bug 1: Infinite Reconnect Loop ✅ Fixed

Symptom: WebSocket connects, immediately disconnects, reconnects, loops forever.

Root cause:

// OLD CODE — DO NOT USE
_connect() {
  if (this.ws) this.ws.close(); // ← THIS fires onclose
  // onclose → _scheduleReconnect() → _connect() → this.ws.close() → ∞
}

Fix: Three-layer defense in client/src/services/SocketService.js:

// 1. Flag: don't reconnect if WE closed intentionally
this._isIntentionalClose = true;
this.ws.onclose = null;   // 2. Remove handler before closing
this.ws.close();

// 3. Only reconnect if the close was unexpected
this.ws.onclose = (e) => {
  if (!this._isIntentionalClose) this._scheduleReconnect();
};

Bug 2: Stale Closure in LOCATION_UPDATE Handler ✅ Fixed

Symptom: Ride partner's marker doesn't move even though they're sending updates.

Root cause:

// OLD CODE — useEffect with deps [ridePartner]
useEffect(() => {
  SocketService.on("LOCATION_UPDATE", ({ userId }) => {
    if (userId !== ridePartner.userId) return; // ← ridePartner is STALE here!
    // ridePartner was null when this closure was created
    // It never sees the updated value
  });
}, [ridePartner]); // re-registers on EVERY position update → drops messages mid-flight

Fix: Use a useRef to give the closure a live reference:

// NEW CODE — client/src/hooks/useRide.js
const ridePartnerRef = useRef(null);
ridePartnerRef.current = ridePartner; // always up-to-date

useEffect(() => {
  SocketService.on("LOCATION_UPDATE", ({ userId }) => {
    const partner = ridePartnerRef.current; // ← reads CURRENT value, never stale
    if (!partner || userId !== partner.userId) return;
    // ...update position
  });
}, []); // registers ONCE, no re-subscription storms

Bug 3: Race Condition — User Position Dropped ✅ Fixed

Symptom: When User A joins, User B can see them in the list but they have no position until A moves again.

Root cause: USER_LIST arrives slightly after LOCATION_UPDATE. Old code dropped updates for unknown users:

// OLD CODE
if (!existing) return prev; // silently dropped!

Fix: Add unknown users as placeholders (client/src/hooks/useRealtimeUsers.js):

if (existing) {
  // Normal: update known user's position
  next.set(userId, { ...existing, lastPosition: newPosition });
} else {
  // Race condition: add placeholder, USER_LIST will fill in the name
  next.set(userId, { name: `User-${userId.slice(0,6)}`, status:"online", lastPosition: newPosition });
}

Bug 4: isStale Timer Removes Ride Button ✅ Fixed

Symptom: Ride button disappears after 30 seconds even though user is still online.

Root cause: Old code had a setTimeout that set isStale = true after 30s of no GPS updates. This hid the Ride button.

Fix: Removed entirely. Online = in the registry. The server deletes users on socket close. No client-side timer can know better than the server.


7. Production Differences

Here's what separates this educational demo from a production system like Zomato:

Feature This Project Production (Zomato/Uber)
Presence tracking In-memory JS Map Redis with key expiry
Fan-out Loop over Map Redis Pub/Sub / Kafka
Horizontal scaling Single server Multiple servers behind load balancer
Authentication None JWT on WS handshake
Location history MongoDB TimescaleDB / InfluxDB
GPS accuracy Browser Geolocation API Mobile SDK (higher accuracy)
Path routing Straight-line polyline Road-snapped route (Google Directions API)
ETA Speed × distance ML model (traffic, driver patterns)
Geofencing None Server-side polygon checks

8. Running the Project

Requirements

  • Node.js 18+
  • MongoDB (local or Atlas)
  • Google Maps JavaScript API key (or use without for a blank map)

Server

cd server
cp .env.example .env        # edit MONGO_URI if needed
npm install
npm run dev                 # starts on http://localhost:4000

Client

cd client
cp .env.example .env        # add VITE_GOOGLE_MAPS_API_KEY
npm install
npm run dev                 # starts on http://localhost:5173

Testing Live Tracking

  1. Open http://localhost:5173 in Tab 1 — grant location permission
  2. Open http://localhost:5173 in Tab 2 — grant location permission
  3. Each tab gets a random username (e.g. "Swift Fox", "Nova Hawk")
  4. Both appear in each other's sidebar immediately
  5. Click Ride on the other user — they get a popup
  6. Accept → both see the live path trail and ETA

Testing Without Real GPS

Both tabs show the same physical location (your computer). To simulate movement, open browser DevTools → Sensors → override geolocation and change the lat/lng values — the map marker will animate to the new position.


File Map

server/
  index.js              ← WebSocket server, registry, fan-out, ride routing
  models/LocationEvent.js  ← MongoDB schema for path replay
  routes/location.js    ← REST API: history, latest, delete

client/src/
  services/
    SocketService.js    ← WS connection, reconnect logic, ride message methods

  hooks/
    useLocation.js      ← Geolocation API wrapper (watchPosition + distance gate)
    useRealtimeUsers.js ← Live user registry from WS events (no stale timers)
    useRide.js          ← Full ride lifecycle state machine

  components/
    Dashboard.jsx       ← Main layout, connect-once guard
    MapContainer.jsx    ← Bridges map engine ↔ real-time data
    MapWrapper.jsx      ← Swap Google Maps / MapLibre / Leaflet here
    map-engines/
      GoogleMapEngine.jsx  ← Dark map, animated markers, ride path polyline
    UserCard.jsx        ← User row with always-visible Ride/End button
    RidePanel.jsx       ← Bottom sheet: request / waiting / active ride UI
    ConnectionBadge.jsx ← WS status indicator

About

Real-time multi-user GPS tracking dashboard — WebSocket fan-out, live path polylines, peer-to-peer ride system, and swappable map engines. Built to teach how Zomato/Uber live tracking works under the hood.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors