mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge branch 'main' into sameh/hud-google-lookups
This commit is contained in:
17 files changed
+268
-24
No files matched your search
@@ -9,9 +9,18 @@ const sourceSlot = createSourceSlot(
|
||||
{
|
||||
getFlowSessionStats: () => ({ tilesFetched: 0 }),
|
||||
resetFlowTileCache: () => {},
|
||||
// The layer reads stale cached tiles through this; a source without it
|
||||
// answers with fresh segments only.
|
||||
fetchFlowDetail: async (...args) => ({
|
||||
segments: await sourceSlot.source.fetchFlowForBounds(...args),
|
||||
stale: false,
|
||||
oldestTileAt: null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const configureTrafficSource = sourceSlot.configure;
|
||||
/** The source the traffic layer reads, as configured. */
|
||||
export const trafficSource = sourceSlot.source;
|
||||
const layer = createApplicationTraffic({
|
||||
source: sourceSlot.source,
|
||||
});
|
||||
|
||||
+105
-19
@@ -22,7 +22,10 @@ import { DataLayerManager, layerFeedState } from './manager.js';
|
||||
const LIVE_CLAIM = /\bLIVE\b|\bGPS\b|\breal[- ]?time\b/;
|
||||
|
||||
test('a superseded flow fetch is not an outage', () => {
|
||||
assert.equal(deriveTrafficFlowError({ name: 'AbortError', message: 'aborted' }), null);
|
||||
assert.equal(
|
||||
deriveTrafficFlowError({ name: 'AbortError', message: 'aborted' }),
|
||||
null,
|
||||
);
|
||||
assert.equal(deriveTrafficFlowError(null), null);
|
||||
assert.equal(deriveTrafficFlowError(undefined), null);
|
||||
});
|
||||
@@ -30,10 +33,22 @@ test('a superseded flow fetch is not an outage', () => {
|
||||
test('flow failures map onto short, specific reasons', () => {
|
||||
const reason = (message) => deriveTrafficFlowError(new Error(message));
|
||||
assert.equal(reason('flow tile 12/1/1: HTTP 503'), 'TomTom key unavailable');
|
||||
assert.equal(reason('flow tile 12/1/1: HTTP 429'), 'TomTom daily budget reached');
|
||||
assert.equal(reason('flow tile 12/1/1: HTTP 502'), 'TomTom upstream unreachable');
|
||||
assert.equal(reason('flow tile 12/1/1: HTTP 504'), 'TomTom upstream unreachable');
|
||||
assert.equal(reason('flow tile 12/1/1: HTTP 418'), 'TomTom flow error (HTTP 418)');
|
||||
assert.equal(
|
||||
reason('flow tile 12/1/1: HTTP 429'),
|
||||
'TomTom daily budget reached',
|
||||
);
|
||||
assert.equal(
|
||||
reason('flow tile 12/1/1: HTTP 502'),
|
||||
'TomTom upstream unreachable',
|
||||
);
|
||||
assert.equal(
|
||||
reason('flow tile 12/1/1: HTTP 504'),
|
||||
'TomTom upstream unreachable',
|
||||
);
|
||||
assert.equal(
|
||||
reason('flow tile 12/1/1: HTTP 418'),
|
||||
'TomTom flow error (HTTP 418)',
|
||||
);
|
||||
assert.equal(reason('flow fetch failed'), 'TomTom flow unavailable');
|
||||
});
|
||||
|
||||
@@ -56,19 +71,32 @@ test('no keyless label ever implies a live feed', () => {
|
||||
trafficFeedPresentation({ liveMode: false, fetching: false }),
|
||||
trafficFeedPresentation({ liveMode: false, fetching: true }),
|
||||
trafficFeedPresentation({ statusUnavailable: true }),
|
||||
trafficFeedPresentation({ liveMode: true, flowError: 'TomTom flow unavailable' }),
|
||||
trafficFeedPresentation({ liveMode: true, fetching: true, flowError: 'TomTom flow unavailable' }),
|
||||
trafficFeedPresentation({
|
||||
liveMode: true,
|
||||
flowError: 'TomTom flow unavailable',
|
||||
}),
|
||||
trafficFeedPresentation({
|
||||
liveMode: true,
|
||||
fetching: true,
|
||||
flowError: 'TomTom flow unavailable',
|
||||
}),
|
||||
].map((feed) => feed.loadingLabel);
|
||||
for (const label of labels) {
|
||||
assert.ok(!LIVE_CLAIM.test(label), `label implies live data: ${label}`);
|
||||
assert.ok(label.startsWith('SIMULATED'), `fallback label must lead with the mode: ${label}`);
|
||||
assert.ok(
|
||||
label.startsWith('SIMULATED'),
|
||||
`fallback label must lead with the mode: ${label}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('simulating because the status probe failed reads differently from keyless by design', () => {
|
||||
const probeDown = trafficFeedPresentation({ statusUnavailable: true });
|
||||
assert.equal(probeDown.mode, 'sim');
|
||||
assert.equal(probeDown.loadingLabel, 'SIMULATED — traffic service unreachable');
|
||||
assert.equal(
|
||||
probeDown.loadingLabel,
|
||||
'SIMULATED — traffic service unreachable',
|
||||
);
|
||||
});
|
||||
|
||||
test('a healthy keyed layer reports live flow with its real coverage', () => {
|
||||
@@ -76,7 +104,8 @@ test('a healthy keyed layer reports live flow with its real coverage', () => {
|
||||
assert.deepEqual(idle, {
|
||||
mode: 'live',
|
||||
error: null,
|
||||
loadingLabel: 'LIVE · Roads: OpenStreetMap · Flow: TomTom · 87% cov · Unmatched: simulated',
|
||||
loadingLabel:
|
||||
'LIVE · Roads: OpenStreetMap · Flow: TomTom · 87% cov · Unmatched: simulated',
|
||||
});
|
||||
assert.equal(
|
||||
trafficFeedPresentation({ liveMode: true, fetching: true }).loadingLabel,
|
||||
@@ -100,7 +129,11 @@ test('a mid-session flow outage degrades instead of reporting stale live coverag
|
||||
fetching: true,
|
||||
flowError: 'TomTom daily budget reached',
|
||||
});
|
||||
assert.deepEqual(busy, down, 'the degraded state reads the same whether or not a load is in flight');
|
||||
assert.deepEqual(
|
||||
busy,
|
||||
down,
|
||||
'the degraded state reads the same whether or not a load is in flight',
|
||||
);
|
||||
});
|
||||
|
||||
test('the rendered steady-state meta line carries the SIMULATED copy', () => {
|
||||
@@ -116,10 +149,12 @@ test('the rendered steady-state meta line carries the SIMULATED copy', () => {
|
||||
assert.equal(
|
||||
mgr._buildMetaText({
|
||||
source: 'OpenStreetMap',
|
||||
stats: stats(trafficFeedPresentation({
|
||||
liveMode: true,
|
||||
flowError: 'TomTom daily budget reached',
|
||||
})),
|
||||
stats: stats(
|
||||
trafficFeedPresentation({
|
||||
liveMode: true,
|
||||
flowError: 'TomTom daily budget reached',
|
||||
}),
|
||||
),
|
||||
}),
|
||||
'DEGRADED · OpenStreetMap · SIMULATED — TomTom daily budget reached',
|
||||
);
|
||||
@@ -128,17 +163,26 @@ test('the rendered steady-state meta line carries the SIMULATED copy', () => {
|
||||
test('the manager reads keyless as FALLBACK and an outage as DEGRADED', () => {
|
||||
const settled = { count: 4200, lastUpdate: Date.now() };
|
||||
assert.equal(
|
||||
layerFeedState({ ...settled, ...trafficFeedPresentation({ liveMode: false }) }),
|
||||
layerFeedState({
|
||||
...settled,
|
||||
...trafficFeedPresentation({ liveMode: false }),
|
||||
}),
|
||||
'fallback',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({ ...settled, ...trafficFeedPresentation({ liveMode: true }) }),
|
||||
layerFeedState({
|
||||
...settled,
|
||||
...trafficFeedPresentation({ liveMode: true }),
|
||||
}),
|
||||
'nominal',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({
|
||||
...settled,
|
||||
...trafficFeedPresentation({ liveMode: true, flowError: 'TomTom flow unavailable' }),
|
||||
...trafficFeedPresentation({
|
||||
liveMode: true,
|
||||
flowError: 'TomTom flow unavailable',
|
||||
}),
|
||||
}),
|
||||
'degraded',
|
||||
);
|
||||
@@ -148,7 +192,10 @@ test('the shipped layer boots keyless-honest before any status check', () => {
|
||||
const stats = trafficLayer.getStats();
|
||||
assert.equal(stats.mode, 'sim');
|
||||
assert.equal(stats.error, null);
|
||||
assert.ok(!LIVE_CLAIM.test(stats.loadingLabel), `boot label implies live data: ${stats.loadingLabel}`);
|
||||
assert.ok(
|
||||
!LIVE_CLAIM.test(stats.loadingLabel),
|
||||
`boot label implies live data: ${stats.loadingLabel}`,
|
||||
);
|
||||
assert.equal(layerFeedState(stats), 'fallback');
|
||||
});
|
||||
|
||||
@@ -167,3 +214,42 @@ test('zero matched dots never claims live coverage', () => {
|
||||
assert.match(feed.loadingLabel, /TomTom \(no matches\)/);
|
||||
assert.ok(!LIVE_CLAIM.test(feed.loadingLabel));
|
||||
});
|
||||
|
||||
test('the traffic layer source passes fetchFlowDetail through, so stale tiles reach the layer', async () => {
|
||||
const { trafficSource, configureTrafficSource } =
|
||||
await import('./traffic.js');
|
||||
assert.equal(typeof trafficSource.fetchFlowDetail, 'function');
|
||||
const base = {
|
||||
requestRoads: async () => ({}),
|
||||
getStatus: async () => ({}),
|
||||
fetchFlowForBounds: async () => ['segment'],
|
||||
};
|
||||
let release = configureTrafficSource({
|
||||
...base,
|
||||
fetchFlowDetail: async () => ({
|
||||
segments: ['segment'],
|
||||
stale: true,
|
||||
oldestTileAt: 1000,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
assert.deepEqual(await trafficSource.fetchFlowDetail({}), {
|
||||
segments: ['segment'],
|
||||
stale: true,
|
||||
oldestTileAt: 1000,
|
||||
});
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
// A source without fetchFlowDetail answers with fresh segments.
|
||||
release = configureTrafficSource(base);
|
||||
try {
|
||||
assert.deepEqual(await trafficSource.fetchFlowDetail({}), {
|
||||
segments: ['segment'],
|
||||
stale: false,
|
||||
oldestTileAt: null,
|
||||
});
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
});
|
||||
@@ -864,6 +864,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}%",
|
||||
|
||||
@@ -1054,6 +1054,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;
|
||||
|
||||
@@ -714,18 +714,24 @@ export class LayerPanel {
|
||||
});
|
||||
}
|
||||
if (feedState === 'stale') {
|
||||
// A layer drawing cached upstream data (Traffic's proxy tiles) dates the
|
||||
// row by the oldest of it, not by the load that re-served it.
|
||||
const dataAgo =
|
||||
Number(stats.oldestTileAt) > 0
|
||||
? this._timeAgo(stats.oldestTileAt)
|
||||
: ago;
|
||||
if (typeof stats.retryInSec === 'number' && stats.retryInSec > 0) {
|
||||
return t('{state} · {source} · {ago} · retrying in {seconds}s', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
ago,
|
||||
ago: dataAgo,
|
||||
seconds: stats.retryInSec,
|
||||
});
|
||||
}
|
||||
return t('{state} · {source} · {ago}', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
ago,
|
||||
ago: dataAgo,
|
||||
});
|
||||
}
|
||||
if (typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()) {
|
||||
|
||||
@@ -93,6 +93,27 @@ test('partial feed controls distinguish incomplete records from stale data and o
|
||||
assert.equal(button.textContent, 'OFF');
|
||||
});
|
||||
|
||||
test('a stale row dates cached data by its oldest tile, not the last load', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const panel = LayerPanel.prototype;
|
||||
const layer = {
|
||||
id: 'traffic',
|
||||
name: 'Street Traffic',
|
||||
source: 'TomTom',
|
||||
enabled: true,
|
||||
stats: {
|
||||
mode: 'live',
|
||||
stale: true,
|
||||
count: 4,
|
||||
lastUpdate: Date.now(),
|
||||
oldestTileAt: Date.now() - 3 * 3600_000,
|
||||
},
|
||||
};
|
||||
assert.match(panel._buildMetaText(layer), /^STALE · TomTom · 3h ago$/);
|
||||
delete layer.stats.oldestTileAt;
|
||||
assert.match(panel._buildMetaText(layer), /^STALE · TomTom · just now$/);
|
||||
});
|
||||
|
||||
test('readout rows contain only toggles and metadata; ordinary rows retain controls', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const { railFixture } = await import('./railTestFixture.mjs');
|
||||
|
||||
Reference in new issue
Block a user