perf abort obsolete chart requests

- attach abort signals to chart and recent-reading fetches - cancel active probe requests when graph controls change range

Commit
54a52bbd7b0871b820303670a9b9c4fb5f5e881d
Author
GPT-5 medium <codex@openai.com>
Author date
Committer
GPT-5 medium <codex@openai.com>
Committer date
Changed files
roles/dashboard/public/js/dashboard.js
index 7a84d19d..c8f73c15 100644..100644
@@ -12,7 +12,7 @@
12 12 button.addEventListener("click", () => probes.activateTab(button.dataset.probe));
13 13 });
14 14
15 Removed: initialiseGraphControls(timeframe, probes.loadDisplayed);
15 Added: initialiseGraphControls(timeframe, probes.reloadDisplayed);
16 16
17 17 window.addEventListener("hashchange", () => {
18 18 const probe = probes.probeFromLocation();
roles/dashboard/public/js/dashboard/api.js
index a0d68b72..ca5d209e 100644..100644
@@ -1,5 +1,5 @@
1 Removed: async function fetchJson(url) {
2 Removed: const response = await fetch(url);
1 Added: async function fetchJson(url, options = {}) {
2 Added: const response = await fetch(url, options);
3 3
4 4 if (!response.ok) {
5 5 throw new Error(`HTTP ${response.status}`);
@@ -21,18 +21,19 @@
21 21 return readings.length ? readings[readings.length - 1] : null;
22 22 }
23 23
24 Removed: export async function fetchProbeData(probe, timeframe, range, includeReadings = false) {
24 Added: export async function fetchProbeData(probe, timeframe, range, includeReadings = false, signal = null) {
25 25 const seriesParams = new URLSearchParams({
26 26 timeframe,
27 27 range: String(range),
28 28 smooth: "1"
29 29 });
30 30 const readingsParams = new URLSearchParams({ limit: "10" });
31 Added: const options = signal ? { signal } : {};
31 32 const readingsRequest = includeReadings
32 Removed: ? fetchJson(`/api/readings/${probe}?${readingsParams}`)
33 Added: ? fetchJson(`/api/readings/${probe}?${readingsParams}`, options)
33 34 : Promise.resolve({ readings: [] });
34 35 const [series, readings] = await Promise.all([
35 Removed: fetchJson(`/api/readings/${probe}/series?${seriesParams}`),
36 Added: fetchJson(`/api/readings/${probe}/series?${seriesParams}`, options),
36 37 readingsRequest
37 38 ]);
38 39
roles/dashboard/public/js/dashboard/charts.js
index 4a596263..ba79b4f7 100644..100644
@@ -212,7 +212,7 @@
212 212 const charts = new Map();
213 213 const loading = new Map();
214 214
215 Removed: async function requestProbe(probe) {
215 Added: async function requestProbe(probe, signal) {
216 216 const canvas = chartElement(probe);
217 217 if (!probe || !canvas) return;
218 218
@@ -224,7 +224,8 @@
224 224 probe,
225 225 timeframe.selected(),
226 226 timeframe.range(),
227 Removed: hasMessageTable
227 Added: hasMessageTable,
228 Added: signal
228 229 );
229 230 const series = chartSeries(data.series, timeframe.selected());
230 231 const latest = [...data.readings]
@@ -264,21 +265,30 @@
264 265 }
265 266 renderMessageTable(probe, data.readings);
266 267 } catch (error) {
268 Added: if (error.name === "AbortError") return;
269 Added:
267 270 if (status) status.textContent = `Could not load ${probe} readings: ${error.message}`;
268 271 renderMessageTable(probe, []);
269 272 }
270 273 }
271 274
272 Removed: function loadProbe(probe) {
273 Removed: if (loading.has(probe)) return loading.get(probe);
275 Added: function loadProbe(probe, { replace = false } = {}) {
276 Added: const active = loading.get(probe);
277 Added: if (active && !replace) return active.request;
278 Added: if (active) active.controller.abort();
274 279
275 Removed: const request = requestProbe(probe).finally(() => {
276 Removed: if (loading.get(probe) === request) loading.delete(probe);
280 Added: const entry = { controller: new AbortController(), request: null };
281 Added: entry.request = requestProbe(probe, entry.controller.signal).finally(() => {
282 Added: if (loading.get(probe) === entry) loading.delete(probe);
277 283 });
278 Removed: loading.set(probe, request);
279 Removed: return request;
284 Added: loading.set(probe, entry);
285 Added: return entry.request;
280 286 }
281 287
288 Added: function loadDisplayed(options = {}) {
289 Added: return Promise.all(displayedProbes().map(probe => loadProbe(probe, options)));
290 Added: }
291 Added:
282 292 function activateTab(probe) {
283 293 document.querySelectorAll(".tab-button").forEach(button => {
284 294 const active = button.dataset.probe === probe;
@@ -309,7 +319,8 @@
309 319 activateTab,
310 320 displayedProbes,
311 321 loadProbe,
312 Removed: loadDisplayed: () => Promise.all(displayedProbes().map(loadProbe)),
322 Added: loadDisplayed,
323 Added: reloadDisplayed: () => loadDisplayed({ replace: true }),
313 324 probeFromLocation,
314 325 probeFromHash: () => window.location.hash.match(/^#probe-([a-z]+)$/)?.[1] || null
315 326 };