import {BinanceWsProviderProps} from "./BinanceWsProvider.props";
import {ReadyState, useBinancePrice} from "../../hooks/binancePrices";
import {createContext, useContext, useEffect, useState} from "react";

interface BinanceWsProviderContext {
  futuresSubscribe: (symbol: string) => void;
  futuresUnsubscribe: (symbol: string) => void;
  futuresReadyState: number;
  spotSubscribe: (symbol: string) => void;
  spotUnsubscribe: (symbol: string) => void;
  spotReadyState: number;
}

const defaultContextValue: BinanceWsProviderContext = {
  futuresSubscribe: (symbol: string) => {},
  futuresUnsubscribe: (symbol: string) => {},
  futuresReadyState: 0,
  spotSubscribe: (symbol: string) => {},
  spotUnsubscribe: (symbol: string) => {},
  spotReadyState: 0,
};

const Context = createContext(defaultContextValue);

export function useBinanceWsProvider() {
  return useContext(Context);
}

const BinanceWsProvider = ({children}: BinanceWsProviderProps) => {
  const [futuresSymbols, setFuturesSymbols] = useState<Array<string>>([]);
  const [futuresQueue, setFuturesQueue] = useState<Array<string>>([]);
  const [spotSymbols, setSpotSymbols] = useState<Array<string>>([]);
  const [spotQueue, setSpotQueue] = useState<Array<string>>([]);
  const {
    futuresSubscribe,
    // futuresUnsubscribe,
    futuresReadyState,
    spotSubscribe,
    // spotUnsubscribe,
    spotReadyState,
  } = useBinancePrice();

  const futuresSymbolSubscribe = (symbol: string) => {
    if (futuresSymbols.includes(symbol) || futuresQueue.includes(symbol)) return;

    setFuturesQueue(prev => [...prev, symbol]);
  };

  const futuresSymbolUnsubscribe = (symbol: string) => {
    if (!futuresSymbols.includes(symbol) && !futuresQueue.includes(symbol)) return;

    // TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
    // futuresUnsubscribe(symbol);
    setFuturesQueue(prev => prev.filter(item => item !== symbol));
  };

  const subscribeFromFuturesQueue = () => {
    futuresSubscribe(futuresQueue);
    setFuturesSymbols([...futuresSymbols, ...futuresQueue]);
    setFuturesQueue([]);
  };

  useEffect(() => {
    if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
      subscribeFromFuturesQueue();
    }
  }, [futuresQueue]);

  useEffect(() => {
    if (futuresReadyState === ReadyState.CLOSED) {
      setFuturesQueue([...futuresSymbols, ...futuresQueue]);
      setFuturesSymbols([]);
    }

    if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
      subscribeFromFuturesQueue();
    }
  }, [futuresReadyState]);

  const spotSymbolSubscribe = (symbol: string) => {
    if (spotSymbols.includes(symbol) || spotQueue.includes(symbol)) return;

    setSpotQueue(prev => [...prev, symbol]);
  };

  const spotSymbolUnsubscribe = (symbol: string) => {
    if (!spotSymbols.includes(symbol) && !spotQueue.includes(symbol)) return;

    // TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
    // spotUnsubscribe(symbol);
    setSpotQueue(prev => prev.filter(item => item !== symbol));
  };

  const subscribeFromSpotQueue = () => {
    spotSubscribe(spotQueue);
    setSpotSymbols([...spotSymbols, ...spotQueue]);
    setSpotQueue([]);
  };

  useEffect(() => {
    if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
      subscribeFromSpotQueue();
    }
  }, [spotQueue]);

  useEffect(() => {
    if (spotReadyState === ReadyState.CLOSED) {
      setSpotQueue([...spotSymbols, ...spotQueue]);
      setSpotSymbols([]);
    }

    if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
      subscribeFromSpotQueue();
    }
  }, [spotReadyState]);

  return <Context.Provider
    value={{
      futuresSubscribe: futuresSymbolSubscribe,
      futuresUnsubscribe: futuresSymbolUnsubscribe,
      futuresReadyState: futuresReadyState,
      spotSubscribe: spotSymbolSubscribe,
      spotUnsubscribe: spotSymbolUnsubscribe,
      spotReadyState: spotReadyState,
    }}
  >
    {children}
  </Context.Provider>;
};

export default BinanceWsProvider;

import {useCallback, useEffect, useState} from "react";
import {useDispatch} from "react-redux";
import {setFuturesBinancePriceState, setSpotBinancePriceState} from "../store/binancePriceSlice";

export enum ReadyState {
  UNINSTANTIATED = -1,
  CONNECTING = 0,
  OPEN = 1,
  CLOSING = 2,
  CLOSED = 3,
}

export function useBinancePrice() {
  const stream = "@ticker";
  const dispatch = useDispatch();
  const [futuresWs, setFuturesWs] = useState<WebSocket|null>(null);
  const [futuresReadyState, setFuturesReadyState] = useState(0);
  const [spotWs, setSpotWs] = useState<WebSocket|null>(null);
  const [spotReadyState, setSpotReadyState] = useState(0);

  const connectToWs = () => {
    if ("undefined" === typeof window) {
      setTimeout("connectToWs", 1000);

      return;
    }

    setFuturesWs(new WebSocket("wss://fstream.binance.com/stream"));
    setSpotWs(new WebSocket("wss://stream.binance.com:9443/stream"));
  };

  const futuresSubscribe = useCallback((symbols: Array<string>) => {
    if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;
    const symbolsTicker = symbols.map(symbol => `${symbol.toLowerCase()}@ticker`);

    futuresWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "SUBSCRIBE",
      params: symbolsTicker,
    }));
  }, [futuresReadyState]);

  const futuresUnsubscribe = useCallback((symbols: Array<string>) => {
    if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;
    const symbolsTicker = symbols.map(symbol => `${symbol.toLowerCase()}@ticker`);

    futuresWs.send(JSON.stringify({
      id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
      method: "UNSUBSCRIBE",
      params: symbolsTicker,
    }));
  }, [futuresReadyState]);

  useEffect(() => {
    if (null === futuresWs) return;

    futuresWs.onmessage = (message: MessageEvent) => {
      if (!message.data) return;

      const data = JSON.parse(message.data);

      if (!data?.stream) return;

      dispatch(setFuturesBinancePriceState({symbol: data.stream.replace(stream, ""), price: data.data.c}));
    };

    futuresWs.onopen = () => {
      setFuturesReadyState(ReadyState.OPEN);
    };

    futuresWs.onclose = () => {
      setFuturesReadyState(ReadyState.CLOSED);
    };
  }, [futuresWs]);

  const spotSubscribe = useCallback((symbols: Array<string>) => {
    if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;
    const symbolsTicker = symbols.map(symbol => `${symbol.toLowerCase()}@ticker`);

    spotWs.send(JSON.stringify({
      id: Math.floor((new Date()).getTime() + Math.random() * 10000),
      method: "SUBSCRIBE",
      params: symbolsTicker,
    }));
  }, [spotReadyState]);

  const spotUnsubscribe = useCallback((symbols: Array<string>) => {
    if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;
    const symbolsTicker = symbols.map(symbol => `${symbol.toLowerCase()}@ticker`);

    spotWs.send(JSON.stringify({
      id: Math.floor((new Date()).getTime() + Math.random() * 10000),
      method: "UNSUBSCRIBE",
      params: symbolsTicker,
    }));
  }, [spotReadyState]);

  useEffect(() => {
    if (null === spotWs) return;

    spotWs.onmessage = (message: MessageEvent) => {
      if (!message.data) return;

      const data = JSON.parse(message.data);

      if (!data?.stream) return;

      dispatch(setSpotBinancePriceState({symbol: data.stream.replace(stream, ""), price: data.data.c}));
    };

    spotWs.onopen = () => {
      setSpotReadyState(ReadyState.OPEN);
    };

    spotWs.onclose = () => {
      setSpotReadyState(ReadyState.CLOSED);
    };
  }, [spotWs]);

  useEffect(() => {
    connectToWs();
  }, []);

  return {
    futuresSubscribe,
    futuresUnsubscribe,
    futuresReadyState,
    spotSubscribe,
    spotUnsubscribe,
    spotReadyState,
  };
}


const BinanceLiveCoin = ({symbol, name}: BinanceLiveCoinProps) => {
  const price = useSelector((state: AppState) => state.binancePrice.futures[symbol]) || 0;
  const {futuresSubscribe, futuresUnsubscribe} = useBinanceWsProvider();
  useEffect(() => { 
    futuresSubscribe(symbol);

    return () => futuresUnsubscribe(symbol);
  }, []);

расширить useBinancePrice, добавить туда подписку наnew WebSocket(`wss://fstream.binance.com/ws/${symbol.toLowerCase()}@miniTicker`);  и чтобы получаемые данные сохранялись в стейт.
В BinancePriceSlice добавить futures24h: {[key: string]: {price: number, percent: number}};