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>
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { hashNoGoAreas, pairKey } from "./route-merge";
|
|
|
|
describe("pairKey", () => {
|
|
it("is stable for the same inputs", () => {
|
|
const a = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "trekking", "abc");
|
|
const b = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "trekking", "abc");
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it("changes when the profile changes", () => {
|
|
const a = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "trekking", "");
|
|
const b = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "fastbike", "");
|
|
expect(a).not.toBe(b);
|
|
});
|
|
|
|
it("changes when either endpoint moves", () => {
|
|
const base = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "p", "");
|
|
const moveFrom = pairKey({ lat: 1.0001, lon: 2 }, { lat: 3, lon: 4 }, "p", "");
|
|
const moveTo = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4.0001 }, "p", "");
|
|
expect(base).not.toBe(moveFrom);
|
|
expect(base).not.toBe(moveTo);
|
|
});
|
|
|
|
it("is direction-sensitive (A→B and B→A are distinct)", () => {
|
|
const forward = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "p", "");
|
|
const reverse = pairKey({ lat: 3, lon: 4 }, { lat: 1, lon: 2 }, "p", "");
|
|
expect(forward).not.toBe(reverse);
|
|
});
|
|
|
|
it("changes when the no-go hash changes", () => {
|
|
const a = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "p", "h1");
|
|
const b = pairKey({ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, "p", "h2");
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|
|
|
|
describe("hashNoGoAreas", () => {
|
|
it("returns empty string for no areas", () => {
|
|
expect(hashNoGoAreas()).toBe("");
|
|
expect(hashNoGoAreas([])).toBe("");
|
|
});
|
|
|
|
it("is stable for equal inputs", () => {
|
|
const areas = [
|
|
{ points: [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 5, lon: 6 }] },
|
|
];
|
|
expect(hashNoGoAreas(areas)).toBe(hashNoGoAreas(areas));
|
|
});
|
|
|
|
it("differs when a polygon changes", () => {
|
|
const a = hashNoGoAreas([
|
|
{ points: [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 5, lon: 6 }] },
|
|
]);
|
|
const b = hashNoGoAreas([
|
|
{ points: [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 5, lon: 7 }] },
|
|
]);
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|