1/* Volume Monitor shared helpers for coin.js (/coin/<BASE>) and coins.js (/coins): formatters, venue registry, poller. 2 Formatters mirror templates/partials/usd.html and the template snippets in coin.html. Exposes window.VMC. */ 3(function () { 4 'use strict'; 5 var $ = function (s, r) { return (r || document).querySelector(s); }; 6 var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }; 7 // Exchanges from the server's venue registry (base.html #vm-venues, monitor/venues_ui.py), in display order. 8 var VENUES = (function () { try { return JSON.parse(document.getElementById('vm-venues').textContent) || []; } catch (e) { return []; } })(); 9 // The brand (settings.SITE_NAME, T-292) as base.html prints it in og:site_name. 10 var SITE_NAME = (function () { var m = document.querySelector('meta[property="og:site_name"]'); return (m && m.content) || ''; })(); 11 var VN = {}, VFULL = {}, LIQS = {}; // name -> chip text ("BIN") / label ("Binance") / has a liquidation feed 12 VENUES.forEach(function (v) { 13 VN[v.name] = v.short || v.label || v.name; 14 VFULL[v.name] = v.label || v.name; 15 if (Array.isArray(v.caps) && v.caps.indexOf('liqs') >= 0) LIQS[v.name] = 1; 16 }); 17 var offset = 0; // server clock minus local clock, ms 18 19 // ---- formatting ----------------------------------------------------------- 20 function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; }); } 21 function usd(v) { 22 if (v == null || isNaN(v)) return 'â'; 23 var a = Math.abs(v), s = v < 0 ? '-$' : '$'; 24 if (a >= 999.5e6) return s + (a / 1e9).toFixed(2) + 'B'; 25 if (a >= 999500) return s + (a / 1e6).toFixed(2) + 'M'; 26 if (a >= 9999.5) return s + (a / 1e3).toFixed(0) + 'K'; 27 return s + Math.round(a).toLocaleString('en-US'); 28 } 29 function signedUsd(v) { return v > 0 ? '+' + usd(v) : v < 0 ? usd(v) : '$0'; } 30 function pct(f) { if (f == null) return 'â'; var v = f * 100; return (v > 0 ? '+' : v < 0 ? '-' : '') + Math.abs(v).toFixed(2) + '%'; } 31 function fund(f) { if (f == null) return 'â'; return (f < 0 ? '-' : '') + Math.abs(f * 100).toFixed(4) + '%'; } 32 function apr(f) { return f == null ? '' : 'â' + Math.round(f * 109500) + '% APR'; } 33 function cls(f) { return f == null ? 'z' : f > 0 ? 'up' : f < 0 ? 'down' : ''; } 34 function price(p) { 35 if (!p) return 'â'; 36 if (p >= 1000) return p.toLocaleString('en-US', { maximumFractionDigits: 1 }); 37 var d = p >= 100 ? 2 : p >= 1 ? 3 : p >= 0.1 ? 4 : p >= 0.01 ? 5 : p >= 0.001 ? 6 : p >= 0.0001 ? 7 : p >= 0.00001 ? 8 : 10; 38 return p.toFixed(d); 39 } 40 function now() { return Date.now() + offset; } 41 function ago(ms) { 42 var d = now() - ms; 43 if (d < 60000) return 'just now'; 44 if (d < 5400000) return Math.round(d / 60000) + 'm ago'; 45 return Math.round(d / 3600000) + 'h ago'; 46 } 47 function hm(ms) { var d = new Date(ms); return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2); } 48 // Funding schedule (mirror monitor/templatetags/vm.py fint, iv_rate, countdown, utc). 49 function fint(h) { if (!h || h <= 0) return ''; return h < 1 ? Math.round(h * 60) + 'm' : (+h.toFixed(4)) + 'h'; } 50 function ivRate(p) { 51 var f = p.funding, h = p.funding_h; 52 if (f == null || !h || h <= 0) return 'â'; 53 var v = f * h / 8; 54 return (v < 0 ? '-' : '') + Math.abs(v * 100).toFixed(h < 4 ? 5 : 4) + '%'; 55 } 56 function countdown(next, at) { 57 if (!next || next <= at) return ''; 58 var m = Math.floor((next - at) / 60000); 59 if (m < 1) return '<1m'; 60 return m >= 60 ? Math.floor(m / 60) + 'h ' + ('0' + (m % 60)).slice(-2) + 'm' : m + 'm'; 61 } 62 function utcHM(ms) { var d = new Date(ms); return ('0' + d.getUTCHours()).slice(-2) + ':' + ('0' + d.getUTCMinutes()).slice(-2) + ' UTC'; } 63 function chip(venue, market) { 64 return '<span class="chip ' + esc(venue) + '" title="' + esc((VFULL[venue] || venue) + (market ? ' ' + market : '')) + '">' + 65 esc(VN[venue] || venue) + (market ? ' <span class="m">' + esc(market) + '</span>' : '') + '</span>'; 66 } 67 function num(v) { if (v === '' || v == null) return null; var n = parseFloat(v); return isNaN(n) ? null : n; } 68 function live(ok, text) { 69 var el = $('#live'); if (!el) return; 70 el.classList.toggle('ok', ok); el.classList.toggle('bad', !ok); 71 $('span', el).textContent = text; 72 } 73 74 // ---- polling helper --------------------------------------------------------- 75 // A live fetch that never settles must not stop polling (T-663): run(signal) is aborted and the promise rejects 76 // (err.timeout) after ms, so the caller's settle path (in-flight flag, next schedule) always runs. 77 function bounded(ms, run) { 78 var ac = typeof AbortController === 'function' ? new AbortController() : null, t = 0; 79 var cap = new Promise(function (_, no) { 80 t = setTimeout(function () { var e = new Error('timeout'); e.timeout = true; if (ac) ac.abort(); no(e); }, ms); 81 }); 82 return Promise.race([Promise.resolve().then(function () { return run(ac ? ac.signal : undefined); }
82), cap]) 83 .then(function (v) { clearTimeout(t); return v; }, function (e) { clearTimeout(t); throw e; }); 84 } 85 function poller(url, every, onData, onFail, delay) { 86 var timer = null, busy = false, lastRun = delay ? Date.now() - every + delay : 0; 87 function tick() { 88 clearTimeout(timer); 89 if (document.hidden) return; // resumes on visibilitychange 90 var wait = lastRun + every - Date.now(); 91 if (busy || wait > 50) { timer = setTimeout(tick, busy ? every : wait); return; } 92 busy = true; lastRun = Date.now(); 93 bounded(Math.max(15000, 2 * every), function (signal) { 94 return fetch(url, { cache: 'no-store', signal: signal }).then(function (r) { 95 if (!r.ok) { var e = new Error(r.status); e.status = r.status; throw e; } 96 return r.json(); 97 }); 98 }).then(function (d) { 99 if (d.server_ts) offset = d.server_ts - Date.now(); 100 onData(d); 101 }).catch(function (e) { try { onFail(e); } catch (x) { } }) 102 .then(function () { busy = false; clearTimeout(timer); timer = setTimeout(tick, every); }); 103 } 104 document.addEventListener('visibilitychange', function () { if (!document.hidden) tick(); }); 105 return tick; 106 } 107 108 window.VMC = { esc: esc, usd: usd, signedUsd: signedUsd, pct: pct, fund: fund, apr: apr, cls: cls, price: price, now: now, ago: ago, hm: hm, fint: fint, ivRate: ivRate, countdown: countdown, utcHM: utcHM, chip: chip, num: num, live: live, poller: poller, bounded: bounded, "$": $, "$$": $$, SITE_NAME: SITE_NAME, VN: VN, VFULL: VFULL, LIQS: LIQS }; 109 // Spike feed links (T-545): stars persist only signed in or in device mode, so otherwise a[data-feedlink] 110 // carries them as a shared view (/?w=BTC,SOL, T-514) and the feed still highlights this visit's stars. 111 (function () { 112 var P = window.vmPrefs, S = P && P.stars, links = $$('a[data-feedlink]'); 113 if (!S || !links.length) return; 114 function url() { 115 var l = S.list().filter(function (b) { return /^[A-Za-z0-9]{1,20}$/.test(b); }).slice(0, 50); 116 return P.signedIn() || P.device() || !l.length ? '/' : '/?w=' + l.join(','); 117 } 118 function upd() { var u = url(); links.forEach(function (a) { a.setAttribute('href', u); }); } 119 links.forEach(function (a) { a.addEventListener('click', upd); }); 120 S.on(upd); 121 upd(); 122 })(); 123 // Coin-to-coin navigation: character keys only act on focused neighbouring-coin links. 124 (function () { 125 var prev = $('a[data-coinnav="prev"]'), next = $('a[data-coinnav="next"]'); 126 if (!prev && !next) return; 127 document.addEventListener('keydown', function (e) { 128 if (e.defaultPrevented || e.isComposing || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return; 129 var t = e.target, tag = t && t.tagName; 130 if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || (t && t.isContentEditable)) return; 131 if ((t !== prev && t !== next) || document.activeElement !== t) return; 132 var a = (e.key === '[' || e.key === 'k') ? prev : (e.key === ']' || e.key === 'j') ? next : null; 133 if (a) { e.preventDefault(); location.href = a.href; } 134 }); 135 var x0 = null, y0 = 0; 136 document.addEventListener('touchstart', function (e) { 137 var t = e.touches[0]; 138 x0 = e.touches.length === 1 && !(e.target.closest && e.target.closest('table, canvas, input, select, textarea, .scroll, [data-noswipe]')) ? t.clientX : null; y0 = t.clientY; 139 }, { passive: true }); 140 document.addEventListener('touchend', function (e) { 141 if (x0 === null) return; 142 var t = e.changedTouches[0], dx = t.clientX - x0, dy = t.clientY - y0; x0 = null; 143 if (Math.abs(dx) < 110 || Math.abs(dy) > Math.abs(dx) / 2) return; 144 var a = dx > 0 ? prev : next; 145 if (a) location.href = a.href; 146 }, { passive: true }); 147 })(); 148 // "Notify me" quick alerts (T-552): alerts.js (rule creation, manage token, push) loads only on first open. 149 (function () { 150 var nt = $('details[data-notify]'); 151 if (!nt) return; 152 nt.addEventListener('toggle', function () { 153 if (!nt.open || nt.getAttribute('data-loaded')) return; 154 nt.setAttribute('data-loaded', '1'); 155 var s = document.createElement('script'); 156 s.src = nt.getAttribute('data-src'); 157 s.onload = function () { if (window.SwoleAlerts) window.SwoleAlerts.quick(nt); }; 158 s.onerror = function () { 159 nt.removeAttribute('data-loaded'); 160 var m = $('[data-notify-msg]', nt); 161 m.hidden = false; m.className = 'nt-msg err'; m.textContent = 'Could not load alerts. Close and reopen to try again.'; 162 }; 163 document.head.appendChild(s); 164 }); 165 })(); 166})();
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.