repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

3.5 KB · 108 lines · JavaScript Raw History
  1// On-demand data refresh for the symbol page (Phase B).
  2//
  3// The server fetches nothing on a timer any more: a symbol's data is pulled
  4// here, when its page is open. On load this auto-runs a refresh (the live price
  5// always; slow SEC / metadata only when their stored copy is stale); the
  6// Refresh button re-pulls everything. Progress streams over SSE from
  7// `/api/symbols/{ticker}/refresh` and drives the bar.
  8//
  9// When a "deep" (server-rendered) section was refreshed, the server's `done`
 10// event asks us to reload so the new data and its "synced … ago" age render;
 11// when only the live price moved, the stream client has already patched it in
 12// place, so no reload is needed. A one-shot sessionStorage flag set just before
 13// our own reload keeps that reload from re-triggering the auto-refresh.
 14
 15export function initRefresh() {
 16  const root = document.querySelector("[data-refresh-root]");
 17  if (!root) return;
 18
 19  const ticker = root.dataset.ticker;
 20  const btn = root.querySelector("[data-refresh]");
 21  const bar = root.querySelector("[data-refresh-bar]");
 22  const fill = root.querySelector("[data-refresh-fill]");
 23  const status = root.querySelector("[data-refresh-status]");
 24  const skipKey = "fin:skipnext:" + ticker;
 25
 26  let running = false;
 27
 28  const setStatus = (t) => {
 29    if (status) status.textContent = t;
 30  };
 31  const setFill = (pct) => {
 32    if (fill) fill.style.width = Math.max(0, Math.min(100, pct)) + "%";
 33  };
 34
 35  function run(force) {
 36    if (running) return;
 37    running = true;
 38    root.classList.add("is-running");
 39    if (bar) bar.hidden = false;
 40    setFill(2);
 41    if (btn) btn.disabled = true;
 42    setStatus("Updating…");
 43
 44    const url = `/api/symbols/${encodeURIComponent(ticker)}/refresh?force=${force ? 1 : 0}`;
 45    const es = new EventSource(url);
 46    let done = false;
 47
 48    es.addEventListener("step", (e) => {
 49      let d;
 50      try {
 51        d = JSON.parse(e.data);
 52      } catch {
 53        return;
 54      }
 55      // A step counts as half-done while running, full when it reports back, so
 56      // the bar advances smoothly across the step list.
 57      const progressed = d.i - (d.state === "running" ? 0.5 : 0);
 58      setFill((progressed / Math.max(1, d.n)) * 100);
 59      setStatus("Updating: " + d.label);
 60    });
 61
 62    es.addEventListener("done", (e) => {
 63      done = true;
 64      es.close();
 65      running = false;
 66      setFill(100);
 67      let reload = false;
 68      try {
 69        reload = !!JSON.parse(e.data).reload;
 70      } catch {}
 71      if (reload) {
 72        // Show the new server-rendered data + ages. Mark this so the load it
 73        // triggers does not auto-refresh again.
 74        sessionStorage.setItem(skipKey, "1");
 75        location.reload();
 76        return;
 77      }
 78      root.classList.remove("is-running");
 79      if (btn) btn.disabled = false;
 80      setStatus("Up to date");
 81      window.setTimeout(() => {
 82        if (bar) bar.hidden = true;
 83      }, 700);
 84    });
 85
 86    es.onerror = () => {
 87      if (done) return; // normal end-of-stream after `done`
 88      es.close();
 89      running = false;
 90      root.classList.remove("is-running");
 91      if (btn) btn.disabled = false;
 92      if (bar) bar.hidden = true;
 93      setStatus("Couldn’t refresh — showing stored data");
 94    };
 95  }
 96
 97  if (btn) btn.addEventListener("click", () => run(true));
 98
 99  // Auto-refresh on load, unless this very load was triggered by our own reload
100  // after a refresh (the one-shot skip flag).
101  if (sessionStorage.getItem(skipKey)) {
102    sessionStorage.removeItem(skipKey);
103    setStatus("Updated just now");
104  } else {
105    run(false);
106  }
107}