// Uber-style auto-match dispatch. A requested ride has no driver; this engine // offers it to the nearest eligible driver of the matching service. Drivers // accept/decline; a decline (or a 15s offer expiry) triggers the next-nearest // match. There is no background worker — matchNextDriver is called lazily from // the rider status poll and the driver poll, so matching progresses on every // request cycle. import { transaction } from "@/lib/db"; import { haversine } from "@/lib/utils"; // A driver has this long to respond to an offer before it expires and the next // driver is offered. Tuned short so a rider searching for a driver isn't left // hanging on a phone that's face-down on a seat. const OFFER_TTL_SECONDS = 15; // A driver whose last location ping is older than this is treated as offline // even if their `online` flag is still true (they closed the app without // toggling off). const DRIVER_STALE_SECONDS = 60; type EligibleDriver = { id: number; latitude: number; longitude: number; }; // Offer `rideId` to the nearest eligible driver, if no offer is already in // flight for it. Idempotent: safe to call on every poll. Returns the driver id // that was offered, or null if no driver was available. export const matchNextDriver = async ( rideId: number, ): Promise => { try { return await transaction(async (tx) => { // Lock the ride row so concurrent matchers serialize on it. const rides = await tx<{ status: string; service: string }>` SELECT status, service FROM rides WHERE ride_id = ${rideId} FOR UPDATE `; const ride = rides[0]; if (!ride || ride.status !== "requested") return null; // Expire any offers that have been sitting past their TTL. await tx` UPDATE ride_offers SET status = 'expired', responded_at = CURRENT_TIMESTAMP WHERE ride_id = ${rideId} AND status = 'offered' AND offered_at < CURRENT_TIMESTAMP - make_interval(secs => ${OFFER_TTL_SECONDS}) `; // If there is still an active (unexpired) offer in flight, leave it — // don't stack a second offer on top. const inFlight = await tx<{ n: number }>` SELECT COUNT(*)::int AS n FROM ride_offers WHERE ride_id = ${rideId} AND status = 'offered' `; if ((inFlight[0]?.n ?? 0) > 0) return null; const rideOrigin = await tx<{ lat: number; lng: number }>` SELECT origin_latitude AS lat, origin_longitude AS lng FROM rides WHERE ride_id = ${rideId} `; const origin = rideOrigin[0]; if (!origin) return null; // Eligible: right service, online, fresh, a real account, not on an // active ride, and not already offered/declined for THIS ride. const candidates = await tx` SELECT d.id, d.latitude, d.longitude FROM drivers d WHERE d.service = ${ride.service} AND d.online = TRUE AND d.user_id IS NOT NULL AND d.latitude IS NOT NULL AND d.longitude IS NOT NULL AND d.last_seen > CURRENT_TIMESTAMP - make_interval(secs => ${DRIVER_STALE_SECONDS}) AND NOT EXISTS ( SELECT 1 FROM rides r WHERE r.driver_id = d.id AND r.status IN ('accepted', 'en_route') ) AND NOT EXISTS ( SELECT 1 FROM ride_offers ro WHERE ro.ride_id = ${rideId} AND ro.driver_id = d.id ) `; if (candidates.length === 0) return null; // Nearest by great-circle distance to the pickup point. candidates.sort((a, b) => { const da = haversine(origin.lat, origin.lng, a.latitude, a.longitude); const db = haversine(origin.lat, origin.lng, b.latitude, b.longitude); return da - db; }); const nearest = candidates[0]; await tx` INSERT INTO ride_offers (ride_id, driver_id, status) VALUES (${rideId}, ${nearest.id}, 'offered') `; return nearest.id; }); } catch (error) { console.error("[MATCH_NEXT_DRIVER]: ", error); return null; } };