feat(directions): keyless A→B directions with turn-by-turn steps and FLY (#564)

Supersedes #219 by @spcpza, ported onto the current component layout with fixes from review.

Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.

Co-authored-by: spcpza <[email protected]>
This commit is contained in:
Bilawal Sidhuandspcpza authored and GitHub committed 2026-09-14 20:09:41 -05:00
1 parent 9fc8d1966b
commit d78696dc17
29 files changed
+4658 -65

No files matched your search

+256
View File
@@ -4,6 +4,14 @@ import {
googlePlacesContextProxy,
installRouteMiddleware,
} from '../../server/providers/places.js';
import {
ROUTE_UPSTREAM_MIN_INTERVAL_MS,
ROUTE_UPSTREAM_QUEUE_MAX,
_resetRouteUpstreamForTest,
_routeInflightCountForTest,
_setRouteUpstreamIntervalForTest,
} from '../../server/providers/places/routes.js';
import { ROUTE_STEPS_MAX } from '../../src/data/routeSteps.js';
import {
projectNearbyPlaces,
projectTextSearchPlaces,
@@ -226,3 +234,251 @@ test('OSRM routing preserves aliases, cache, span guards and upstream failure be
);
assert.equal((await request(route, query)).body.error, 'no route found');
});
/** One OSRM route with `count` maneuvers, for the step tests. */
function osrmRouteWithSteps(count) {
return {
code: 'Ok',
routes: [
{
distance: 123.6,
duration: 80.2,
geometry: {
coordinates: [
[-97, 30],
[-97.01, 30.01],
],
},
legs: [
{
steps: Array.from({ length: count }, (_, i) => ({
name: `Road ${i}`,
distance: 10,
duration: 5,
maneuver: {
type: i === 0 ? 'depart' : 'turn',
modifier: 'left',
location: [-97 + i / 1000, 30],
},
})),
},
],
},
],
};
}
test('steps are opt-in: the upstream is only asked for them when a caller is', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
const asked = [];
t.mock.method(globalThis, 'fetch', async (url) => {
asked.push(String(url));
return Response.json(osrmRouteWithSteps(3));
});
const route = '/api/route';
const query = '?profile=car&coords=-97,30;-97.01,30.01';
// The voice route annotation and fly_route do not read maneuvers. What they
// get back must be exactly what this endpoint returned before steps existed.
const plain = await request(route, query);
assert.deepEqual(plain.body, {
ok: true,
profile: 'car',
distanceM: 124,
durationS: 80,
geometry: [
[-97, 30],
[-97.01, 30.01],
],
});
assert.equal('steps' in plain.body, false, 'no steps key at all');
assert.match(asked[0], /steps=false/, 'the upstream payload stays small');
// Directions asks, and gets them.
_resetRouteUpstreamForTest();
const withSteps = await request(route, `${query}&steps=1`);
assert.equal(asked.length, 2);
assert.match(asked[1], /steps=true/);
assert.equal(withSteps.body.steps.length, 3);
assert.equal(withSteps.body.steps[0].instruction, 'Head out on Road 0');
assert.equal(withSteps.body.steps[1].instruction, 'Turn left onto Road 1');
// A stepful cache entry still serves a stepless caller its own shape.
const reuse = await request(route, query);
assert.equal(asked.length, 2, 'served from cache');
assert.deepEqual(
reuse.body,
plain.body,
'byte-identical to the no-steps shape',
);
});
test('identical concurrent route requests make one upstream call', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
let calls = 0;
let release;
const gate = new Promise((resolve) => {
release = resolve;
});
t.mock.method(globalThis, 'fetch', async () => {
calls += 1;
await gate;
return Response.json(osrmRouteWithSteps(4));
});
const route = '/api/route';
const query = '?profile=car&coords=-97,30;-97.01,30.01&steps=1';
// Three rapid reroutes of the same A→B, before any of them answers.
const all = [
request(route, query),
request(route, query),
request(route, query),
];
// ...plus a caller that wants no steps; it rides the same call.
all.push(request(route, '?profile=car&coords=-97,30;-97.01,30.01'));
await Promise.resolve();
assert.equal(
_routeInflightCountForTest(),
1,
'one upstream call is in flight',
);
release();
const results = await Promise.all(all);
assert.equal(calls, 1, 'the FOSSGIS servers saw one request, not four');
assert.equal(results[0].body.steps.length, 4);
assert.deepEqual(results[0].body, results[1].body);
assert.deepEqual(results[0].body, results[2].body);
assert.equal(
'steps' in results[3].body,
false,
'the stepless caller still gets no steps',
);
assert.equal(
_routeInflightCountForTest(),
0,
'the in-flight entry is released',
);
});
test('a pathological maneuver count is bounded before it reaches the browser', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
t.mock.method(globalThis, 'fetch', async () =>
Response.json(osrmRouteWithSteps(5000)),
);
const result = await request(
'/api/route',
'?profile=car&coords=-97,30;-97.01,30.01&steps=1',
);
assert.equal(result.body.steps.length, ROUTE_STEPS_MAX);
assert.ok(ROUTE_STEPS_MAX <= 200, 'the cap stays a cap');
});
test('outbound route requests are spaced to the rate the routing service asks for', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
const sentAt = [];
t.mock.method(globalThis, 'fetch', async (url) => {
sentAt.push({ at: Date.now(), url: String(url) });
return Response.json(osrmRouteWithSteps(2));
});
// FOSSGIS publish "one request per second max". The per-client limiter alone
// cannot honour that: a DRIVE and a WALK of the same trip are two different
// cache keys and used to leave 48 ms apart.
assert.ok(ROUTE_UPSTREAM_MIN_INTERVAL_MS >= 1000);
const first = await request(
'/api/route',
'?profile=car&coords=-97,30;-97.01,30.01',
);
assert.equal(first.body.ok, true);
assert.equal(sentAt.length, 1);
const second = request(
'/api/route',
'?profile=foot&coords=-97,30;-97.01,30.01',
);
await new Promise((resolve) => setTimeout(resolve, 120));
assert.equal(sentAt.length, 1, 'the second profile waits for its slot');
assert.equal((await second).body.ok, true);
assert.ok(
sentAt[1].at - sentAt[0].at >= ROUTE_UPSTREAM_MIN_INTERVAL_MS - 20,
`two outbound calls ${sentAt[1].at - sentAt[0].at} ms apart`,
);
// A cached route costs no outbound slot at all.
const cached = await request(
'/api/route',
'?profile=car&coords=-97,30;-97.01,30.01',
);
assert.equal(cached.body.ok, true);
assert.equal(sentAt.length, 2, 'the cache answers without an upstream call');
});
test('past the outbound queue depth the answer is an honest 429, not a growing queue', async (t) => {
_resetRouteUpstreamForTest();
// The real gate is one per second (asserted above); this test is about what
// happens past the queue depth, so it runs the gate fast.
_setRouteUpstreamIntervalForTest(5);
t.after(() => _resetRouteUpstreamForTest());
const request = install(installRouteMiddleware);
t.mock.method(globalThis, 'fetch', async () =>
Response.json(osrmRouteWithSteps(2)),
);
const pending = [];
for (let i = 0; i < ROUTE_UPSTREAM_QUEUE_MAX + 3; i += 1) {
pending.push(
request(
'/api/route',
`?profile=car&coords=-97,${30 + i / 100};-97.01,30.01`,
),
);
}
const results = await Promise.all(pending);
const refused = results.filter((result) => result.statusCode === 429);
assert.ok(refused.length >= 1, 'the excess is refused rather than queued');
assert.match(refused[0].body.error, /routing busy/);
assert.ok(
Number(refused[0].headers['retry-after']) >= 1,
`Retry-After is ${refused[0].headers['retry-after']}`,
);
// Everything that was admitted still got a real answer.
assert.ok(results.some((result) => result.body.ok === true));
});
test('an upstream rate limit is reported as one, not as "no route found"', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
t.mock.method(
globalThis,
'fetch',
async () =>
new Response('slow down', {
status: 429,
headers: { 'retry-after': '30', 'content-type': 'text/plain' },
}),
);
const result = await request(
'/api/route',
'?profile=car&coords=-97,30;-97.01,30.01',
);
assert.equal(result.statusCode, 429);
assert.match(result.body.error, /routing service is rate limited/);
assert.equal(
result.headers['retry-after'],
'30',
"the service's own wait is passed on",
);
});
test('the upstream host is pinned: a redirect is refused, not followed', async (t) => {
_resetRouteUpstreamForTest();
const request = install(installRouteMiddleware);
const seen = [];
t.mock.method(globalThis, 'fetch', async (url, options) => {
seen.push(options?.redirect);
return Response.json(osrmRouteWithSteps(2));
});
await request('/api/route', '?profile=car&coords=-97,30;-97.01,30.01');
assert.deepEqual(seen, ['error'], 'a redirect would escape the pinned host');
});