1/** 2 * srr-onair live hydration. 3 * 4 * Strategy: 5 * 1. Try the static-cache JSON file first (data-srr-onair-static-url). 6 * That file is served directly by the webserver â no PHP, no MySQL, 7 * no WordPress boot. This is the hot path for every poll. 8 * 2. If the file 404s, fails, or contains a stale valid_until, fall back 9 * once to the REST endpoint (/onair/v1/render?t=<token>). The REST 10 * handler regenerates the static file as a side effect, so subsequent 11 * polls are back on the static path. 12 * 13 * Refreshes on DOMContentLoaded, every `interval` ms while the tab is 14 * visible, on visibilitychange to visible, and on pageshow (Safari/Firefox 15 * bfcache restore). 16 * 17 * Localized config (see srr_onair_enqueue_frontend_styles in onair.php): 18 * window.SRR_ONAIR_LIVE = { 19 * restUrl: "https://example.com/wp-json/onair/v1/render", 20 * interval: 60000, 21 * debug: false, 22 * cacheBust: false // dev-only â adds &_=Date.now() to fallback URL 23 * }; 24 */ 25(function () { 26 'use strict'; 27 28 var cfg = window.SRR_ONAIR_LIVE || {}; 29 if (!cfg.restUrl) { return; } 30 31 var interval = parseInt(cfg.interval, 10); 32 if (!interval || interval < 10000) { interval = 60000; } 33 34 var DEBUG = !!cfg.debug; 35 var timer = null; 36 var inflight = new WeakSet(); 37 38 function log() { 39 if (!DEBUG) { return; } 40 try { console.log.apply(console, ['[srr-onair-live]'].concat([].slice.call(arguments))); } catch (e) {} 41 } 42 43 function nowSeconds() { return Math.floor(Date.now() / 1000); } 44 45 function applyData(el, data) { 46 if (!data || typeof data.html !== 'string') { return; } 47 var currentRev = el.getAttribute('data-srr-onair-rev') || ''; 48 if (data.rev && data.rev === currentRev) { 49 log('unchanged', el); 50 } else { 51 el.innerHTML = data.html; 52 if (data.rev) { 53 el.setAttribute('data-srr-onair-rev', data.rev); 54 } 55 log('updated', el, data.rev); 56 } 57 el.setAttribute('data-srr-onair-fresh', '1'); 58 } 59 60 /** 61 * Fetch via the REST endpoint. This boots WordPress, so it should only 62 * fire (a) on first hydration of a never-seen widget config, or 63 * (b) when the static file is past its valid_until window. 64 */ 65 function fetchRest(el) { 66 var token = el.getAttribute('data-srr-onair-token'); 67 if (!token) { return Promise.reject(new Error('no token')); } 68 69 var url = cfg.restUrl 70 + (cfg.restUrl.indexOf('?') === -1 ? '?' : '&') 71 + 't=' + encodeURIComponent(token); 72 73 if (cfg.cacheBust) { 74 url += '&_=' + Date.now(); 75 } 76 77 return fetch(url, { 78 method: 'GET', 79 credentials: 'omit', 80 headers: { 'Accept': 'application/json' } 81 }).then(function (resp) { 82 if (!resp.ok) { throw new Error('REST HTTP ' + resp.status); } 83 return resp.json(); 84 }); 85 } 86 87 /** 88 * Fetch the static JSON file. Resolves with the parsed JSON on success, 89 * or rejects so the caller can fall back to REST. 90 * 91 * We bust the browser HTTP cache with `&v=<floor(now/30s)>` so a single 92 * URL doesn't get pinned to stale content by an over-eager intermediate 93 * cache, but only every 30s so the same URL is reused across a poll 94 * cycle and any CDN can still serve repeats. 95 */ 96 function fetchStatic(staticUrl) { 97 var bucket = Math.floor(nowSeconds() / 30); 98 var url = staticUrl 99 + (staticUrl.indexOf('?') === -1 ? '?' : '&') 100 + 'v=' + bucket; 101 102 return fetch(url, { 103 method: 'GET', 104 credentials: 'omit', 105 headers: { 'Accept': 'application/json' } 106 }).then(function (resp) { 107 if (!resp.ok) { throw new Error('static HTTP ' + resp.status); } 108 return resp.json(); 109 }); 110 } 111 112 function hydrate(el) { 113 if (inflight.has(el)) { return; } 114 inflight.add(el); 115 116 var staticUrl = el.getAttribute('data-srr-onair-static-url') || ''; 117 var done = function () { inflight.delete(el); }; 118 119 var doRest = function (reason) { 120 log('fallback-rest', reason, el); 121 fetchRest(el).then(function (data) { 122 applyData(el, data); 123 }).catch(function (err) { 124 log('rest-error', err, el); 125 }).then(done); 126 }; 127 128 if (!staticUrl) { 129 doRest('no-static-url'); 130 return; 131 } 132 133 fetchStatic(staticUrl).then(function (data) { 134 if (!data || typeof data.html !== 'string') { 135 doRest('static-malformed'); 136 return; 137 } 138 // Static file has a valid_until embedded by the server. If we're 139 // past it, the schedule has rolled past a show boundary â hit 140 // REST once to regenerate, then we're back on the static path.
141 var validUntil = data.valid_until ? parseInt(data.valid_until, 10) : 0; 142 if (validUntil && validUntil <= nowSeconds()) { 143 doRest('static-stale'); 144 return; 145 } 146 applyData(el, data); 147 done(); 148 }).catch(function (err) { 149 doRest(err && err.message ? err.message : 'static-fetch-failed'); 150 }); 151 } 152 153 function hydrateAll() { 154 var nodes = document.querySelectorAll('[data-srr-onair-live="1"]'); 155 for (var i = 0; i < nodes.length; i++) { 156 hydrate(nodes[i]); 157 } 158 } 159 160 function start() { 161 stop(); 162 if (document.hidden) { return; } 163 timer = setInterval(hydrateAll, interval); 164 } 165 166 function stop() { 167 if (timer) { clearInterval(timer); timer = null; } 168 } 169 170 function init() { 171 hydrateAll(); 172 start(); 173 } 174 175 if (document.readyState === 'loading') { 176 document.addEventListener('DOMContentLoaded', init, false); 177 } else { 178 init(); 179 } 180 181 // Refresh + restart timer when tab becomes visible. 182 document.addEventListener('visibilitychange', function () { 183 if (document.hidden) { 184 stop(); 185 } else { 186 hydrateAll(); 187 start(); 188 } 189 }, false); 190 191 // Catches Safari/Firefox bfcache restore â page comes back with stale DOM. 192 window.addEventListener('pageshow', function (e) { 193 if (e.persisted) { 194 hydrateAll(); 195 start(); 196 } 197 }, false); 198 199 // Public hook for manual refresh (e.g., from station-control panels). 200 window.SRR_ONAIR_LIVE.refresh = hydrateAll; 201})();
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.