1- /* eslint-disable @typescript-eslint/no-explicit-any */
2- import { useEffect , useRef , useState , useCallback } from 'react' ;
1+ import { useEffect , useRef , useState } from 'react' ;
32
43const WS_BASE = `${ process . env . NEXT_PUBLIC_COINGECKO_WEBSOCKET_URL } ?x_cg_pro_api_key=${ process . env . NEXT_PUBLIC_COINGECKO_API_KEY } ` ;
54
@@ -13,157 +12,131 @@ export function useCoinGeckoWebSocket({
1312 const [ price , setPrice ] = useState < ExtendedPriceData | null > ( null ) ;
1413 const [ trades , setTrades ] = useState < Trade [ ] > ( [ ] ) ;
1514 const [ ohlcv , setOhlcv ] = useState < OHLCData | null > ( null ) ;
16- const lastOhlcvTimestamp = useRef < number > ( 0 ) ;
1715
1816 const [ isWsReady , setIsWsReady ] = useState ( false ) ;
1917
20- const handleMessage = useCallback ( ( event : MessageEvent ) => {
21- const ws = wsRef . current ;
22- const msg : WebSocketMessage = JSON . parse ( event . data ) ;
23-
24- // Ping/Pong to keep connection alive
25- if ( msg . type === 'ping' ) return ws ?. send ( JSON . stringify ( { type : 'pong' } ) ) ;
26-
27- // Confirm subscription
28- if ( msg . type === 'confirm_subscription' ) {
29- const { channel } = JSON . parse ( msg ?. identifier ?? '' ) ;
30- subscribed . current . add ( channel ) ;
31- return ;
32- }
33-
34- // C1: Price updates
35- if ( msg . c === 'C1' ) {
36- setPrice ( {
37- usd : msg . p ?? 0 ,
38- coin : msg . i ,
39- price : msg . p ,
40- change24h : msg . pp ,
41- marketCap : msg . m ,
42- volume24h : msg . v ,
43- timestamp : msg . t ,
44- } ) ;
45- }
46-
47- // G2: Trade updates
48- if ( msg . c === 'G2' ) {
49- const newTrade : Trade = {
50- price : msg . pu ,
51- value : msg . vo ,
52- timestamp : msg . t ?? 0 ,
53- type : msg . ty ,
54- amount : msg . to ,
55- } ;
56-
57- setTrades ( ( prev ) => [ newTrade , ...prev ] . slice ( 0 , 7 ) ) ;
58- }
59- // G3: OHLCV updates
60- if ( msg . ch === 'G3' ) {
61- const timestamp = msg . t || 0 ; // already in seconds
62- const newCandle : OHLCData = [
63- timestamp ,
64- Number ( msg . o ?? 0 ) ,
65- Number ( msg . h ?? 0 ) ,
66- Number ( msg . l ?? 0 ) ,
67- Number ( msg . c ?? 0 ) ,
68- ] ;
69-
70- // Always update with the latest candle - chart will handle deduplication
71- setOhlcv ( newCandle ) ;
72- lastOhlcvTimestamp . current = timestamp ;
73- }
74- } , [ ] ) ;
75-
7618 // WebSocket connection
7719 useEffect ( ( ) => {
7820 const ws = new WebSocket ( WS_BASE ) ;
7921 wsRef . current = ws ;
22+ const send = ( payload : Record < string , unknown > ) =>
23+ ws . send ( JSON . stringify ( payload ) ) ;
8024
81- ws . onopen = ( ) => setIsWsReady ( true ) ;
82- ws . onmessage = handleMessage ;
83- ws . onclose = ( ) => setIsWsReady ( false ) ;
25+ const handleMessage = ( event : MessageEvent ) => {
26+ const msg : WebSocketMessage = JSON . parse ( event . data ) ;
8427
85- return ( ) => ws . close ( ) ;
86- } , [ handleMessage ] ) ;
28+ if ( msg . type === 'ping' ) {
29+ send ( { type : 'pong' } ) ;
30+ return ;
31+ }
8732
88- // Subscribe helper
89- const subscribe = useCallback (
90- ( channel : string , data ?: Record < string , any > ) => {
91- const ws = wsRef . current ;
92- if ( ! ws || ! isWsReady || subscribed . current . has ( channel ) ) return ;
33+ if ( msg . type === 'confirm_subscription' ) {
34+ const { channel } = JSON . parse ( msg ?. identifier ?? '' ) ;
35+ subscribed . current . add ( channel ) ;
36+ return ;
37+ }
9338
94- ws . send (
95- JSON . stringify ( {
96- command : 'subscribe' ,
97- identifier : JSON . stringify ( { channel } ) ,
98- } )
99- ) ;
39+ if ( msg . c === 'C1' ) {
40+ setPrice ( {
41+ usd : msg . p ?? 0 ,
42+ coin : msg . i ,
43+ price : msg . p ,
44+ change24h : msg . pp ,
45+ marketCap : msg . m ,
46+ volume24h : msg . v ,
47+ timestamp : msg . t ,
48+ } ) ;
49+ }
10050
101- if ( data ) {
102- ws . send (
103- JSON . stringify ( {
104- command : 'message' ,
105- identifier : JSON . stringify ( { channel } ) ,
106- data : JSON . stringify ( data ) ,
107- } )
108- ) ;
51+ if ( msg . c === 'G2' ) {
52+ const newTrade : Trade = {
53+ price : msg . pu ,
54+ value : msg . vo ,
55+ timestamp : msg . t ?? 0 ,
56+ type : msg . ty ,
57+ amount : msg . to ,
58+ } ;
59+
60+ setTrades ( ( prev ) => [ newTrade , ...prev ] . slice ( 0 , 7 ) ) ;
10961 }
110- } ,
111- [ isWsReady ]
112- ) ;
11362
114- const unsubscribeAll = useCallback ( ( ) => {
115- const ws = wsRef . current ;
116- subscribed . current . forEach ( ( channel ) => {
117- ws ?. send (
118- JSON . stringify ( {
119- command : 'unsubscribe' ,
120- identifier : JSON . stringify ( { channel } ) ,
121- } )
122- ) ;
123- } ) ;
124- subscribed . current . clear ( ) ;
63+ if ( msg . ch === 'G3' ) {
64+ const timestamp = msg . t || 0 ;
65+ const newCandle : OHLCData = [
66+ timestamp ,
67+ Number ( msg . o ?? 0 ) ,
68+ Number ( msg . h ?? 0 ) ,
69+ Number ( msg . l ?? 0 ) ,
70+ Number ( msg . c ?? 0 ) ,
71+ ] ;
72+
73+ setOhlcv ( newCandle ) ;
74+ }
75+ } ;
76+
77+ ws . onopen = ( ) => setIsWsReady ( true ) ;
78+ ws . onmessage = handleMessage ;
79+ ws . onclose = ( ) => setIsWsReady ( false ) ;
80+
81+ return ( ) => ws . close ( ) ;
12582 } , [ ] ) ;
12683
12784 // Subscribe on connection ready
12885 useEffect ( ( ) => {
12986 if ( ! isWsReady ) return ;
87+ const ws = wsRef . current ;
88+ if ( ! ws ) return ;
89+ const send = ( payload : Record < string , unknown > ) =>
90+ ws . send ( JSON . stringify ( payload ) ) ;
13091
131- let active = true ;
92+ const unsubscribeAll = ( ) => {
93+ subscribed . current . forEach ( ( channel ) => {
94+ send ( {
95+ command : 'unsubscribe' ,
96+ identifier : JSON . stringify ( { channel } ) ,
97+ } ) ;
98+ } ) ;
99+ subscribed . current . clear ( ) ;
100+ } ;
132101
133- ( async ( ) => {
134- if ( ! active ) return ;
102+ const subscribe = ( channel : string , data ?: Record < string , unknown > ) => {
103+ if ( subscribed . current . has ( channel ) ) return ;
135104
136- // Reset state
105+ send ( { command : 'subscribe' , identifier : JSON . stringify ( { channel } ) } ) ;
106+
107+ if ( data ) {
108+ send ( {
109+ command : 'message' ,
110+ identifier : JSON . stringify ( { channel } ) ,
111+ data : JSON . stringify ( data ) ,
112+ } ) ;
113+ }
114+ } ;
115+
116+ queueMicrotask ( ( ) => {
137117 setPrice ( null ) ;
138118 setTrades ( [ ] ) ;
139119 setOhlcv ( null ) ;
140- lastOhlcvTimestamp . current = 0 ;
141-
142- unsubscribeAll ( ) ;
143-
144- // Subscribe channels
145- subscribe ( 'CGSimplePrice' , { coin_id : [ coinId ] , action : 'set_tokens' } ) ;
120+ } ) ;
146121
147- const wsPools = [ poolId . replace ( '_' , ':' ) ] ;
122+ unsubscribeAll ( ) ;
148123
149- if ( wsPools . length ) {
150- subscribe ( 'OnchainTrade' , {
151- 'network_id:pool_addresses' : wsPools ,
152- action : 'set_pools' ,
153- } ) ;
124+ subscribe ( 'CGSimplePrice' , { coin_id : [ coinId ] , action : 'set_tokens' } ) ;
154125
155- subscribe ( 'OnchainOHLCV' , {
156- 'network_id:pool_addresses' : wsPools ,
157- interval : '1s' ,
158- action : 'set_pools' ,
159- } ) ;
160- }
161- } ) ( ) ;
126+ const poolAddress = poolId . replace ( '_' , ':' ) ;
127+ if ( poolAddress ) {
128+ subscribe ( 'OnchainTrade' , {
129+ 'network_id:pool_addresses' : [ poolAddress ] ,
130+ action : 'set_pools' ,
131+ } ) ;
162132
163- return ( ) => {
164- active = false ;
165- } ;
166- } , [ coinId , poolId , isWsReady , subscribe , unsubscribeAll ] ) ;
133+ subscribe ( 'OnchainOHLCV' , {
134+ 'network_id:pool_addresses' : [ poolAddress ] ,
135+ interval : '1s' ,
136+ action : 'set_pools' ,
137+ } ) ;
138+ }
139+ } , [ coinId , poolId , isWsReady ] ) ;
167140
168141 return {
169142 price,
0 commit comments