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
53
apps/planner/app/lib/segment-cache.ts
Normal file
53
apps/planner/app/lib/segment-cache.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Host-local LRU cache of BRouter segment responses, keyed by
|
||||
// (from, to, profile, noGoHash). Shrinks BRouter load on rapid edits:
|
||||
// moving one waypoint only invalidates the two adjacent pair keys, so we
|
||||
// fetch 2 segments instead of N-1 on every recompute.
|
||||
//
|
||||
// Lives only in the elected Yjs host's tab. When the host changes, the
|
||||
// new host starts cold — acceptable because host handover is rare and
|
||||
// the penalty is one full recompute.
|
||||
|
||||
const DEFAULT_MAX = 500;
|
||||
|
||||
export class SegmentCache {
|
||||
// Map preserves insertion order, which we use for an LRU eviction
|
||||
// policy: get() re-inserts the entry to move it to the tail; set()
|
||||
// evicts from the head when over `max`.
|
||||
private entries = new Map<string, Record<string, unknown>>();
|
||||
private readonly max: number;
|
||||
|
||||
constructor(max: number = DEFAULT_MAX) {
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
get(key: string): Record<string, unknown> | undefined {
|
||||
const value = this.entries.get(key);
|
||||
if (value === undefined) return undefined;
|
||||
// LRU bump
|
||||
this.entries.delete(key);
|
||||
this.entries.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key: string, value: Record<string, unknown>): void {
|
||||
if (this.entries.has(key)) this.entries.delete(key);
|
||||
this.entries.set(key, value);
|
||||
while (this.entries.size > this.max) {
|
||||
const oldest = this.entries.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
this.entries.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.entries.has(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.entries.size;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue