mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
9fc8d1966b
commit
d78696dc17
29 files changed
+4658
-65
No files matched your search
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user