spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { useEffect, useRef, useState } from "react";34export type SseEvent<T = unknown> = { type: string; data: T[]; received_at: number };56/** Subscribe to the market-data SSE relay. Handlers are kept in a ref so re-renders don't reconnect. */7export function useMarketStream(symbols: string[], channels: string[], handlers: Partial<Record<string, (ev: SseEvent<Record<string, unknown>>) => void>>) {8 const [connected, setConnected] = useState(false);9 const ref = useRef(handlers);10 useEffect(() => {11 ref.current = handlers;12 });13 const key = `${symbols.join(",")}|${channels.join(",")}`;14 useEffect(() => {15 if (!symbols.length) return;16 const url = `/api/market/stream?symbols=${encodeURIComponent(symbols.join(","))}&channels=${encodeURIComponent(channels.join(","))}`;17 const es = new EventSource(url, { withCredentials: true });18 es.onopen = () => setConnected(true);19 es.onerror = () => setConnected(false);20 const listeners: [string, (e: MessageEvent) => void][] = [];21 for (const ch of channels) {22 const fn = (e: MessageEvent) => {23 try {24 const parsed = JSON.parse(e.data) as SseEvent<Record<string, unknown>>;25 ref.current[ch]?.(parsed);26 } catch {27 /* ignore malformed */28 }29 };30 es.addEventListener(ch, fn);31 listeners.push([ch, fn]);32 }33 return () => {34 for (const [ch, fn] of listeners) es.removeEventListener(ch, fn);35 es.close();36 setConnected(false);37 };38 // eslint-disable-next-line react-hooks/exhaustive-deps39 }, [key]);40 return connected;41}42