212 lines
No EOL
5.4 KiB
TypeScript
212 lines
No EOL
5.4 KiB
TypeScript
import { writable } from 'svelte/store';
|
|
import { browser } from '$app/environment';
|
|
import { PUBLIC_WEBSOCKET_URL } from '$env/static/public';
|
|
|
|
export interface LiveTrade {
|
|
type: 'BUY' | 'SELL';
|
|
username: string;
|
|
amount: number;
|
|
coinSymbol: string;
|
|
coinName?: string;
|
|
coinIcon?: string;
|
|
totalValue: number;
|
|
price: number;
|
|
timestamp: number;
|
|
userId: string;
|
|
userImage?: string;
|
|
}
|
|
|
|
// Constants
|
|
const WEBSOCKET_URL = PUBLIC_WEBSOCKET_URL;
|
|
const RECONNECT_DELAY = 5000;
|
|
const MAX_LIVE_TRADES = 5;
|
|
const MAX_ALL_TRADES = 100;
|
|
|
|
// WebSocket state
|
|
let socket: WebSocket | null = null;
|
|
let reconnectTimer: NodeJS.Timeout | null = null;
|
|
let activeCoin: string = '@global';
|
|
|
|
// Stores
|
|
export const liveTradesStore = writable<LiveTrade[]>([]);
|
|
export const allTradesStore = writable<LiveTrade[]>([]);
|
|
export const isConnectedStore = writable<boolean>(false);
|
|
export const isLoadingTrades = writable<boolean>(false);
|
|
|
|
// Comment callbacks
|
|
const commentSubscriptions = new Map<string, (message: any) => void>();
|
|
|
|
async function loadInitialTrades(): Promise<void> {
|
|
if (!browser) return;
|
|
|
|
isLoadingTrades.set(true);
|
|
|
|
try {
|
|
const [largeTradesResponse, allTradesResponse] = await Promise.all([
|
|
fetch('/api/trades/recent?limit=5&minValue=1000'),
|
|
fetch('/api/trades/recent?limit=100')
|
|
]);
|
|
|
|
if (largeTradesResponse.ok) {
|
|
const { trades } = await largeTradesResponse.json();
|
|
liveTradesStore.set(trades);
|
|
}
|
|
|
|
if (allTradesResponse.ok) {
|
|
const { trades } = await allTradesResponse.json();
|
|
allTradesStore.set(trades);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load initial trades:', error);
|
|
} finally {
|
|
isLoadingTrades.set(false);
|
|
}
|
|
}
|
|
|
|
function clearReconnectTimer(): void {
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
}
|
|
|
|
function scheduleReconnect(): void {
|
|
clearReconnectTimer();
|
|
reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
|
|
}
|
|
|
|
function isSocketConnected(): boolean {
|
|
return socket?.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
function isSocketConnecting(): boolean {
|
|
return socket?.readyState === WebSocket.CONNECTING;
|
|
}
|
|
|
|
function sendMessage(message: object): void {
|
|
if (isSocketConnected()) {
|
|
socket!.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
function subscribeToChannels(): void {
|
|
sendMessage({ type: 'subscribe', channel: 'trades:all' });
|
|
sendMessage({ type: 'subscribe', channel: 'trades:large' });
|
|
sendMessage({ type: 'set_coin', coinSymbol: activeCoin });
|
|
}
|
|
|
|
function handleTradeMessage(message: any): void {
|
|
const trade: LiveTrade = {
|
|
...message.data,
|
|
timestamp: Number(message.data.timestamp)
|
|
};
|
|
|
|
if (message.type === 'live-trade') {
|
|
liveTradesStore.update(trades => [trade, ...trades.slice(0, MAX_LIVE_TRADES - 1)]);
|
|
} else if (message.type === 'all-trades') {
|
|
allTradesStore.update(trades => [trade, ...trades.slice(0, MAX_ALL_TRADES - 1)]);
|
|
}
|
|
}
|
|
|
|
function handleCommentMessage(message: any): void {
|
|
const callback = commentSubscriptions.get(activeCoin);
|
|
if (callback) {
|
|
callback(message);
|
|
}
|
|
}
|
|
|
|
function handleWebSocketMessage(event: MessageEvent): void {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
|
|
switch (message.type) {
|
|
case 'live-trade':
|
|
case 'all-trades':
|
|
handleTradeMessage(message);
|
|
break;
|
|
|
|
case 'ping':
|
|
sendMessage({ type: 'pong' });
|
|
break;
|
|
|
|
case 'new_comment':
|
|
case 'comment_liked':
|
|
handleCommentMessage(message);
|
|
break;
|
|
|
|
default:
|
|
console.log('Unhandled message type:', message.type, message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to process WebSocket message:', error);
|
|
}
|
|
}
|
|
|
|
function connect(): void {
|
|
if (!browser) return;
|
|
|
|
// Don't connect if already connected or connecting
|
|
if (isSocketConnected() || isSocketConnecting()) {
|
|
return;
|
|
}
|
|
|
|
clearReconnectTimer();
|
|
|
|
socket = new WebSocket(WEBSOCKET_URL);
|
|
|
|
loadInitialTrades();
|
|
|
|
socket.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
isConnectedStore.set(true);
|
|
clearReconnectTimer();
|
|
subscribeToChannels();
|
|
};
|
|
|
|
socket.onmessage = handleWebSocketMessage;
|
|
|
|
socket.onclose = (event) => {
|
|
console.log(`WebSocket disconnected. Code: ${event.code}`);
|
|
isConnectedStore.set(false);
|
|
socket = null;
|
|
scheduleReconnect();
|
|
};
|
|
|
|
socket.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
isConnectedStore.set(false);
|
|
};
|
|
}
|
|
|
|
function setCoin(coinSymbol: string): void {
|
|
activeCoin = coinSymbol;
|
|
sendMessage({ type: 'set_coin', coinSymbol });
|
|
}
|
|
|
|
function disconnect(): void {
|
|
clearReconnectTimer();
|
|
|
|
if (socket) {
|
|
socket.close();
|
|
socket = null;
|
|
}
|
|
|
|
isConnectedStore.set(false);
|
|
}
|
|
|
|
function subscribeToComments(coinSymbol: string, callback: (message: any) => void): void {
|
|
commentSubscriptions.set(coinSymbol, callback);
|
|
}
|
|
|
|
function unsubscribeFromComments(coinSymbol: string): void {
|
|
commentSubscriptions.delete(coinSymbol);
|
|
}
|
|
|
|
export const websocketController = {
|
|
connect,
|
|
disconnect,
|
|
setCoin,
|
|
subscribeToComments,
|
|
unsubscribeFromComments,
|
|
loadInitialTrades
|
|
}; |