@livo-build/charts 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,13 +32,22 @@ export interface PolymarketFeedOptions {
32
32
  host?: string;
33
33
  /** Live poll cadence in ms (default 30s; 0 disables the live poll). */
34
34
  refetchMs?: number;
35
+ /**
36
+ * Optional Livo indexer base (e.g. "https://polymarket.livo.build"). When set (and
37
+ * EventSource is available), the live candle is driven by the indexer's `/stream` SSE
38
+ * — INSTANT updates on every price/trade — instead of the 30s history poll. History
39
+ * still loads from the public CLOB, so this is a pure realtime upgrade. Omit to keep
40
+ * the dependency-free poll-only behavior.
41
+ */
42
+ liveUrl?: string;
35
43
  /** Override "now" (ms) — for tests/determinism. */
36
44
  now?: number;
37
45
  }
38
46
  /**
39
47
  * A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
40
- * candles, plus a polling "live" candle (Polymarket has no public candle socket, so the
41
- * latest bucket is re-fetched on an interval). Price-history has no volume, so candles
42
- * carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds` as the interval.
48
+ * candles, plus a "live" candle. By default the latest bucket is re-fetched on an
49
+ * interval (Polymarket has no public candle socket). Pass `liveUrl` (a Livo indexer) to
50
+ * upgrade the live candle to INSTANT SSE updates. Price-history has no volume, so
51
+ * history candles carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds`.
43
52
  */
44
53
  export declare function polymarketFeed(opts: PolymarketFeedOptions): ChartFeed;
@@ -41,15 +41,17 @@ export async function fetchPolymarketPriceHistory(opts) {
41
41
  }
42
42
  /**
43
43
  * A {@link ChartFeed} for a Polymarket outcome: paged price history bucketed into OHLC
44
- * candles, plus a polling "live" candle (Polymarket has no public candle socket, so the
45
- * latest bucket is re-fetched on an interval). Price-history has no volume, so candles
46
- * carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds` as the interval.
44
+ * candles, plus a "live" candle. By default the latest bucket is re-fetched on an
45
+ * interval (Polymarket has no public candle socket). Pass `liveUrl` (a Livo indexer) to
46
+ * upgrade the live candle to INSTANT SSE updates. Price-history has no volume, so
47
+ * history candles carry `vol: 0`. Pass it to `connectFeed` with `bucketSeconds`.
47
48
  */
48
49
  export function polymarketFeed(opts) {
49
50
  const secs = opts.bucketSeconds ?? 3600;
50
51
  const host = opts.host ?? POLYMARKET_CLOB;
51
52
  const tokenId = opts.tokenId;
52
53
  const fidelity = Math.max(1, Math.round(secs / 60));
54
+ const ES = globalThis.EventSource;
53
55
  return {
54
56
  async loadHistory({ before, limit }) {
55
57
  const endTs = Math.floor((before ?? opts.now ?? Date.now()) / 1000);
@@ -58,35 +60,114 @@ export function polymarketFeed(opts) {
58
60
  return buildOHLC(points, secs);
59
61
  },
60
62
  subscribe(_params, onUpdate) {
61
- const ms = opts.refetchMs ?? 30000;
62
- if (ms <= 0)
63
- return () => { };
64
- let stopped = false;
65
- let timer;
66
- const tick = async () => {
67
- if (stopped)
68
- return;
69
- try {
70
- // re-fetch the last few buckets and emit the most recent candle.
71
- const endTs = Math.floor((opts.now ?? Date.now()) / 1000);
72
- const points = await fetchPolymarketPriceHistory({ tokenId, host, startTs: endTs - secs * 3, endTs, fidelity });
73
- const candles = buildOHLC(points, secs);
74
- const last = candles[candles.length - 1];
75
- if (last && !stopped)
76
- onUpdate(last);
77
- }
78
- catch {
79
- /* transient fetch error — try again next tick */
80
- }
81
- if (!stopped)
82
- timer = setTimeout(tick, ms);
83
- };
84
- timer = setTimeout(tick, ms);
85
- return () => {
86
- stopped = true;
87
- if (timer)
88
- clearTimeout(timer);
63
+ // Poll the latest buckets from the public CLOB (the default + the SSE fallback).
64
+ const startPoll = () => {
65
+ const ms = opts.refetchMs ?? 30000;
66
+ if (ms <= 0)
67
+ return () => { };
68
+ let stopped = false;
69
+ let timer;
70
+ const tick = async () => {
71
+ if (stopped)
72
+ return;
73
+ try {
74
+ const endTs = Math.floor((opts.now ?? Date.now()) / 1000);
75
+ const points = await fetchPolymarketPriceHistory({ tokenId, host, startTs: endTs - secs * 3, endTs, fidelity });
76
+ const candles = buildOHLC(points, secs);
77
+ const last = candles[candles.length - 1];
78
+ if (last && !stopped)
79
+ onUpdate(last);
80
+ }
81
+ catch {
82
+ /* transient fetch error — try again next tick */
83
+ }
84
+ if (!stopped)
85
+ timer = setTimeout(tick, ms);
86
+ };
87
+ timer = setTimeout(tick, ms);
88
+ return () => {
89
+ stopped = true;
90
+ if (timer)
91
+ clearTimeout(timer);
92
+ };
89
93
  };
94
+ // Realtime via the Livo indexer's SSE stream (instant), when configured + available.
95
+ // Falls back to polling if the stream can't connect (e.g. the indexer is down).
96
+ if (opts.liveUrl && ES)
97
+ return subscribeIndexerSse(opts.liveUrl, tokenId, secs, opts.now, onUpdate, ES, startPoll);
98
+ return startPoll();
90
99
  },
91
100
  };
92
101
  }
102
+ /**
103
+ * Drive the live candle from Livo's Polymarket indexer SSE (`/stream`): `price` frames
104
+ * (mid) and `trade` frames (price + size → volume) for our token roll the in-progress
105
+ * bucket. If the stream can't connect (indexer down), it falls back to `startPoll` so the
106
+ * chart still gets a building candle. Returns an unsubscribe fn.
107
+ */
108
+ function subscribeIndexerSse(liveUrl, tokenId, secs, nowOverride, onUpdate, ES, startPoll) {
109
+ let cur = null;
110
+ let gotMessage = false;
111
+ let pollStop;
112
+ const es = new ES(`${liveUrl.replace(/\/$/, "")}/stream`);
113
+ // If the stream errors before ever delivering a frame, it's almost certainly down —
114
+ // close it and fall back to the poll. (A drop AFTER messages auto-reconnects via ES.)
115
+ es.onerror = () => {
116
+ if (!gotMessage && !pollStop) {
117
+ try {
118
+ es.close();
119
+ }
120
+ catch {
121
+ /* already closing */
122
+ }
123
+ pollStop = startPoll();
124
+ }
125
+ };
126
+ const tick = (price, vol = 0) => {
127
+ gotMessage = true;
128
+ if (!(price > 0) || !isFinite(price))
129
+ return;
130
+ const nowSec = Math.floor((nowOverride ?? Date.now()) / 1000);
131
+ const bt = Math.floor(nowSec / secs) * secs;
132
+ if (!cur || bt > cur.time)
133
+ cur = { time: bt, o: price, h: price, l: price, c: price, vol };
134
+ else {
135
+ cur.h = Math.max(cur.h, price);
136
+ cur.l = Math.min(cur.l, price);
137
+ cur.c = price;
138
+ cur.vol += vol;
139
+ }
140
+ onUpdate({ ...cur });
141
+ };
142
+ const onPrice = (ev) => {
143
+ try {
144
+ const d = JSON.parse(ev.data);
145
+ if (d.tokenId === tokenId)
146
+ tick(d.mid || d.last || 0);
147
+ }
148
+ catch {
149
+ /* ignore malformed frame */
150
+ }
151
+ };
152
+ const onTrade = (ev) => {
153
+ try {
154
+ const d = JSON.parse(ev.data);
155
+ if (d.tokenId === tokenId && d.price)
156
+ tick(d.price, d.size || 0);
157
+ }
158
+ catch {
159
+ /* ignore */
160
+ }
161
+ };
162
+ es.addEventListener("price", onPrice);
163
+ es.addEventListener("trade", onTrade);
164
+ return () => {
165
+ try {
166
+ es.close();
167
+ }
168
+ catch {
169
+ /* already closed */
170
+ }
171
+ pollStop?.();
172
+ };
173
+ }
@@ -6,8 +6,13 @@ export interface PolymarketChartProps {
6
6
  bucketSeconds?: number;
7
7
  /** Override the CLOB host. */
8
8
  host?: string;
9
- /** Live poll cadence in ms (default 30s; 0 = fetch once). */
9
+ /** Live poll cadence in ms (default 30s; 0 = fetch once). Ignored when `liveUrl` is set. */
10
10
  refetchMs?: number;
11
+ /**
12
+ * Optional Livo indexer base (e.g. "https://polymarket.livo.build") — upgrades the live
13
+ * candle from the 30s poll to INSTANT SSE updates. History still loads from the CLOB.
14
+ */
15
+ liveUrl?: string;
11
16
  /** Candles per history page; older pages load as you scroll left (default 500). */
12
17
  pageSize?: number;
13
18
  height?: number;
@@ -37,4 +42,4 @@ export interface PolymarketChartProps {
37
42
  * <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
38
43
  * ```
39
44
  */
40
- export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, pageSize, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, label, priceFormat, }: PolymarketChartProps): import("react").JSX.Element;
45
+ export declare function PolymarketChart({ tokenId, bucketSeconds, host, refetchMs, liveUrl, pageSize, height, chartType, indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools, showVolume: showVolumeProp, volumeProfile: volumeProfileProp, theme, toolbar, fitContent, label, priceFormat, }: PolymarketChartProps): import("react").JSX.Element;
@@ -16,7 +16,7 @@ const pct = (v) => `${(v * 100).toFixed(1)}%`;
16
16
  * <PolymarketChart tokenId={yesTokenId} bucketSeconds={3600} label="Yes" />
17
17
  * ```
18
18
  */
19
- export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs, pageSize = 500, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, label, priceFormat = pct, }) {
19
+ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs, liveUrl, pageSize = 500, height = 420, chartType = "line", indicators, oscillators, drawings, onDrawingsChange, hideDrawingTools = false, showVolume: showVolumeProp = false, volumeProfile: volumeProfileProp, theme, toolbar = true, fitContent = true, label, priceFormat = pct, }) {
20
20
  const [secs, setSecs] = useState(bucketSeconds);
21
21
  const [type, setType] = useState(chartType);
22
22
  const [log, setLog] = useState(false);
@@ -70,7 +70,7 @@ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs
70
70
  return;
71
71
  setLoading(true);
72
72
  setError(null);
73
- const feed = polymarketFeed({ tokenId, bucketSeconds: secs, host, refetchMs });
73
+ const feed = polymarketFeed({ tokenId, bucketSeconds: secs, host, refetchMs, liveUrl });
74
74
  const conn = connectFeed(chart.current, feed, {
75
75
  interval: secs,
76
76
  pageSize,
@@ -85,7 +85,7 @@ export function PolymarketChart({ tokenId, bucketSeconds = 3600, host, refetchMs
85
85
  },
86
86
  });
87
87
  return () => conn.disconnect();
88
- }, [tokenId, secs, host, refetchMs, pageSize]);
88
+ }, [tokenId, secs, host, refetchMs, liveUrl, pageSize]);
89
89
  const info = legend ?? last;
90
90
  const lc = info && info.c >= info.o ? th.up : th.down;
91
91
  const { btn, sepStyle, barStyle, muted } = toolbarUi(th);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/charts",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "livo-charts — a lightweight, dependency-free canvas charting library (candlesticks/line/baseline/Heikin-Ashi, zoom/pan, crosshair, MA/RSI/MACD/Stochastic/ATR indicators, volume profile, drawing tools, and live Hyperliquid/Polymarket/Signal-Radar feeds) with a framework-agnostic core and a React wrapper.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",