Add route error toasts with waypoint rollback

- Show error toasts when route calculation fails, with distinct messages:
  - "No route found" for BRouter 4xx (unroutable waypoints, missing tiles)
  - "Route calculation failed" for server errors / network failures
  - "Too many requests" for rate limiting
- Rollback all waypoints to last known good state on route failure
- Guard against restore→recompute loop via restoringRef flag
- Extract reusable useToasts hook from awareness toast logic
- Add BRouterError class to distinguish client vs server errors
- API returns 422 for unroutable requests instead of blanket 502

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-29 14:09:20 +02:00
parent 6a18980d7e
commit b9a77be020
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 100 additions and 24 deletions

View file

@ -21,11 +21,36 @@ function getWaypointsFromYjs(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[]
}));
}
function restoreWaypoints(yjs: YjsState, snapshot: WaypointData[], restoringRef: React.RefObject<boolean>) {
restoringRef.current = true;
yjs.doc.transact(() => {
for (let i = 0; i < snapshot.length && i < yjs.waypoints.length; i++) {
const yMap = yjs.waypoints.get(i);
const wp = snapshot[i]!;
if (yMap) {
yMap.set("lat", wp.lat);
yMap.set("lon", wp.lon);
}
}
// Remove extra waypoints added since snapshot
if (yjs.waypoints.length > snapshot.length) {
yjs.waypoints.delete(snapshot.length, yjs.waypoints.length - snapshot.length);
}
});
// Reset after microtask so Yjs observers fire first
queueMicrotask(() => { restoringRef.current = false; });
}
export type RouteError = "no_route" | "failed" | "rate_limit" | null;
export function useRouting(yjs: YjsState | null) {
const [isHost, setIsHost] = useState(false);
const [computing, setComputing] = useState(false);
const [routeError, setRouteError] = useState<RouteError>(null);
const [routeStats, setRouteStats] = useState<RouteStats>({});
const debounceTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
const lastGoodWaypointsRef = useRef<WaypointData[] | null>(null);
const restoringRef = useRef(false);
// Host election via Yjs awareness
useEffect(() => {
@ -56,6 +81,9 @@ export function useRouting(yjs: YjsState | null) {
points: (yMap.get("points") as Array<{ lat: number; lon: number }>) ?? [],
})).filter((a) => a.points.length >= 3);
// Save current waypoints so we can restore on failure
const snapshotBeforeCompute = getWaypointsFromYjs(yjs.waypoints);
setComputing(true);
try {
const response = await fetch("/api/route", {
@ -69,13 +97,22 @@ export function useRouting(yjs: YjsState | null) {
});
if (response.status === 429) {
console.warn("[Rate Limit] Route computation rate limit exceeded");
setRouteError("rate_limit");
restoreWaypoints(yjs, lastGoodWaypointsRef.current ?? snapshotBeforeCompute, restoringRef);
return;
}
if (!response.ok) {
const body = await response.json().catch(() => ({}));
const code = (body as { code?: string }).code;
setRouteError(code === "no_route" ? "no_route" : "failed");
restoreWaypoints(yjs, lastGoodWaypointsRef.current ?? snapshotBeforeCompute, restoringRef);
return;
}
if (!response.ok) return;
const enriched = await response.json();
setRouteError(null);
lastGoodWaypointsRef.current = snapshotBeforeCompute;
setRouteStats({
distance: enriched.totalLength || undefined,
elevationGain: enriched.totalAscend || undefined,
@ -91,7 +128,8 @@ export function useRouting(yjs: YjsState | null) {
}
});
} catch {
// Route computation failed
setRouteError("failed");
restoreWaypoints(yjs, lastGoodWaypointsRef.current ?? snapshotBeforeCompute, restoringRef);
} finally {
setComputing(false);
}
@ -101,7 +139,7 @@ export function useRouting(yjs: YjsState | null) {
const requestRoute = useCallback(
(waypoints: WaypointData[]) => {
if (!isHost) return;
if (!isHost || restoringRef.current) return;
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(() => computeRoute(waypoints), 500);
},
@ -139,5 +177,5 @@ export function useRouting(yjs: YjsState | null) {
};
}, [yjs, isHost, requestRoute]);
return { isHost, computing, routeStats, requestRoute };
return { isHost, computing, routeError, routeStats, requestRoute };
}