Cache BRouter segments client-side, fetch only the diff
Moving one waypoint previously re-fetched all N-1 segments of the route
through /api/route every time, because the server-side computeRoute
recomputes the whole route from scratch. Under rapid edits this piles up
on BRouter's thread pool and wastes BRouter+Planner cycles on segments
the caller already has.
This change keeps a host-local LRU segment cache in the Planner tab,
keyed by (from, to, profile, noGoHash). On each computeRoute:
- build the pair list from the current waypoints
- split into cached vs. missing by looking up each pair key
- if any missing, POST { pairs: missing, … } to the new
/api/route-segments endpoint and populate the cache with its ordered
raw BRouter responses
- merge cache-ordered segments into an EnrichedRoute client-side via
mergeGeoJsonSegments (moved to a pure, isomorphic route-merge.ts)
- write to yjs.routeData exactly as before
Typical drag of one waypoint in a 10-waypoint route drops from 9 BRouter
fetches to 2. A session re-entering a cached arrangement (undo/redo)
short-circuits the server entirely. Profile/no-go changes invalidate all
keys and trigger a full refetch, matching prior behavior.
Yjs stays out of the cache: segments are bulky and CRDT sync of large
blobs hurts more than it helps. The elected host owns the cache; on host
handover the new host pays one full recompute.
Backwards-compat: /api/route is unchanged for external callers (e2e
integration tests, the Journal demo-bot's format=gpx path). Server-side
it now delegates to the shared fetchSegments helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d3c45e929
commit
c0c3ce98d0
8 changed files with 574 additions and 248 deletions
59
apps/planner/app/routes/api.route-segments.ts
Normal file
59
apps/planner/app/routes/api.route-segments.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { data } from "react-router";
|
||||
import type { Route } from "./+types/api.route-segments";
|
||||
import { fetchSegments, BRouterError } from "~/lib/brouter";
|
||||
import { checkRateLimit } from "~/lib/rate-limit";
|
||||
import { requireSession } from "~/lib/require-session";
|
||||
|
||||
// Client-side segment cache endpoint: the browser sends only the
|
||||
// waypoint pairs it doesn't already have cached; the server fetches those
|
||||
// from BRouter in parallel and returns the raw responses in order. The
|
||||
// merge into an EnrichedRoute happens in the browser so unchanged pairs
|
||||
// never cross the network.
|
||||
//
|
||||
// Session-binding + per-session rate limiting mirror /api/route: the
|
||||
// Planner is not an anonymous BRouter proxy.
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
if (request.method !== "POST") {
|
||||
return data({ error: "Method not allowed" }, { status: 405 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { pairs, profile, sessionId, noGoAreas } = body as {
|
||||
pairs: Array<{ from: { lat: number; lon: number }; to: { lat: number; lon: number } }>;
|
||||
profile?: string;
|
||||
sessionId?: string;
|
||||
noGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(pairs) || pairs.length === 0) {
|
||||
return data({ error: "At least 1 pair is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const session = await requireSession(sessionId);
|
||||
if (session instanceof Response) return session;
|
||||
|
||||
const limit = checkRateLimit(`route:${session.id}`);
|
||||
if (!limit.allowed) {
|
||||
return data(
|
||||
{ error: "Rate limit exceeded" },
|
||||
{
|
||||
status: 429,
|
||||
headers: { "Retry-After": String(limit.retryAfterSeconds) },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const segments = await fetchSegments({ pairs, profile, noGoAreas });
|
||||
return data(
|
||||
{ segments },
|
||||
{ headers: { "X-RateLimit-Remaining": String(limit.remaining) } },
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof BRouterError && e.statusCode >= 400 && e.statusCode < 500) {
|
||||
return data({ error: e.message, code: "no_route" }, { status: 422 });
|
||||
}
|
||||
const message = e instanceof Error ? e.message : "Route computation failed";
|
||||
return data({ error: message, code: "server_error" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue