trails/apps/planner/app/lib/segment-cache.test.ts
Ullrich Schäfer c0c3ce98d0 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>
2026-04-24 17:57:54 +02:00

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);
});
});