| |
| |
| import WebSocket from 'ws'; |
| |
| export function connectFeed(url, onFrame, log = console.log) { |
| let ws = null, wait = 1000, alive = 0, timer = null, closed = false; |
| const state = { url, connected: false, since: 0, reconnects: 0 }; |
| |
| const open = () => { |
| if (closed) return; |
| ws = new WebSocket(url, { perMessageDeflate: true, handshakeTimeout: 15000 }); |
| ws.on('open', () => { |
| state.connected = true; |
| state.since = Date.now(); |
| if (!state.reconnects) log(`feed: connected to ${url}`); |
| }); |
| ws.on('message', buf => { |
| alive = Date.now(); |
| const raw = buf.toString(); |
| let f; |
| try { f = JSON.parse(raw); } catch { return; } |
| |
| if (Array.isArray(f)) for (const x of f) onFrame(x, null); |
| else onFrame(f, raw); |
| }); |
| ws.on('close', (code, reason) => { |
| const lived = state.connected ? Date.now() - state.since : 0; |
| |
| if (lived > 15000) wait = 1000; |
| const why = `${code}${reason && reason.length ? ' ' + reason.toString().slice(0, 120) : ''}`; |
| if (lived > 15000 || state.reconnects < 3 || state.reconnects % 20 === 0) { |
| log(`feed: closed (${why}) after ${Math.round(lived / 1000)} s, retrying in ${Math.round(wait / 1000)} s${lived < 2000 && state.reconnects > 1 ? '. The stream may allow only a few connections per IP address: close other apps reading it.' : ''}`); |
| } |
| state.connected = false; |
| state.reconnects++; |
| setTimeout(open, wait); |
| wait = Math.min(wait * 2, 30000); |
| }); |
| ws.on('error', err => { log(`feed: ${err.message}`); try { ws.terminate(); } catch {} }); |
| }; |
| open(); |
| |
| timer = setInterval(() => { if (state.connected && Date.now() - alive > 30000) { log('feed: quiet for 30 s, reopening'); try { ws.terminate(); } catch {} } }, 10000); |
| state.close = () => { closed = true; clearInterval(timer); try { ws.close(); } catch {} }; |
| return state; |
| } |
| |
| |
| export function solPrice(log = console.log) { |
| const st = { usd: 0, at: 0 }; |
| const sources = [ |
| async () => +(await (await fetch('https://api.coinbase.com/v2/prices/SOL-USD/spot', { signal: AbortSignal.timeout(6000) })).json()).data.amount, |
| async () => +(await (await fetch('https://api.kraken.com/0/public/Ticker?pair=SOLUSD', { signal: AbortSignal.timeout(6000) })).json()).result.SOLUSD.c[0], |
| async () => +(await (await fetch('https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT', { signal: AbortSignal.timeout(6000) })).json()).price, |
| ]; |
| const tick = async () => { |
| for (const s of sources) { |
| try { const v = await s(); if (v > 1) { st.usd = v; st.at = Date.now(); return; } } catch {} |
| } |
| if (!st.usd) log('sol price: no source answered, retrying'); |
| }; |
| tick(); |
| setInterval(tick, 20000).unref(); |
| return st; |
| } |