Skip to content

Commit 8afdbce

Browse files
Merge pull request #4 from Front2FullStack/feature/ws-implementation
Websocket backend config done
2 parents 8cefbd6 + cbc5165 commit 8afdbce

23 files changed

Lines changed: 687 additions & 67 deletions

File tree

.github/instructions/snyk_rules.instructions.md

Lines changed: 0 additions & 14 deletions
This file was deleted.

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ This will start:
2323
- Frontend: http://localhost:3000
2424
- Backend API: http://localhost:3005 (health endpoint: `/health`)
2525

26+
Local development with Docker (hot reload)
27+
28+
Use the development compose file to run both services with live reload and all required env vars configured:
29+
30+
```bash
31+
docker compose -f docker-compose.dev.yml up --build
32+
```
33+
34+
This will start:
35+
36+
- Frontend (dev server): http://localhost:3000
37+
- Backend API (dev): http://localhost:3005
38+
- WebSocket (dev): ws://localhost:8080
39+
2640
Local development (without Docker)
2741

2842
- Frontend:
@@ -47,6 +61,46 @@ Tests
4761
- Frontend E2E: run Cypress after starting the frontend with `npm run cypress:open` for local `npm run cypress:run` for CI/CD pipelines
4862
- Backend unit test run in `server/market-trading-service` with `npm run test` (Jest), `npm run test:watch`, `npm run test:coverage` also available if working on test and see the coverage
4963

64+
## Assumptions & Trade-offs
65+
66+
### Assumptions
67+
68+
- Data model: simplified ticker domain (price, change, volume, 24h high/low); no corporate actions, splits, multi-currency, or latency-sensitive guarantees.
69+
70+
- Simulation: price history and live ticks are synthetic for UX validation, not financial accuracy.
71+
72+
- Environment: single-node backend with in-memory storage is acceptable for this challenge; no cross-process persistence required.
73+
74+
- Client: no auth is required to view market data.
75+
76+
- Contracts: WebSocket message shapes are minimal and stable: `{"type":"connected","payload":{"clientId"}}` and `{"type":"data","payload":{"ticker"}}`. REST endpoints are reachable for initial bootstrap/fallback.
77+
78+
- CI/CD via github actions to run test on PR
79+
80+
- Used Code Rabbit to review
81+
82+
### Trade-offs
83+
84+
- Real-time delivery vs simplicity: WebSocket for live updates plus a one-time REST bootstrap for fast first paint (slight duplication accepted for responsiveness).
85+
86+
- Subscription scope: initial subscribe-to-all for clarity; future optimization could subscribe only to visible/selected symbols.
87+
88+
- Update cadence: immediate per-tick broadcasts, no batching/debouncing; simple but more frames under heavy load. Batching window (e.g., 50–150 ms) can be added later.
89+
90+
- State storage: in-memory repository and subscription registry (per-process); not horizontally scalable without a shared store/pub-sub or sticky sessions.
91+
92+
- Consistency vs responsiveness: values rounded for display and updated on each `data` frame; suited for UI, not for reconciliation.
93+
94+
- Error handling: if REST fails, fall back to mock data; if WS drops, auto-reconnect and keep last-known data. Prioritizes resilience for demos.
95+
96+
- Client architecture: Next.js app with a WebSocket Provider context and React Query; SSR of live data is out of scope to keep the real-time logic client-side and testable.
97+
98+
- Testing: REST fallback and provider no-op defaults keep unit tests stable; deep WS integration tests are limited and can be added with a small WS mock.
99+
100+
- Selection model: selection tracked by symbol and derived from the latest ticker list to ensure live updates without stale references.
101+
102+
- Some of the Bonus features aren't covered due to time constrain.
103+
50104
License
51105

52106
This project is available under the repository LICENSE file.

client/trading-dashboard/app/(dashboard)/dashboard/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const TradingDashboard = () => {
3737
});
3838
};
3939

40+
41+
4042
const handleSelectTicker = (ticker: Ticker) => {
4143
setSelectedTicker(ticker);
4244
setSidebarOpen(false); // Close sidebar on mobile after selection

client/trading-dashboard/app/Providers.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
44
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
55
import { useState } from "react";
6+
import { WebSocketProvider } from "@/providers/WebSocketProvider";
67

78
export default function Providers({ children }: { children: React.ReactNode }) {
89
const [queryClient] = useState(
@@ -19,7 +20,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
1920

2021
return (
2122
<QueryClientProvider client={queryClient}>
22-
{children}
23+
<WebSocketProvider>{children}</WebSocketProvider>
2324
<ReactQueryDevtools initialIsOpen={false} />
2425
</QueryClientProvider>
2526
);

client/trading-dashboard/components/Footer.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { BarChart3 } from "lucide-react";
2-
31
const Footer = () => {
42
const year = new Date().getFullYear();
53
return (

client/trading-dashboard/components/TickerGrid.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import TickerCard from "./TickerCard";
33
import { Ticker } from "@/types";
44
import { useQuery } from "@tanstack/react-query";
55
import { API_BASE_URL, MOCK_TICKERS } from "@/constants";
6-
import { LoadingSpinner } from "./ui/LoadingSpinner";
6+
import { useWebSocketContext } from "@/providers/WebSocketProvider";
77

88
async function getPosts(): Promise<Ticker[]> {
99
const res = await fetch(`${API_BASE_URL}/tickers`);
@@ -18,14 +18,21 @@ async function getPosts(): Promise<Ticker[]> {
1818
return tickers;
1919
}
2020
const TickerGrid = () => {
21+
const { tickers: wsTickers } = useWebSocketContext();
22+
const useFallback = !wsTickers || wsTickers.length === 0;
2123
const { data, error } = useQuery<Ticker[], Error>({
2224
queryKey: ["tickers"],
2325
queryFn: getPosts,
24-
refetchInterval: 1000,
26+
refetchInterval: useFallback ? 1000 : false,
2527
placeholderData: MOCK_TICKERS,
28+
enabled: useFallback,
2629
});
2730

28-
if (error) {
31+
const tickersToShow: Ticker[] = useFallback
32+
? data ?? MOCK_TICKERS
33+
: wsTickers;
34+
35+
if (error && useFallback) {
2936
console.error("Error fetching tickers:", error);
3037
}
3138

@@ -45,9 +52,9 @@ const TickerGrid = () => {
4552
data-testid="ticker-grid"
4653
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
4754
>
48-
{data &&
49-
data.length > 0 &&
50-
data?.map((ticker) => (
55+
{tickersToShow &&
56+
tickersToShow.length > 0 &&
57+
tickersToShow.map((ticker) => (
5158
<TickerCard key={ticker.symbol} {...ticker} />
5259
))}
5360
</div>

client/trading-dashboard/constants/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
// All constants
22
import { Ticker } from "@/types";
33

4-
export const API_BASE_URL =
5-
`${process.env.NEXT_PUBLIC_MARKET_TRADING_URL}/api` ||
6-
"http://localhost:3005/api";
4+
// Ensure sane defaults even when env vars are missing at build time
5+
const PUBLIC_API_BASE =
6+
process.env.NEXT_PUBLIC_MARKET_TRADING_URL &&
7+
process.env.NEXT_PUBLIC_MARKET_TRADING_URL.trim() !== ""
8+
? process.env.NEXT_PUBLIC_MARKET_TRADING_URL
9+
: "http://localhost:3005";
10+
export const API_BASE_URL = `${PUBLIC_API_BASE}/api`;
11+
12+
export const WS_URL =
13+
process.env.NEXT_PUBLIC_WS_URL && process.env.NEXT_PUBLIC_WS_URL.trim() !== ""
14+
? process.env.NEXT_PUBLIC_WS_URL
15+
: "ws://localhost:8080";
716

817
export const MOCK_TICKERS: Ticker[] = [
918
{
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
NEXT_PUBLIC_BASE_URL=http://localhost:3000
2-
NEXT_PUBLIC_MARKET_TRADING_URL=http://localhost:3005
2+
NEXT_PUBLIC_MARKET_TRADING_URL=http://localhost:3005
3+
NEXT_PUBLIC_WS_URL=ws://localhost:8080

client/trading-dashboard/hooks/useTradingData.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import { useState, useRef, useEffect } from "react";
1+
import { useState, useRef, useEffect, useMemo } from "react";
22
import { useQuery } from "@tanstack/react-query";
33
import { Ticker, HistoricalData } from "@/types";
44
import { API_BASE_URL, MOCK_TICKERS } from "@/constants";
55
import { generateMockHistory } from "@/lib/utils";
6+
import { useWebSocketContext } from "@/providers/WebSocketProvider";
67

78
export const useTradingData = () => {
8-
const [selectedTicker, setSelectedTicker] = useState<Ticker | null>(null);
9+
const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
910
const [chartDays, setChartDays] = useState<number>(7);
1011
const [error, setError] = useState<string | null>(null);
1112
const priceStatus = useRef<{ [key: string]: "up" | "down" | "neutral" }>({});
1213
const previousTickers = useRef<Ticker[]>([]);
1314

14-
// Fetch tickers with automatic polling every 2 seconds for tickers
15-
const { data: tickers = [], isLoading: tickersLoading } = useQuery({
15+
const { tickers: wsTickers, priceStatus: wsPriceStatus } =
16+
useWebSocketContext();
17+
18+
// REST fallback only if WS not yet providing data
19+
const useFallback = !wsTickers || wsTickers.length === 0;
20+
const { data: restTickers = [], isLoading: restLoading } = useQuery({
1621
queryKey: ["tickers"],
1722
queryFn: async () => {
1823
try {
@@ -29,12 +34,21 @@ export const useTradingData = () => {
2934
return MOCK_TICKERS;
3035
}
3136
},
32-
refetchInterval: 2000,
37+
enabled: useFallback,
38+
refetchInterval: useFallback ? 2000 : false,
3339
staleTime: 0,
3440
});
3541

36-
// Track price changes for animations
42+
const tickers: Ticker[] = useFallback ? restTickers : wsTickers;
43+
44+
// Track price changes for animations (fallback when WS not in use)
3745
useEffect(() => {
46+
if (!useFallback) {
47+
// When using WS, price status is provided by the provider
48+
priceStatus.current = wsPriceStatus;
49+
return;
50+
}
51+
3852
if (previousTickers.current.length > 0 && tickers.length > 0) {
3953
tickers.forEach((newTicker) => {
4054
const oldTicker = previousTickers.current.find(
@@ -50,14 +64,20 @@ export const useTradingData = () => {
5064
});
5165
}
5266
previousTickers.current = tickers;
53-
}, [tickers]);
67+
}, [tickers, useFallback, wsPriceStatus]);
5468

55-
// Set initial selected ticker
69+
// Set initial selected symbol
5670
useEffect(() => {
57-
if (!selectedTicker && tickers.length > 0) {
58-
setSelectedTicker(tickers[0]);
71+
if (!selectedSymbol && tickers.length > 0) {
72+
setSelectedSymbol(tickers[0].symbol);
5973
}
60-
}, [tickers, selectedTicker]);
74+
}, [tickers, selectedSymbol]);
75+
76+
// Derive the selected ticker from the latest tickers list so it always stays fresh
77+
const selectedTicker: Ticker | null = useMemo(() => {
78+
if (!selectedSymbol) return null;
79+
return tickers.find((t) => t.symbol === selectedSymbol) ?? null;
80+
}, [tickers, selectedSymbol]);
6181

6282
// Fetch chart data when ticker or days change
6383
const { data: chartData = [], isLoading: chartLoading } = useQuery({
@@ -83,11 +103,14 @@ export const useTradingData = () => {
83103
return {
84104
tickers,
85105
selectedTicker,
86-
setSelectedTicker,
106+
setSelectedTicker: (ticker: Ticker) => setSelectedSymbol(ticker.symbol),
87107
chartData,
88108
chartDays,
89109
setChartDays,
90-
loading: { tickers: tickersLoading, chart: chartLoading },
110+
loading: {
111+
tickers: useFallback ? restLoading : false,
112+
chart: chartLoading,
113+
},
91114
error,
92115
priceStatus: priceStatus.current,
93116
};

client/trading-dashboard/package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)