mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(traffic): the traffic layer reads STALE, not LIVE, on cached flow tiles
Once the TomTom tile budget is spent, or TomTom fails, the proxy serves cached tiles marked STALE-BUDGET / STALE-ERROR. flowSource works out `stale`, but the layer fetched flow through fetchFlowForBounds, which returns only the segments, so getStats() and the sync chip still read "LIVE · Roads: …". #1239 fixed the get_traffic_flow tool the same way; the layer now uses fetchFlowDetail too, reports stale and oldestTileAt in its stats, and the chip reads "STALE · Roads: <source> · Flow: cached TomTom tiles". A source without fetchFlowDetail keeps the previous behaviour. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2006c035d9
commit
7981a179ea
11 files changed
+120
-3
No files matched your search
@@ -5651,7 +5651,10 @@ easier to meet (detection is now on more often), but does not create it.
|
||||
coverage number. `stats.loading` covers outstanding flow work as well as the
|
||||
road fetch, so a flow failure landing after the first paint still ends the
|
||||
shared loading batch as LOAD FAILED. Harnesses must gate on `!stats.error`,
|
||||
never on `mode === 'live'` alone.
|
||||
never on `mode === 'live'` alone. When the drawn flow came from the proxy's
|
||||
cached STALE-BUDGET / STALE-ERROR tiles, `getStats()` reports `stale: true`
|
||||
and `oldestTileAt`, the row reads STALE, and the sync chip reads
|
||||
`STALE · Roads: <source> · Flow: cached TomTom tiles` instead of LIVE.
|
||||
- Traffic runs in `sim` mode (white dots, hardcoded speeds) unless `TOMTOM_API_KEY`
|
||||
is configured (env or Keychain `tomtom-api`/`api-key`), which enables `live` mode:
|
||||
TomTom flow vector tiles via the budget-governed `/api/tomtom` proxy
|
||||
|
||||
@@ -857,6 +857,7 @@
|
||||
"LIVE · Roads: {source} · No flow roads in view": "EN VIVO · Vías: {source} · Sin vías con flujo a la vista",
|
||||
"LIVE · Roads: {source} · Flow {percent}%": "EN VIVO · Vías: {source} · Flujo {percent}%",
|
||||
"SIMULATED · Roads: {source} · Flow {percent}%": "SIMULADO · Vías: {source} · Flujo {percent}%",
|
||||
"STALE · Roads: {source} · Flow: cached TomTom tiles": "DESACTUALIZADO · Vías: {source} · Flujo: teselas de TomTom en caché",
|
||||
"Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Sincronizando flujo · Vías: OpenStreetMap · Flujo: TomTom · Sin coincidencia: simulado",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "EN VIVO · Vías: OpenStreetMap · Flujo: TomTom · {percent}% cob. · Sin coincidencia: simulado",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "EN VIVO · Vías: OpenStreetMap · Flujo: TomTom · {percent}% cob.",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"LIVE · Roads: {source} · No flow roads in view": "ОНЛАЙН · Дороги: {source} · В кадре нет дорог с потоком",
|
||||
"LIVE · Roads: {source} · Flow {percent}%": "ОНЛАЙН · Дороги: {source} · Поток {percent}%",
|
||||
"SIMULATED · Roads: {source} · Flow {percent}%": "СИМУЛЯЦИЯ · Дороги: {source} · Поток {percent}%",
|
||||
"STALE · Roads: {source} · Flow: cached TomTom tiles": "УСТАРЕЛ · Дороги: {source} · Поток: тайлы TomTom из кэша",
|
||||
"Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Синхронизация потока · Дороги: OpenStreetMap · Поток: TomTom · Без совпадений: симуляция",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "ОНЛАЙН · Дороги: OpenStreetMap · Поток: TomTom · охват {percent}% · Без совпадений: симуляция",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "ОНЛАЙН · Дороги: OpenStreetMap · Поток: TomTom · охват {percent}%",
|
||||
|
||||
@@ -1059,6 +1059,7 @@
|
||||
"LIVE · Roads: {source} · No flow roads in view": "实时 · 道路:{source} · 视野内没有带路况数据的道路",
|
||||
"LIVE · Roads: {source} · Flow {percent}%": "实时 · 道路:{source} · 路况 {percent}%",
|
||||
"SIMULATED · Roads: {source} · Flow {percent}%": "模拟 · 道路:{source} · 路况 {percent}%",
|
||||
"STALE · Roads: {source} · Flow: cached TomTom tiles": "过期 · 道路:{source} · 路况:TomTom 缓存瓦片",
|
||||
"Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "正在同步路况 · 道路:OpenStreetMap · 路况:TomTom · 未匹配:模拟",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "实时 · 道路:OpenStreetMap · 路况:TomTom · 覆盖 {percent}% · 未匹配:模拟",
|
||||
"LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "实时 · 道路:OpenStreetMap · 路况:TomTom · 覆盖 {percent}%",
|
||||
|
||||
Vendored
+15
@@ -310,6 +310,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
liveMode: layerState._liveMode,
|
||||
fetching: loading,
|
||||
flowError: layerState._flowError,
|
||||
stale: layerState._flowStale,
|
||||
coveragePct: flowCoveragePct,
|
||||
statusUnavailable: layerState._flowStatusUnavailable,
|
||||
roadSource: layerState._roadSource,
|
||||
@@ -321,6 +322,20 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
lastUpdate: layerState._lastUpdate,
|
||||
loading,
|
||||
mode: feed.mode,
|
||||
// Cached proxy tiles (TomTom budget spent or upstream failing): the
|
||||
// manager reads this as STALE. Any error (flow, roads, detail) reads
|
||||
// DEGRADED instead.
|
||||
stale: Boolean(
|
||||
layerState._liveMode &&
|
||||
layerState._flowStale &&
|
||||
!(
|
||||
layerState._roadError ||
|
||||
layerState._roadWarning ||
|
||||
layerState._detailError ||
|
||||
feed.error
|
||||
),
|
||||
),
|
||||
oldestTileAt: layerState._flowOldestTileAt ?? null,
|
||||
error:
|
||||
layerState._roadError ||
|
||||
layerState._roadWarning ||
|
||||
|
||||
@@ -186,10 +186,19 @@ export function createFlow({ state: layerState, services, parts, source }) {
|
||||
return layerState._warmFlow;
|
||||
layerState._warmFlowGeneration = generation;
|
||||
const signal = layerState._activeFetchAbort?.signal;
|
||||
layerState._warmFlow = ensureFlowStatus(signal).then(() => {
|
||||
layerState._warmFlow = ensureFlowStatus(signal).then(async () => {
|
||||
if (!layerState._liveMode || generation !== layerState._loadGeneration)
|
||||
return [];
|
||||
return fetchFlowForBounds(clamped, { signal });
|
||||
if (typeof source.fetchFlowDetail !== 'function')
|
||||
return fetchFlowForBounds(clamped, { signal });
|
||||
// The proxy serves cached tiles once the TomTom budget is spent or the
|
||||
// upstream fails; carry that mark so getStats() reads STALE, not LIVE.
|
||||
const detail = await source.fetchFlowDetail(clamped, { signal });
|
||||
if (generation === layerState._loadGeneration) {
|
||||
layerState._flowStale = detail.stale === true;
|
||||
layerState._flowOldestTileAt = detail.oldestTileAt ?? null;
|
||||
}
|
||||
return detail.segments;
|
||||
});
|
||||
layerState._warmFlow.catch(() => {});
|
||||
return layerState._warmFlow;
|
||||
|
||||
@@ -42,6 +42,8 @@ export function createLifecycle({
|
||||
layerState._lastViewCenter = null;
|
||||
layerState._flowRoads = null;
|
||||
layerState._flowError = null;
|
||||
layerState._flowStale = false;
|
||||
layerState._flowOldestTileAt = null;
|
||||
if (TRAFFIC_TIMING_ENABLED) {
|
||||
layerState._trafficTimingCurrentAnchor = null;
|
||||
layerState._trafficTimingSequence = 0;
|
||||
@@ -159,6 +161,8 @@ export function createLifecycle({
|
||||
// A stale outage from the last session would misreport a fresh enable —
|
||||
// the next load re-derives feed health from real evidence.
|
||||
layerState._flowError = null;
|
||||
layerState._flowStale = false;
|
||||
layerState._flowOldestTileAt = null;
|
||||
|
||||
layerState._surfaceFrameRemover?.();
|
||||
layerState._surfaceFrameRemover = null;
|
||||
|
||||
@@ -229,6 +229,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
* @param {boolean} [input.liveMode] - `/api/tomtom/status` reported a key.
|
||||
* @param {boolean} [input.fetching] - A viewport load is in flight.
|
||||
* @param {string|null} [input.flowError] - `deriveTrafficFlowError` result, if any.
|
||||
* @param {boolean} [input.stale] - The flow came from cached proxy tiles.
|
||||
* @param {number} [input.coveragePct] - Share of shown dots on matched roads, 0–100.
|
||||
* @param {boolean} [input.statusUnavailable] - The status probe itself failed.
|
||||
* @param {string} [input.roadSource] - Name of the geometry being drawn.
|
||||
@@ -239,6 +240,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
liveMode = false,
|
||||
fetching = false,
|
||||
flowError = null,
|
||||
stale = false,
|
||||
coveragePct = 0,
|
||||
statusUnavailable = false,
|
||||
roadSource = 'OpenStreetMap',
|
||||
@@ -258,6 +260,16 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
});
|
||||
return { mode, error: degraded, loadingLabel: degraded };
|
||||
}
|
||||
// Cached proxy tiles (TomTom budget spent or upstream failing) are not live.
|
||||
if (liveMode && stale && !fetching) {
|
||||
return {
|
||||
mode,
|
||||
error: null,
|
||||
loadingLabel: t('STALE · Roads: {source} · Flow: cached TomTom tiles', {
|
||||
source: roadSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
// TomTom and Hybrid name their geometry; OpenStreetMap keeps its match copy.
|
||||
if (liveMode && roadSource === 'TomTom') {
|
||||
return {
|
||||
|
||||
@@ -283,6 +283,64 @@ test('flow application preserves direct values and leaves Hybrid fill simulated'
|
||||
assert.equal(state._flowPending, 0);
|
||||
});
|
||||
|
||||
test('stale proxy flow tiles reach the layer stats and never read LIVE', async () => {
|
||||
const { createFlow } = await import('./flow.js');
|
||||
const { createModel } = await import('./model.js');
|
||||
const { createControls } = await import('./controls.js');
|
||||
const { createState } = await import('./state.js');
|
||||
const { layerFeedState } = await import('../../data/feedState.js');
|
||||
let detail = {
|
||||
segments: [flow],
|
||||
partial: false,
|
||||
stale: true,
|
||||
oldestTileAt: 1000,
|
||||
};
|
||||
const source = {
|
||||
fetchFlowDetail: async () => detail,
|
||||
fetchFlowForBounds: async () => detail.segments,
|
||||
getFlowSessionStats: () => ({ tilesFetched: 1 }),
|
||||
};
|
||||
const state = createState({ services: {} });
|
||||
Object.assign(state, {
|
||||
_enabled: true,
|
||||
_liveMode: true,
|
||||
_loadGeneration: 1,
|
||||
_flowStatusPromise: Promise.resolve(),
|
||||
_bucketCounts: { free: 3, slow: 0, jam: 0, sim: 1 },
|
||||
});
|
||||
const parts = {};
|
||||
const context = { state, services: { credits: {} }, parts, source };
|
||||
parts.model = createModel(context);
|
||||
parts.flow = createFlow(context);
|
||||
const controls = createControls(context);
|
||||
|
||||
await parts.flow.applyFlowToRoads([{ coords: coordinates }], box, 1);
|
||||
const stale = controls.methods.getStats();
|
||||
assert.equal(stale.mode, 'live');
|
||||
assert.equal(stale.stale, true);
|
||||
assert.equal(stale.oldestTileAt, 1000);
|
||||
assert.doesNotMatch(stale.loadingLabel, /LIVE/);
|
||||
assert.equal(layerFeedState(stale), 'stale');
|
||||
|
||||
// A road failure outranks cached flow: the row reads the failure.
|
||||
state._roadError = 'Road geometry unavailable';
|
||||
assert.equal(controls.methods.getStats().stale, false);
|
||||
state._roadError = null;
|
||||
|
||||
detail = {
|
||||
segments: [flow],
|
||||
partial: false,
|
||||
stale: false,
|
||||
oldestTileAt: 2000,
|
||||
};
|
||||
state._loadGeneration = 2;
|
||||
await parts.flow.applyFlowToRoads([{ coords: coordinates }], box, 2);
|
||||
const live = controls.methods.getStats();
|
||||
assert.equal(live.stale, false);
|
||||
assert.match(live.loadingLabel, /^LIVE · Roads: OpenStreetMap/);
|
||||
assert.equal(layerFeedState(live), 'nominal');
|
||||
});
|
||||
|
||||
test('status names the drawn road source for each mode', async () => {
|
||||
const { createModel } = await import('./model.js');
|
||||
const { trafficFeedPresentation: present } = createModel({
|
||||
|
||||
@@ -108,6 +108,16 @@ export function createState({ services }) {
|
||||
|
||||
layerState._flowError = null;
|
||||
|
||||
/**
|
||||
* True while the drawn flow came from cached proxy tiles (TomTom budget
|
||||
* spent or upstream failing); `_flowOldestTileAt` is the oldest tile's
|
||||
* fetch time in ms, or null.
|
||||
* @type {boolean}
|
||||
*/
|
||||
|
||||
layerState._flowStale = false;
|
||||
layerState._flowOldestTileAt = null;
|
||||
|
||||
/**
|
||||
* True when `/api/tomtom/status` itself could not be reached, so the layer is
|
||||
* simulating because it could not ask — not because the server said "no key".
|
||||
|
||||
@@ -183,6 +183,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
layerState._fetching = false;
|
||||
layerState._flowPending = 0;
|
||||
layerState._roadError = null;
|
||||
// Nothing is drawn up here, cached or not.
|
||||
layerState._flowStale = false;
|
||||
layerState._flowOldestTileAt = null;
|
||||
parts.animation.clearDots();
|
||||
layerState._lastBounds = null;
|
||||
layerState._lastViewCenter = null;
|
||||
|
||||
Reference in new issue
Block a user