import { useData } from '@jaspers-ai/sdk' import { useEffect, useMemo, useRef, useState } from 'react' import { mergeBars, requestsFor, rowsFromText, toBars, type Bar, type HistoryArgs, type Interval } from './plan' /** What one symbol's calls have answered so far. */ export interface FeedResult { bars: Bar[] error: string | null loading: boolean /** A short state for the chip: queued, rate-limited, retrying in 20s, failed, refresh failed. */ note?: string /** The message behind the note, for the chip's tooltip. */ detail?: string } const EMPTY: FeedResult = { bars: [], error: null, loading: true } /** Runs every call for one symbol at one interval and reports the merged bars under "interval:symbol"; draws nothing. */ export function SymbolFeed(props: { symbol: string interval: Interval tick: number onResult: (symbol: string, result: FeedResult) => void }) { const { symbol, interval, tick, onResult } = props // Dates are worked out again on each refresh, so a new day moves the window. const requests = useMemo(() => requestsFor(symbol, interval, new Date()), [symbol, interval, tick]) const [parts, setParts] = useState([]) useEffect(() => setParts([]), [symbol, interval]) useEffect(() => { const done = requests.map((_, i) => parts[i] ?? EMPTY) const bars = mergeBars(done.map((p) => p.bars)) const loading = done.some((p) => p.loading) // Older 1m chunks Yahoo refuses are dropped quietly while any call has bars. const error = bars.length === 0 && !loading ? (done.find((p) => p.error)?.error ?? 'No data') : null onResult(`yahoo:${interval}:${symbol}`, { bars, error, loading }) }, [parts, requests, symbol, interval, onResult]) return ( <> {requests.map((args, i) => ( setParts((prev) => { const next = prev.slice() next[i] = result return next }) } /> ))} ) } /** One history call. Runs again when its arguments change, and on each refresh tick. */ function Chunk(props: { args: HistoryArgs; interval: Interval; tick: number; onDone: (r: FeedResult) => void }) { const { args, interval, tick, onDone } = props const { data, text, error, loading, refetch } = useData('charts/history', { ...args }) const lastTick = useRef(tick) const onDoneRef = useRef(onDone) onDoneRef.current = onDone useEffect(() => { if (lastTick.current === tick) return lastTick.current = tick refetch?.() }, [tick, refetch]) useEffect(() => { const rows = Array.isArray(data) && data.length > 0 ? (data as unknown[]) : rowsFromText(text) const bars = toBars(rows, interval) const pending = !error && (loading || (data === undefined && !text)) const message = error ? String(error).split('\n')[0] : bars.length === 0 && !pending ? (text ?? 'No data').split('\n')[0].slice(0, 160) : null onDoneRef.current({ bars, error: message, loading: pending && bars.length === 0 }) }, [data, text, error, loading, interval]) return null }