1+ // src/services/pythService.ts
2+
3+ type PriceListener = ( price : number ) => void ;
4+
5+ const BASE_URL = import . meta. env . VITE_API_URL ;
6+
7+ if ( ! BASE_URL ) {
8+ throw new Error ( "VITE_API_URL is not defined in .env" ) ;
9+ }
10+
11+ const listeners = new Set < PriceListener > ( ) ;
12+ let intervalId : ReturnType < typeof setInterval > | null = null ;
13+ let currentPrice = 0 ;
14+ let priceHistory : { time : number ; value : number } [ ] = [ ] ;
15+ let failCount = 0 ;
16+
17+ const MAX_HISTORY = 200 ;
18+ const POLL_INTERVAL = 10_000 ; // ✅ 10 segundos base
19+ const MAX_BACKOFF = 60_000 ; // máximo 60 segundos de espera
20+
21+ // ─── API calls ────────────────────────────────────────────────────
22+
23+ export const fetchCurrentPrice = async ( ) : Promise < number > => {
24+ const res = await fetch ( `${ BASE_URL } /api/get-adaprice` ) ;
25+ if ( res . status === 429 ) throw new Error ( "RATE_LIMIT" ) ;
26+ if ( ! res . ok ) throw new Error ( `get-adaprice failed: HTTP ${ res . status } ` ) ;
27+ const data = await res . json ( ) ;
28+ return data . price as number ;
29+ } ;
30+
31+ export const fetchPriceRange = async (
32+ from : number ,
33+ to : number ,
34+ interval = 60
35+ ) : Promise < { time : number ; value : number } [ ] > => {
36+ const res = await fetch (
37+ `${ BASE_URL } /api/get-adaprice-range?from=${ from } &to=${ to } &interval=${ interval } `
38+ ) ;
39+ if ( res . status === 429 ) throw new Error ( "RATE_LIMIT" ) ;
40+ if ( ! res . ok ) throw new Error ( `get-adaprice-range failed: HTTP ${ res . status } ` ) ;
41+ const data = await res . json ( ) ;
42+ return ( data . data as { time : number ; value : number } [ ] ) . map (
43+ ( { time, value } ) => ( { time, value } )
44+ ) ;
45+ } ;
46+
47+ // ─── Getters ──────────────────────────────────────────────────────
48+
49+ export const getPrice = ( ) => currentPrice ;
50+ export const getPriceHistory = ( ) => [ ...priceHistory ] ;
51+
52+ // ─── Init ─────────────────────────────────────────────────────────
53+
54+ export const initHistory = async ( ) => {
55+ try {
56+ const to = Math . floor ( Date . now ( ) / 1000 ) ;
57+ const from = to - 60 * 60 ; // última hora
58+
59+ const [ history , price ] = await Promise . all ( [
60+ fetchPriceRange ( from , to , 15 ) ,
61+ fetchCurrentPrice ( ) ,
62+ ] ) ;
63+
64+ priceHistory = history . slice ( - MAX_HISTORY ) ;
65+ currentPrice = price ;
66+ failCount = 0 ;
67+ } catch ( e ) {
68+ console . error ( "[pythService] Error initializing history:" , e ) ;
69+ }
70+ } ;
71+
72+ // ─── Polling con backoff exponencial ─────────────────────────────
73+
74+ const scheduleNextPoll = ( ) => {
75+ // Backoff: 10s, 20s, 40s... hasta MAX_BACKOFF
76+ const delay = Math . min ( POLL_INTERVAL * Math . pow ( 2 , failCount ) , MAX_BACKOFF ) ;
77+
78+ if ( failCount > 0 ) {
79+ console . warn ( `[pythService] Retrying in ${ delay / 1000 } s (attempt ${ failCount } )` ) ;
80+ }
81+
82+ intervalId = setTimeout ( poll , delay ) ;
83+ } ;
84+
85+ const poll = async ( ) => {
86+ try {
87+ const price = await fetchCurrentPrice ( ) ;
88+ currentPrice = price ;
89+ failCount = 0 ; // reset en éxito
90+
91+ const lastTime = priceHistory [ priceHistory . length - 1 ] ?. time ?? 0 ;
92+ const newTime = Math . max ( Math . floor ( Date . now ( ) / 1000 ) , lastTime + 1 ) ;
93+
94+ priceHistory . push ( { time : newTime , value : + price . toFixed ( 6 ) } ) ;
95+ if ( priceHistory . length > MAX_HISTORY ) priceHistory . shift ( ) ;
96+
97+ listeners . forEach ( fn => fn ( price ) ) ;
98+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99+ } catch ( e : any ) {
100+ failCount ++ ;
101+ if ( e . message === "RATE_LIMIT" ) {
102+ console . warn ( "[pythService] Rate limited — backing off" ) ;
103+ } else {
104+ console . error ( "[pythService] Poll error:" , e ) ;
105+ }
106+ } finally {
107+ // Solo reprogramar si hay listeners activos
108+ if ( listeners . size > 0 ) {
109+ scheduleNextPoll ( ) ;
110+ } else {
111+ intervalId = null ;
112+ }
113+ }
114+ } ;
115+
116+ const startPolling = ( ) => {
117+ if ( intervalId ) return ;
118+ scheduleNextPoll ( ) ;
119+ } ;
120+
121+ const stopPolling = ( ) => {
122+ if ( intervalId ) {
123+ clearTimeout ( intervalId as unknown as ReturnType < typeof setTimeout > ) ;
124+ intervalId = null ;
125+ }
126+ failCount = 0 ;
127+ } ;
128+
129+ // ─── Subscribe ────────────────────────────────────────────────────
130+
131+ export const subscribe = ( fn : PriceListener ) => {
132+ listeners . add ( fn ) ;
133+ startPolling ( ) ;
134+ return ( ) => {
135+ listeners . delete ( fn ) ;
136+ if ( listeners . size === 0 ) stopPolling ( ) ;
137+ } ;
138+ } ;
139+
140+ export const subscribeWithHistory = ( fn : PriceListener ) => subscribe ( fn ) ;
0 commit comments