fix(tours): wait for the public Valhalla's turn before every request

The public instance allows about one request a second per client, and the
Tours elevation questions went out right behind the route request with no
pause, so they could be refused with a 429 and leave the tour without the
elevation it needs to be saved. Every request to that host now waits for
its turn, across callers; an operator's own Valhalla is not paced.
This commit is contained in:
Maurice
2026-10-07 21:39:48 +02:00
parent e0efa0314a
commit e5ceba00f3
4 changed files with 118 additions and 4 deletions
@@ -1,13 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { valhallaBase, valhallaRun } = vi.hoisted(() => ({
const { valhallaBase, valhallaRun, valhallaTurn } = vi.hoisted(() => ({
valhallaBase: vi.fn(),
valhallaRun: vi.fn(),
valhallaTurn: vi.fn(async () => {}),
}))
vi.mock('../../Map/valhallaRoute', () => ({
valhallaBase,
valhallaRun,
valhallaTurn,
}))
import { enrichTourElevations, parseValhallaElevation, routeWalkingTour } from './tourRouting'
@@ -67,6 +69,33 @@ describe('routeWalkingTour', () => {
])
})
it('waits for its turn at the host before every height question', async () => {
const order: string[] = []
valhallaTurn.mockImplementation(async () => { order.push('turn') })
vi.stubGlobal('fetch', vi.fn(async (_url: string, init: { body: string }) => {
order.push('fetch')
const shape = (JSON.parse(init.body) as { shape: unknown[] }).shape
return { ok: true, json: async () => ({ range_height: shape.map((_, i) => [i, 100]) }) }
}))
const coordinates = Array.from({ length: 600 }, (_, i) => [48 + i / 1000, 11] as [number, number])
const result = await enrichTourElevations(coordinates)
expect(order).toEqual(['turn', 'fetch', 'turn', 'fetch'])
expect(valhallaTurn).toHaveBeenCalledWith('https://valhalla.test', undefined)
expect(result!.every(point => point.length === 3)).toBe(true)
})
it('asks nothing once the caller gave up while waiting for its turn', async () => {
const controller = new AbortController()
valhallaTurn.mockImplementation(async () => { controller.abort() })
const fetchSpy = vi.fn()
vi.stubGlobal('fetch', fetchSpy)
await expect(enrichTourElevations([[48, 11], [48.01, 11.01]], controller.signal)).resolves.toBeNull()
expect(fetchSpy).not.toHaveBeenCalled()
})
it('keeps the full route as 2D geometry when height enrichment is unavailable', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, json: async () => ({}) }))
const coordinates: [number, number][] = [[48, 11], [48.01, 11.01]]
@@ -1,5 +1,5 @@
import type { Waypoint } from '../../../types'
import { valhallaBase, valhallaRun } from '../../Map/valhallaRoute'
import { valhallaBase, valhallaRun, valhallaTurn } from '../../Map/valhallaRoute'
export interface TourRouteResult {
coordinates: [number, number][]
@@ -68,6 +68,11 @@ export async function enrichTourElevations(
const chunk = coordinates.slice(offset, offset + HEIGHT_CHUNK_SIZE)
if (chunk.length === 1 && offset > 0) break
try {
// The route came from the same host a moment ago, and the public one allows a
// request a second: without the wait the first height question is refused and
// the tour cannot be saved for want of elevation.
await valhallaTurn(base, signal)
if (signal?.aborted) return null
const response = await fetch(`${base}/height`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Client-Id': 'trek' },