// Progressive nearby-driver search for the rider side. // // /(api)/driver/nearby is bounded by a radius, so a single fixed value is // always wrong in one direction: too tight and a rider in a quiet area sees an // empty map, too wide and a rider in Beirut gets pins from cars that are forty // minutes away and will never be matched to them. // // So the search starts tight — the handful of cars actually near the rider — // and only widens when that comes back empty, in 5 km steps, until it finds // someone or hits the cap. A rider in a busy street gets a close, honest map; // a rider in the mountains still gets an answer a few seconds later. // // The radius is reported back so the UI can say what it's doing rather than // showing a spinner that looks stuck. import { useEffect, useRef, useState } from "react"; import { fetchAPI } from "@/lib/fetch"; import type { Driver } from "@/types/type"; /** First pass: only cars genuinely next to the rider. */ export const SEARCH_START_RADIUS_M = 2000; /** Each unsuccessful pass widens the net by this much. */ export const SEARCH_STEP_M = 5000; /** Ceiling, matching the server's own cap on the endpoint. */ export const SEARCH_MAX_RADIUS_M = 20000; // Gap between widening attempts. Long enough not to hammer the endpoint with // four requests in a single frame, short enough that a rider in an empty area // reaches the full radius in a few seconds rather than half a minute. const EXPAND_DELAY_MS = 1200; // Once drivers are found (or the search has run out of room to widen), settle // into a steady poll so the map keeps up with cars moving and going offline. // // Matched to the driver heartbeat (lib/use-driver-location PING_INTERVAL_MS): // polling faster than drivers report would burn requests to redraw identical // positions, and polling slower is what made the map look static — at 10s a // car in traffic jumped a whole block between frames, which reads as a glitch // rather than as movement. The markers interpolate between these updates, so // this is the rate at which truth arrives, not the frame rate. const SETTLED_POLL_MS = 5000; // Rounding the rider's position to ~110 m before using it as an effect key. // Raw GPS jitters by a few metres constantly, and without this every jitter // would restart the search from the beginning and the radius would never // climb. const QUANTIZE = 1e3; const quantize = (v: number): number => Math.round(v * QUANTIZE) / QUANTIZE; export type NearbySearch = { drivers: Driver[]; /** Radius the current result set came from, in metres. */ radius: number; /** True while widening — i.e. nothing found yet and there's room to grow. */ expanding: boolean; /** True until the first response lands, so callers can tell "none" from "not yet". */ loading: boolean; }; export const useNearbyDrivers = ( service: string, latitude: number | null, longitude: number | null, ): NearbySearch => { const [drivers, setDrivers] = useState([]); const [radius, setRadius] = useState(SEARCH_START_RADIUS_M); const [expanding, setExpanding] = useState(false); const [loading, setLoading] = useState(true); // Quantized so GPS jitter doesn't restart the search; the raw values are // still what gets sent to the server. const latKey = latitude === null ? null : quantize(latitude); const lngKey = longitude === null ? null : quantize(longitude); const coords = useRef({ latitude, longitude }); coords.current = { latitude, longitude }; useEffect(() => { if (latKey === null || lngKey === null) return; let cancelled = false; let timer: ReturnType; let current = SEARCH_START_RADIUS_M; setRadius(SEARCH_START_RADIUS_M); setLoading(true); const run = async () => { const { latitude: lat, longitude: lng } = coords.current; if (lat === null || lng === null) return; try { const res = await fetchAPI( `/(api)/driver/nearby?service=${service}&lat=${lat}&lng=${lng}&radius=${current}`, ); if (cancelled) return; const found = (res.data ?? []) as Driver[]; setDrivers(found); setRadius(current); setLoading(false); if (found.length === 0 && current < SEARCH_MAX_RADIUS_M) { current = Math.min(current + SEARCH_STEP_M, SEARCH_MAX_RADIUS_M); setExpanding(true); timer = setTimeout(run, EXPAND_DELAY_MS); return; } setExpanding(false); timer = setTimeout(run, SETTLED_POLL_MS); } catch { // A failed request shouldn't collapse the search back to the start — // retry at the same radius on the slow cadence. if (cancelled) return; setLoading(false); setExpanding(false); timer = setTimeout(run, SETTLED_POLL_MS); } }; void run(); return () => { cancelled = true; clearTimeout(timer); }; }, [service, latKey, lngKey]); return { drivers, radius, expanding, loading }; }; /** "2 km" / "20 km" — the radius as riders should read it. */ export const radiusKm = (meters: number): number => Math.round(meters / 1000);