Fetch the enrichment pictures concurrently

The column showed "details could not be loaded" for every place. The endpoint
was answering correctly, just after ten seconds, and the shared axios instance
gives up at eight.

The downloads sat in a loop: five Commons hits meant five round trips and five
decodes in sequence, even though the slot limiter already allows five at once.
Both provider listings now run together and the bytes are fetched concurrently,
which brings a warm request back to about a second.

Candidate keys move with it. They now number the provider's list rather than the
surviving strip, so a picture that fails to download leaves a gap instead of
renumbering the ones behind it - a key has to point at the same file on the next
request or the cached result goes stale against the photo cache.

The client asks for enrichment with a longer timeout, and the Wikimedia calls
carry one of their own: a cold TLS handshake from a fresh container was measured
at eight seconds on its own, and no pictures is a better answer than a request
that never comes back.
This commit is contained in:
Maurice
2026-08-09 23:01:51 +02:00
committed by Maurice
parent b36103d2a9
commit 1f1ed630fc
4 changed files with 113 additions and 45 deletions
@@ -219,9 +219,11 @@ describe('collectPhotos', () => {
const out = await make(maps, cacheStub()).enrich(1, REQ);
// The survivor takes index 0 — keys number the strip, not the source list.
// The survivor keeps index 1 — keys number the provider's list, so a picture
// that fails leaves a gap instead of renumbering the ones behind it. Keys
// have to point at the same file on the next request.
expect(out.photos).toHaveLength(1);
expect(out.photos[0].key).toBe('ChIJmuseum~p0');
expect(out.photos[0].key).toBe('ChIJmuseum~p1');
});
it('ENRICH-010: treats an empty body and a throwing download as a miss', async () => {
@@ -274,7 +276,51 @@ describe('collectPhotos', () => {
const out = await make(maps, cacheStub()).enrich(1, REQ);
expect(out.photos).toHaveLength(1);
expect(out.photos[0]).toMatchObject({ key: 'ChIJmuseum~p0', attribution: 'Cara' });
expect(out.photos[0]).toMatchObject({ key: 'ChIJmuseum~p1', attribution: 'Cara' });
});
it('ENRICH-012c: downloads the pictures concurrently, not one after another', async () => {
// Five sequential round trips plus five decodes pushed the request past the
// client's timeout, which is what made the column show an error every time.
let active = 0;
let peak = 0;
const maps = mapsStub({
fetchCommonsCandidates: vi.fn(async () => [commonsCandidate(), commonsCandidate(), commonsCandidate()]),
});
mockSafeFetchFollow.mockImplementation(async () => {
active++;
peak = Math.max(peak, active);
await new Promise((resolve) => setTimeout(resolve, 5));
active--;
return { ok: true, arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer };
});
const out = await make(maps, cacheStub()).enrich(1, REQ);
expect(out.photos).toHaveLength(3);
expect(peak).toBeGreaterThan(1);
});
it('ENRICH-012d: asks both providers at the same time', async () => {
const order: string[] = [];
const maps = mapsStub({
getMapsKey: vi.fn(() => 'key'),
fetchGooglePhotoRefs: vi.fn(async () => {
order.push('google:start');
await new Promise((resolve) => setTimeout(resolve, 10));
order.push('google:end');
return [];
}),
fetchCommonsCandidates: vi.fn(async () => {
order.push('commons:start');
return [];
}),
});
await make(maps, cacheStub()).enrich(1, REQ);
// Commons starts before Google finishes; the listings do not queue.
expect(order.indexOf('commons:start')).toBeLessThan(order.indexOf('google:end'));
});
it('ENRICH-013: falls back to a coordinate pseudo-id when the place has none', async () => {