Skip to content

Commit 70dc5af

Browse files
committed
feat(fullstack): integrate frontend dashboard with backend Pyth service + dynamic ADA price handling
1 parent 58eef0f commit 70dc5af

5 files changed

Lines changed: 145 additions & 13 deletions

File tree

backend/models/server.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,7 @@ class Server{
2828
}
2929

3030
middlewares(){
31-
const limiter = rateLimit({
32-
windowMs: 15 * 60 * 1000,
33-
max: 100,
34-
message: '🚫 Demasiadas peticiones desde esta IP. Intenta más tarde.',
35-
standardHeaders: true,
36-
legacyHeaders: false,
37-
});
38-
39-
this.app.use(limiter);
31+
4032
this.app.use(cors());
4133
this.app.use(express.json({ limit: '25mb' }));
4234
this.app.use(express.urlencoded({ extended: true, limit: '25mb' }));

frontend/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ node_modules
1111
dist
1212
dist-ssr
1313
*.local
14-
14+
.env
1515
# Editor directories and files
1616
.vscode/*
1717
!.vscode/extensions.json

frontend/src/components/Chart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createChart, LineSeries, CandlestickSeries } from "lightweight-charts";
22
import type { IChartApi, ISeriesApi, Time } from "lightweight-charts";
33
import { useEffect, useRef, useState } from "react";
4-
import { subscribeWithHistory, getPriceHistory } from "../services/pythMock";
4+
import { subscribeWithHistory, getPriceHistory } from "../services/pythService";
55

66
type ChartType = "line" | "candle";
77

frontend/src/pages/Dashboard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Wallet from "../components/Wallet";
22
import Chart from "../components/Chart";
33
import NFTCard from "../components/NFTCard";
4-
import { subscribe, getPrice } from "../services/pythMock";
4+
import { subscribe, getPrice } from "../services/pythService";
55
import { useState, useEffect } from "react";
66

77
const ff =
@@ -225,7 +225,7 @@ export default function Dashboard() {
225225
Price History
226226
</h2>
227227
<p style={{ fontSize: 12, color: "#98989d", marginTop: 3 }}>
228-
ADA / USD · Live simulation
228+
ADA / USD · Live
229229
</p>
230230
</div>
231231

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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

Comments
 (0)