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>
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { SegmentCache } from "./segment-cache";
|
|
|
|
describe("SegmentCache", () => {
|
|
it("returns undefined for missing keys", () => {
|
|
const cache = new SegmentCache();
|
|
expect(cache.get("missing")).toBeUndefined();
|
|
expect(cache.has("missing")).toBe(false);
|
|
});
|
|
|
|
it("stores and retrieves segments", () => {
|
|
const cache = new SegmentCache();
|
|
const segment = { type: "FeatureCollection" };
|
|
cache.set("k1", segment);
|
|
expect(cache.has("k1")).toBe(true);
|
|
expect(cache.get("k1")).toBe(segment);
|
|
expect(cache.size).toBe(1);
|
|
});
|
|
|
|
it("overwrites existing keys without changing size", () => {
|
|
const cache = new SegmentCache();
|
|
cache.set("k1", { v: 1 });
|
|
cache.set("k1", { v: 2 });
|
|
expect(cache.size).toBe(1);
|
|
expect(cache.get("k1")).toEqual({ v: 2 });
|
|
});
|
|
|
|
it("evicts the oldest entry when over capacity", () => {
|
|
const cache = new SegmentCache(3);
|
|
cache.set("a", {});
|
|
cache.set("b", {});
|
|
cache.set("c", {});
|
|
cache.set("d", {});
|
|
expect(cache.has("a")).toBe(false);
|
|
expect(cache.has("b")).toBe(true);
|
|
expect(cache.has("c")).toBe(true);
|
|
expect(cache.has("d")).toBe(true);
|
|
expect(cache.size).toBe(3);
|
|
});
|
|
|
|
it("bumps an entry to most-recent on get, delaying its eviction", () => {
|
|
const cache = new SegmentCache(3);
|
|
cache.set("a", {});
|
|
cache.set("b", {});
|
|
cache.set("c", {});
|
|
// Touch "a" so it's most-recent; the next insert should evict "b"
|
|
cache.get("a");
|
|
cache.set("d", {});
|
|
expect(cache.has("a")).toBe(true);
|
|
expect(cache.has("b")).toBe(false);
|
|
expect(cache.has("c")).toBe(true);
|
|
expect(cache.has("d")).toBe(true);
|
|
});
|
|
|
|
it("clear() removes all entries", () => {
|
|
const cache = new SegmentCache();
|
|
cache.set("a", {});
|
|
cache.set("b", {});
|
|
cache.clear();
|
|
expect(cache.size).toBe(0);
|
|
expect(cache.has("a")).toBe(false);
|
|
});
|
|
});
|