SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
5 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
1.6 KB · 42 lines typescript
Raw Blame History
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