/** * MarketDataProvider —— 行情数据提供者(R-005 服务端中转架构,2026-08-31) * * 架构(服务端中转,老师确认): * - DSH 服务端做「一次 WS 订阅 + QMT 桥整合」,维护一份实时行情缓存(MarketDataHub + 持久化); * - 前端**统一轮询** DSH 接口(/odl/api/market-snapshot,按 code 查询),不做前端直连; * - 轮询间隔 5s(老师确认)。 * * 关注列表(2026-08-31 去重优化):不再自拉 positions, * 改由表格组件在渲染时通过 registerCodes(codes) 上报,Provider 只轮询行情。 * * 2026-09-01 修复:registerCodes 上报新 code 后**立即触发一次行情拉取**(不等 5s 定时器), * 解决「打开持仓 tab 价格要等很久才出现」的时序问题。 */ import { createContext, useContext, useEffect, useReducer, useRef, useState, useCallback } from 'react'; import { useRpc } from '../views/connection.jsx'; const MarketContext = createContext(null); /** 连接状态(轮询模式) */ export const MARKET_STATUS = { IDLE: 'idle', CONNECTING: 'connecting', LIVE: 'live', RECONNECTING: 'reconnecting', DISCONNECTED: 'disconnected', }; /** 价格 reducer:code → snapshot */ function pricesReducer(state, action) { if (action.type === 'update') { const next = {}; for (const [code, snap] of Object.entries(action.data || {})) { if (!snap) continue; next[code] = { ...state[code], ...snap }; } return { ...state, ...next }; } return state; } const POLL_INTERVAL_MS = 5000; // 轮询间隔(老师确认 5s) export function MarketDataProvider({ children }) { const call = useRpc(); /** 统一 setStatus:同步 ref(供轮询闭包读取最新值) */ const updateStatus = useCallback((s) => { statusRef.current = s; setStatus(s); }, []); const [prices, dispatch] = useReducer(pricesReducer, {}); const [status, setStatus] = useState(MARKET_STATUS.IDLE); const statusRef = useRef(MARKET_STATUS.IDLE); const mountedRef = useRef(true); const watchCodesRef = useRef(new Set()); const pollMarketRef = useRef(null); // 供 registerCodes 触发立即拉取 /** 轮询行情缓存(按关注 code 查询,服务端中转) */ const pollMarket = useCallback(async () => { const codes = [...watchCodesRef.current]; if (codes.length === 0) return; try { const res = await call('one-divine-lot/market-snapshot', { args: { codes }, }); if (res?.ok && res.value) { const n = Object.keys(res.value).length; if (n > 0) { dispatch({ type: 'update', data: res.value }); updateStatus(MARKET_STATUS.LIVE); } else { updateStatus(MARKET_STATUS.CONNECTING); // 缓存未填充 } } else { console.warn('[odl-mkt] market-snapshot 失败:', JSON.stringify(res).slice(0, 200)); updateStatus(MARKET_STATUS.RECONNECTING); } } catch (e) { console.warn('[odl-mkt] market-snapshot 异常:', e?.message); updateStatus(MARKET_STATUS.RECONNECTING); } }, [call]); /** 表格上报关注代码;上报新 code 后立即拉一次行情(不等定时器) */ const registerCodes = useCallback((codes) => { const list = Array.isArray(codes) ? codes : []; const next = new Set(watchCodesRef.current); let changed = false; for (const c of list) { if (c && !next.has(c)) { next.add(c); changed = true; } } if (changed) { watchCodesRef.current = next; // 立即触发一次行情拉取(首屏快速显示价格) pollMarketRef.current?.(); } }, []); // 同步 pollMarket 到 ref(供 registerCodes 调用) useEffect(() => { pollMarketRef.current = pollMarket; }, [pollMarket]); /** 主循环:定时轮询行情(关注 code 由表格上报) */ useEffect(() => { mountedRef.current = true; let cancelled = false; const poll = async () => { if (cancelled || !mountedRef.current) return; if (watchCodesRef.current.size === 0) { // 关注代码尚未上报(表格未加载)——保持 IDLE if (statusRef.current !== MARKET_STATUS.IDLE) updateStatus(MARKET_STATUS.IDLE); return; } await pollMarket(); }; poll(); const timer = setInterval(poll, POLL_INTERVAL_MS); return () => { cancelled = true; mountedRef.current = false; clearInterval(timer); }; }, [pollMarket]); const value = { prices, status, getPrice: useCallback((code) => prices[code] ?? null, [prices]), registerCodes, wsInfo: null, }; return ( {children} ); } /** 消费价格 Map */ export function useMarket() { return useContext(MarketContext); }