mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(dawarich): three things a tester on dev1 ran into
The Atlas country check timed out. It asked Dawarich for a year of visited cities in one request, and Dawarich computes that over every point in the window with nothing cached, so a dense recorder never got under the 15 s the server allows. The year is now asked for a month at a time, one request after another, and folded back together. The windows sit a second apart because Dawarich reads the bounds as inclusive. The recorded route only appeared after a reload. Every Dawarich call from the browser carried the shared 8 s timeout, so the browser gave up while the server carried on and cached the answer for the next page load. Calls that make the server talk to Dawarich now wait two minutes, the Atlas four. On the GL renderers the draw also gave up whenever isStyleLoaded() was false, which it is while any tile is still loading, and only retried on style.load. It now waits for idle. The hazard overlay had the same guard and gets the same wait. The pill shows a ring while the route loads, so a slow first fetch no longer looks like "done, nothing there". A collapsed day now takes its recorded route off the map, following the same folds the planner already applies to places, the road trip sidebar's included.
This commit is contained in:
@@ -574,6 +574,66 @@ describe('DawarichClient — listVisitedCities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DawarichClient, visited cities over a year', () => {
|
||||
const YEAR_FROM = new Date('2025-09-14T20:00:00.000Z');
|
||||
const YEAR_TO = new Date('2026-09-14T20:00:00.000Z');
|
||||
|
||||
it('DAWARICH-CLIENT-082: a year is asked for a month at a time, one request after another, and folded back', async () => {
|
||||
// Dawarich computes this endpoint over every point in the window. A year in
|
||||
// one request is what timed out on a tester's instance, so the client has to
|
||||
// split it, and has to do so without firing thirteen computations at once.
|
||||
let inFlight = 0;
|
||||
let mostAtOnce = 0;
|
||||
let served = 0;
|
||||
safeFetchMock.mockImplementation(async (url: string, init: RequestInit) => {
|
||||
calls.push({ url, init });
|
||||
inFlight++;
|
||||
mostAtOnce = Math.max(mostAtOnce, inFlight);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
inFlight--;
|
||||
served++;
|
||||
const body =
|
||||
served === 1
|
||||
? { data: [{ country: 'Germany', cities: [{ city: 'Rostock', stayed_for: 90 }] }] }
|
||||
: served === 7
|
||||
? { data: [{ country: 'Germany', cities: [{ city: 'Rostock', stayed_for: 30 }] }, { country: 'Poland', cities: [] }] }
|
||||
: { data: [] };
|
||||
return reply({ body }) as unknown as Response;
|
||||
});
|
||||
|
||||
const out = await client.listVisitedCities(CREDS, YEAR_FROM, YEAR_TO);
|
||||
|
||||
expect(calls).toHaveLength(13);
|
||||
expect(mostAtOnce).toBe(1);
|
||||
expect(requestedUrl(0).searchParams.get('start_at')).toBe(unixSeconds(YEAR_FROM));
|
||||
expect(requestedUrl(12).searchParams.get('end_at')).toBe(unixSeconds(YEAR_TO));
|
||||
for (let i = 1; i < calls.length; i++) {
|
||||
const previousEnd = Number(requestedUrl(i - 1).searchParams.get('end_at'));
|
||||
expect(Number(requestedUrl(i).searchParams.get('start_at'))).toBe(previousEnd + 1);
|
||||
}
|
||||
expect(out).toEqual([
|
||||
{ country: 'Germany', cities: [{ city: 'Rostock', stayed_for: 120 }] },
|
||||
{ country: 'Poland', cities: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DAWARICH-CLIENT-083: one month failing fails the whole year instead of passing off eleven as twelve', async () => {
|
||||
let served = 0;
|
||||
safeFetchMock.mockImplementation(async (url: string, init: RequestInit) => {
|
||||
calls.push({ url, init });
|
||||
served++;
|
||||
if (served === 4) throw new Error('The operation was aborted due to timeout');
|
||||
return reply({ body: { data: [] } }) as unknown as Response;
|
||||
});
|
||||
|
||||
const err = await failure(() => client.listVisitedCities(CREDS, YEAR_FROM, YEAR_TO));
|
||||
expect(err.code).toBe('unreachable');
|
||||
// Stops at the failure rather than spending the remaining nine on a result
|
||||
// that is already going to be refused.
|
||||
expect(calls).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DawarichClient — findVisitsNear', () => {
|
||||
it('DAWARICH-CLIENT-090: pins the radius explicitly instead of letting Dawarich guess 500 m', async () => {
|
||||
const visits = [{ timestamp: 1788000000, distance_meters: 40, points_count: 12 }];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The pure half of the Dawarich integration — DAWARICH-TIME-001 through
|
||||
* DAWARICH-COUNT-003. Everything under test is a free function over plain
|
||||
* DAWARICH-CITIES-006. Everything under test is a free function over plain
|
||||
* fixtures: no DB, no container, and no client. `dawarich.client` appears here
|
||||
* only as a type-only import (erased at compile time), so nothing in this file
|
||||
* can reach the network even by accident — there is no DawarichClient instance
|
||||
@@ -19,10 +19,12 @@ import {
|
||||
distanceMeters,
|
||||
geoJsonToLatLng,
|
||||
localDateOf,
|
||||
mergeVisitedCountries,
|
||||
minutesBetween,
|
||||
normalizeVisit,
|
||||
offsetMinutesOf,
|
||||
simplify,
|
||||
splitWindow,
|
||||
syncWindow,
|
||||
toNumber,
|
||||
visitHash,
|
||||
@@ -927,3 +929,85 @@ describe('countPoints', () => {
|
||||
expect(countPoints(days)).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visited cities over a long range', () => {
|
||||
const DAY = 86_400_000;
|
||||
|
||||
it('DAWARICH-CITIES-001: a range within one window stays one request, bounds untouched', () => {
|
||||
const from = new Date('2026-09-01T00:00:00Z');
|
||||
const to = new Date('2026-09-02T00:00:00Z');
|
||||
expect(splitWindow(from, to, 30)).toEqual([{ from, to }]);
|
||||
});
|
||||
|
||||
it('DAWARICH-CITIES-002: a year becomes months that cover it without a gap or an overlap', () => {
|
||||
// Dawarich reads start_at..end_at as inclusive on both ends, so touching
|
||||
// bounds would hand the point on the seam to two windows.
|
||||
const from = new Date('2025-09-14T20:00:00Z');
|
||||
const to = new Date(from.getTime() + 365 * DAY);
|
||||
const windows = splitWindow(from, to, 30);
|
||||
|
||||
expect(windows).toHaveLength(13);
|
||||
expect(windows[0].from).toEqual(from);
|
||||
expect(windows[windows.length - 1].to).toEqual(to);
|
||||
for (let i = 1; i < windows.length; i++) {
|
||||
expect(windows[i].from.getTime() - windows[i - 1].to.getTime()).toBe(1000);
|
||||
}
|
||||
for (const window of windows) {
|
||||
expect(window.to.getTime() - window.from.getTime()).toBeLessThanOrEqual(30 * DAY);
|
||||
}
|
||||
});
|
||||
|
||||
it('DAWARICH-CITIES-003: bounds are whole seconds, the unit that goes on the wire', () => {
|
||||
const windows = splitWindow(new Date('2026-01-01T00:00:00.750Z'), new Date('2026-03-01T00:00:00.250Z'), 30);
|
||||
for (const window of windows) {
|
||||
expect(window.from.getMilliseconds()).toBe(0);
|
||||
expect(window.to.getMilliseconds()).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('DAWARICH-CITIES-004: a range that ends before it starts asks nothing', () => {
|
||||
expect(splitWindow(new Date('2026-09-02T00:00:00Z'), new Date('2026-09-01T00:00:00Z'), 30)).toEqual([]);
|
||||
});
|
||||
|
||||
it('DAWARICH-CITIES-005: a city seen in two months adds up its time and keeps the later visit', () => {
|
||||
const merged = mergeVisitedCountries([
|
||||
[{ country: 'Germany', cities: [{ city: 'Rostock', points: 10, stayed_for: 90, timestamp: 1_700_000_000 }] }],
|
||||
[
|
||||
{ country: 'Germany', cities: [{ city: 'Rostock', points: 5, stayed_for: 60, timestamp: 1_702_000_000 }, { city: 'Berlin', stayed_for: 120 }] },
|
||||
{ country: 'Poland', cities: [{ city: 'Szczecin', stayed_for: 75, timestamp: 1_701_000_000 }] },
|
||||
],
|
||||
]);
|
||||
|
||||
expect(merged).toEqual([
|
||||
{
|
||||
country: 'Germany',
|
||||
cities: [
|
||||
{ city: 'Rostock', points: 15, stayed_for: 150, timestamp: 1_702_000_000 },
|
||||
{ city: 'Berlin', stayed_for: 120 },
|
||||
],
|
||||
},
|
||||
{ country: 'Poland', cities: [{ city: 'Szczecin', stayed_for: 75, timestamp: 1_701_000_000 }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('DAWARICH-CITIES-006: an unvalidated payload loses its unnamed entries, not its countries', () => {
|
||||
const merged = mergeVisitedCountries([
|
||||
[
|
||||
null as never,
|
||||
{ country: 42 as never, cities: [] },
|
||||
{ country: 'Denmark', cities: null as never },
|
||||
],
|
||||
[{ country: 'Denmark', cities: [null as never, {} as never, { city: 'Gedser', points: '7' as never }] }],
|
||||
]);
|
||||
|
||||
// A count that arrived as a string is still a count; a timestamp nobody sent
|
||||
// stays absent instead of turning into -Infinity.
|
||||
expect(merged).toEqual([{ country: 'Denmark', cities: [{ city: 'Gedser', points: '7' }] }]);
|
||||
|
||||
const again = mergeVisitedCountries([
|
||||
[{ country: 'Denmark', cities: [{ city: 'Gedser', points: '7' as never }] }],
|
||||
[{ country: 'Denmark', cities: [{ city: 'Gedser', points: 3 }] }],
|
||||
]);
|
||||
expect(again[0].cities[0]).toEqual({ city: 'Gedser', points: 10, stayed_for: undefined, timestamp: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user