1// Shared poll transport for the chart islands (ChartForgeX SVG swap + ECharts setOption). 2// 3// Polling only by design (see plan): no EventSource, no Blazor circuit â just fetch() on an interval 4// with ETag/304 + jitter. Stateless and cache-collapsing, so it scales to many concurrent viewers of 5// shared data without holding connections open. Loaded once via a normal <script src> before the 6// per-engine island scripts. 7(function () { 8 "use strict"; 9 if (window.tedChartUpdates) return; 10 11 // Start a poll loop. Returns a stop() function, which also carries a .poke() method. 12 // urlOrFn : endpoint URL, OR a function returning the URL each tick (so a live control 13 // selection can be appended per request); honours If-None-Match â 304 14 // intervalMs : base interval; actual wait is jittered ±20% to avoid a thundering herd 15 // onPayload : (payload) => void, called with the parsed JSON only when it changes (HTTP 200) 16 // stop.poke() : fetch immediately (resetting the ETag) â used when a control selection changes
17 // so the chart updates now instead of on the next interval. 18 function pollLoop(urlOrFn, intervalMs, onPayload, onChecked) { 19 var noop = function () { }; 20 noop.poke = noop; 21 if (!urlOrFn || !(intervalMs > 0) || typeof onPayload !== "function") return noop; 22 23 var stopped = false; 24 var etag = null; 25 var timer = null; 26 27 function jitter(ms) { return Math.round(ms * (0.8 + Math.random() * 0.4)); } 28 function currentUrl() { return typeof urlOrFn === "function" ? urlOrFn() : urlOrFn; } 29 30 async function tick() { 31 if (stopped) return; 32 var url = currentUrl(); 33 if (url) { 34 try { 35 var headers = etag ? { "If-None-Match": etag } : {}; 36 var res = await fetch(url, { headers: headers, credentials: "same-origin" }); 37 if (!stopped && res.status === 200) { 38 var tag = res.headers.get("ETag"); 39 if (tag) etag = tag; 40 onPayload(await res.json()); 41 } 42 // A conclusive answer â fresh payload (200) OR unchanged (304) â means the shown 43 // data is confirmed current; surface it with the server's as-of instant 44 // (X-Ted-Data-Refreshed, UTC ISO â the AsOf marker version the data was pulled 45 // under). The header rides on BOTH statuses: the ETag is a content hash, so an 46 // unchanged payload can still carry a newer as-of. 47 if (!stopped && typeof onChecked === "function" && (res.status === 200 || res.status === 304)) { 48 onChecked(res.headers.get("X-Ted-Data-Refreshed")); 49 } 50 // Transient errors â keep the current render, retry next tick. 51 } catch (e) { 52 // Network blip â swallow and retry on the next interval. 53 } 54 } 55 if (!stopped) timer = setTimeout(tick, jitter(intervalMs)); 56 } 57 58 function stop() { stopped = true; if (timer) clearTimeout(timer); } 59 // Re-poll now under the current URL (selection changed). Reset the ETag so a same-URL, 60 // changed-selection case still gets a fresh 200. 61 stop.poke = function () { if (stopped) return; etag = null; if (timer) clearTimeout(timer); tick(); }; 62 63 // First poll runs almost immediately (a SHORT jittered delay, not a full interval): a board 64 // delivered from the SWR fragment cache â or a live in-app nav â shows the warm-time state 65 // (tiles AND the "Data as of" carrier), which is stale if the marker advanced since that warm. 66 // A prompt first tick re-pulls to CURRENT and carries the fresh X-Ted-Data-Refreshed (which 67 // rides even a 304), so the tiles and the side-menu card catch up in ~1-2s instead of sitting 68 // stale for a whole interval. tick() reschedules itself at the full jittered interval; the 69 // small first-tick jitter still spreads the initial page-load burst. 70 timer = setTimeout(tick, jitter(Math.min(intervalMs, 1500))); 71 return stop; 72 } 73 74 window.tedChartUpdates = { pollLoop: pollLoop }; 75})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.