diff --git a/client/README.md b/client/README.md
index 6b008e7..aada96e 100644
--- a/client/README.md
+++ b/client/README.md
@@ -9,6 +9,7 @@ src/
├── components/ # Reusable UI components
├── hooks/ # Custom React hooks
├── lib/ # Utility libraries and configurations
+├── services/ # Shared WebSocket and Telegram services
├── pages/ # Page components and routing
├── types/ # TypeScript type definitions
├── App.tsx # Main application component
@@ -36,6 +37,26 @@ The application uses a modern component library built on:
- **WebSocket**: Live tournament updates
- **Telegram SDK**: Integration with Telegram Web App
+### Services
+
+Shared logic for WebSocket connections and Telegram SDK is centralized in `src/services` and exposed via React contexts:
+
+```tsx
+import { TelegramProvider, useTelegram } from '@/services/telegram';
+import { WebSocketProvider, useWebSocketService } from '@/services/websocket';
+
+// Wrap your app
+
+
+
+
+;
+
+// Inside components
+const { getAuthHeaders, processStarsPayment } = useTelegram();
+const { addCallback } = useWebSocketService();
+```
+
### Routing
- **Wouter**: Lightweight React router
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 96fc2e1..8840aed 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,12 +1,12 @@
import { QueryClientProvider } from '@tanstack/react-query';
-import { useEffect } from 'react';
import { Switch, Route } from 'wouter';
import { queryClient } from './lib/queryClient';
import { Toaster } from '@/components/ui/toaster';
import { TooltipProvider } from '@/components/ui/tooltip';
-import { initTelegram } from '@/lib/telegram';
+import { TelegramProvider } from '@/services/telegram';
+import { WebSocketProvider } from '@/services/websocket';
import AdminPage from '@/pages/admin';
import NotFound from '@/pages/not-found';
import TournamentDetailPage from '@/pages/tournament-detail';
@@ -24,17 +24,17 @@ function Router() {
}
function App() {
- useEffect(() => {
- initTelegram();
- }, []);
-
return (
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/client/src/lib/telegram.ts b/client/src/lib/telegram.ts
index e8fbb42..86123f1 100644
--- a/client/src/lib/telegram.ts
+++ b/client/src/lib/telegram.ts
@@ -1,175 +1 @@
-// Use the global Telegram WebApp object from types/telegram.ts
-import type { TelegramWebApp } from '../types/telegram';
-
-let webApp: TelegramWebApp | null = null;
-
-export function initTelegram() {
- try {
- console.log('Initializing Telegram Web App');
-
- // Get the WebApp from window object
- const WebApp = window.Telegram?.WebApp;
-
- if (!WebApp) {
- console.warn('Telegram WebApp not available');
- return false;
- }
-
- // Проверяем, что мы внутри Telegram
- if (!WebApp.initDataUnsafe?.user && process.env.NODE_ENV === 'production') {
- console.warn('Not running inside Telegram Web App');
- return false;
- }
-
- WebApp.ready();
- WebApp.expand();
-
- // Настройка цветовой схемы - these methods might not exist outside Telegram
- if (WebApp.setHeaderColor) {
- WebApp.setHeaderColor('#1f2937');
- }
- if (WebApp.setBottomBarColor) {
- WebApp.setBottomBarColor('#ffffff');
- }
-
- // Скрываем главную кнопку по умолчанию
- if (WebApp.MainButton) {
- WebApp.MainButton.hide();
- }
-
- webApp = WebApp;
-
- return true;
- } catch (error) {
- console.error('Failed to initialize Telegram Web App:', error);
- return false;
- }
-}
-
-export function getTelegramWebApp(): TelegramWebApp | null {
- // Fallback for development/testing if webApp is not initialized but WebApp SDK is available
- if (!webApp && window?.Telegram?.WebApp) {
- webApp = window.Telegram.WebApp;
- webApp.ready();
- webApp.expand();
- }
- return webApp;
-}
-
-export function getTelegramUser() {
- try {
- return window.Telegram?.WebApp?.initDataUnsafe?.user || null;
- } catch (error) {
- console.error('Failed to get Telegram user:', error);
- return null;
- }
-}
-
-export function showMainButton(text: string, onClick: () => void) {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.MainButton) {
- WebApp.MainButton.setText(text);
- WebApp.MainButton.show();
- WebApp.MainButton.onClick(onClick);
- }
- } catch (error) {
- console.error('Failed to show main button:', error);
- }
-}
-
-export function hideMainButton() {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.MainButton) {
- WebApp.MainButton.hide();
- }
- } catch (error) {
- console.error('Failed to hide main button:', error);
- }
-}
-
-export function showBackButton(onClick: () => void) {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.BackButton) {
- WebApp.BackButton.onClick(onClick);
- WebApp.BackButton.show();
- }
- } catch (error) {
- console.error('Failed to show back button:', error);
- }
-}
-
-export function hideBackButton() {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.BackButton) {
- WebApp.BackButton.hide();
- }
- } catch (error) {
- console.error('Failed to hide back button:', error);
- }
-}
-
-export function processStarsPayment(amount: number, tournamentId: string): Promise {
- return new Promise((resolve) => {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.openInvoice) {
- // In a real implementation, this would use Telegram's payment API
- // For now, we simulate the payment process
- const invoiceUrl = `https://t.me/invoice/stars?amount=${amount}&payload=${tournamentId}`;
-
- WebApp.openInvoice(invoiceUrl, (status: string) => {
- resolve(status === 'paid');
- });
- } else {
- // Fallback for development/testing
- const confirmed = window.confirm(`Pay ${amount} Telegram Stars to join tournament?`);
- resolve(confirmed);
- }
- });
-}
-
-export function getAuthHeaders(): Record {
- const initData = window.Telegram?.WebApp?.initData;
- if (!initData) {
- return {};
- }
-
- return {
- 'x-telegram-init-data': initData,
- };
-}
-
-export function showAlert(message: string) {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (WebApp?.showAlert) {
- WebApp.showAlert(message);
- } else {
- // Fallback to browser alert
- alert(message);
- }
- } catch (error) {
- console.error('Failed to show alert:', error);
- alert(message);
- }
-}
-
-export function hapticFeedback(type: 'impact' | 'notification' | 'selection' = 'impact') {
- try {
- const WebApp = window.Telegram?.WebApp;
- if (!WebApp?.HapticFeedback) return;
-
- if (type === 'impact') {
- WebApp.HapticFeedback.impactOccurred('medium');
- } else if (type === 'notification') {
- WebApp.HapticFeedback.notificationOccurred('success');
- } else {
- WebApp.HapticFeedback.selectionChanged();
- }
- } catch (error) {
- console.error('Haptic feedback failed:', error);
- }
-}
+export * from '../services/telegram';
diff --git a/client/src/lib/websocket.ts b/client/src/lib/websocket.ts
index f8bffb3..6488d24 100644
--- a/client/src/lib/websocket.ts
+++ b/client/src/lib/websocket.ts
@@ -1,152 +1 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
-
-export interface WebSocketMessage {
- type:
- | 'tournament_created'
- | 'tournament_updated'
- | 'tournament_deleted'
- | 'tournament_registration'
- | 'tournament_unregistration';
- tournament?: any;
- tournamentId?: string;
- userId?: string;
-}
-
-export function useWebSocket(onMessage?: (message: WebSocketMessage) => void) {
- const [isConnected, setIsConnected] = useState(false);
- const wsRef = useRef(null);
- const reconnectTimeoutRef = useRef();
- const websocketCallbacks = useRef<((message: any) => void)[]>([]); // Use a ref to hold the callbacks
-
- // Function to add callbacks
- const addCallback = (callback: (message: any) => void) => {
- websocketCallbacks.current.push(callback);
- // Ensure initial connection status is reflected if already connected
- if (isConnected) {
- callback({ type: 'connected' });
- }
- return () => {
- // Remove callback on cleanup
- websocketCallbacks.current = websocketCallbacks.current.filter((cb) => cb !== callback);
- };
- };
-
- let reconnectAttempts = 0;
- const maxReconnectAttempts = 5;
- const baseReconnectDelay = 1000; // 1 second
-
- const connect = useCallback(() => {
- try {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/ws`;
- // console.log("Connecting to WebSocket at:", wsUrl); // Removed console.log
-
- wsRef.current = new WebSocket(wsUrl);
-
- wsRef.current.onopen = () => {
- // console.log("WebSocket connected"); // Removed console.log
- setIsConnected(true);
- reconnectAttempts = 0;
- // Clear any existing reconnect timeout
- if (reconnectTimeoutRef.current) {
- clearTimeout(reconnectTimeoutRef.current);
- }
- websocketCallbacks.current.forEach((callback) => callback({ type: 'connected' }));
- };
-
- wsRef.current.onmessage = (event) => {
- try {
- const message = JSON.parse(event.data);
- onMessage?.(message); // Call the primary onMessage handler
- websocketCallbacks.current.forEach((callback) =>
- callback({ type: 'message', data: message }),
- ); // Call registered callbacks
- } catch (_error) {
- websocketCallbacks.current.forEach((callback) =>
- callback({
- type: 'error',
- error: 'Failed to parse message',
- }),
- );
- }
- };
-
- wsRef.current.onclose = () => {
- setIsConnected(false);
- websocketCallbacks.current.forEach((callback) => callback({ type: 'disconnected' }));
-
- if (reconnectAttempts < maxReconnectAttempts) {
- const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000);
- const jitter = Math.random() * 1000; // Add some randomness
- const finalDelay = delay + jitter;
- reconnectAttempts++;
- reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay);
- } else {
- websocketCallbacks.current.forEach((callback) =>
- callback({
- type: 'error',
- error: 'Connection failed after maximum attempts',
- }),
- );
- }
- };
-
- wsRef.current.onerror = () => {
- setIsConnected(false); // Ensure isConnected is false on error
- websocketCallbacks.current.forEach((callback) =>
- callback({
- type: 'error',
- error: 'WebSocket connection error',
- }),
- );
- // The onclose event will handle reconnection logic, so we don't need to call connect() here again.
- };
- } catch (error) {
- // console.error("Failed to connect to WebSocket:", error); // Removed console.error
- setIsConnected(false);
- websocketCallbacks.current.forEach((callback) =>
- callback({
- type: 'error',
- error: 'Failed to initiate WebSocket connection',
- }),
- );
- // Attempt to reconnect even if initial connection fails
- if (reconnectAttempts < maxReconnectAttempts) {
- const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts), 30000);
- const jitter = Math.random() * 1000; // Add some randomness
- const finalDelay = delay + jitter;
- // console.log( // Removed console.log
- // `Attempting to reconnect after initial failure in ${Math.round(finalDelay)}ms... (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`,
- // );
- reconnectAttempts++;
- reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay);
- } else {
- // console.error("Max reconnection attempts reached after initial failure"); // Removed console.error
- }
- }
- }, [onMessage]);
-
- useEffect(() => {
- connect();
-
- return () => {
- if (reconnectTimeoutRef.current) {
- clearTimeout(reconnectTimeoutRef.current);
- }
- if (wsRef.current) {
- wsRef.current.close();
- }
- };
- }, [connect]);
-
- const sendMessage = (message: any) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify(message));
- } else {
- // console.warn("Cannot send message: WebSocket is not open."); // Removed console.warn
- // Optionally, queue message or notify user
- }
- };
-
- return { isConnected, sendMessage, addCallback };
-}
+export * from '../services/websocket';
diff --git a/client/src/pages/tournament-detail.tsx b/client/src/pages/tournament-detail.tsx
index 3cbfedc..e81418c 100644
--- a/client/src/pages/tournament-detail.tsx
+++ b/client/src/pages/tournament-detail.tsx
@@ -10,24 +10,20 @@ import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { useToast } from '@/hooks/use-toast';
import { queryClient } from '@/lib/queryClient';
-import {
- getAuthHeaders,
- processStarsPayment,
- showBackButton,
- hideBackButton,
-} from '@/lib/telegram';
+import { useTelegram } from '@/services/telegram';
export default function TournamentDetailPage() {
const params = useParams();
const [, setLocation] = useLocation();
const { toast } = useToast();
const tournamentId = params.id;
+ const { getAuthHeaders, processStarsPayment, showBackButton, hideBackButton } = useTelegram();
// Setup Telegram back button
useEffect(() => {
showBackButton(() => setLocation('/'));
return () => hideBackButton();
- }, [setLocation]);
+ }, [setLocation, showBackButton, hideBackButton]);
// Fetch tournament details
const { data: tournament, isLoading: tournamentLoading } = useQuery({
diff --git a/client/src/pages/tournaments.tsx b/client/src/pages/tournaments.tsx
index 87e08f1..b2d297a 100644
--- a/client/src/pages/tournaments.tsx
+++ b/client/src/pages/tournaments.tsx
@@ -2,6 +2,7 @@ import type { Tournament } from '@shared/schema';
import { useQuery, useMutation } from '@tanstack/react-query';
import { Trophy, Star, Plus } from 'lucide-react';
import { useLocation } from 'wouter';
+import { useEffect } from 'react';
import Header from '@/components/header';
import TournamentCard from '@/components/tournament-card';
@@ -10,12 +11,14 @@ import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { useToast } from '@/hooks/use-toast';
import { queryClient } from '@/lib/queryClient';
-import { getAuthHeaders, processStarsPayment } from '@/lib/telegram';
-import { useWebSocket, type WebSocketMessage } from '@/lib/websocket';
+import { useTelegram } from '@/services/telegram';
+import { useWebSocketService, type WebSocketMessage } from '@/services/websocket';
export default function TournamentsPage() {
const { toast } = useToast();
const [, setLocation] = useLocation();
+ const { getAuthHeaders, processStarsPayment } = useTelegram();
+ const { addCallback } = useWebSocketService();
// Fetch tournaments
const { data: tournaments = [], isLoading: tournamentsLoading } = useQuery({
@@ -76,19 +79,21 @@ export default function TournamentsPage() {
});
// WebSocket for real-time updates
- useWebSocket((message: WebSocketMessage) => {
- switch (message.type) {
- case 'tournament_created':
- case 'tournament_updated':
- case 'tournament_registration':
- case 'tournament_unregistration':
- queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] });
- break;
- case 'tournament_deleted':
- queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] });
- break;
- }
- });
+ useEffect(() => {
+ return addCallback((message: WebSocketMessage) => {
+ switch (message.type) {
+ case 'tournament_created':
+ case 'tournament_updated':
+ case 'tournament_registration':
+ case 'tournament_unregistration':
+ queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] });
+ break;
+ case 'tournament_deleted':
+ queryClient.invalidateQueries({ queryKey: ['/api/tournaments'] });
+ break;
+ }
+ });
+ }, [addCallback]);
const handleJoinTournament = async (tournament: Tournament) => {
if (!user) {
diff --git a/client/src/services/telegram.tsx b/client/src/services/telegram.tsx
new file mode 100644
index 0000000..e01fed1
--- /dev/null
+++ b/client/src/services/telegram.tsx
@@ -0,0 +1,179 @@
+import { createContext, useContext, useEffect, type ReactNode } from 'react';
+import type { TelegramWebApp } from '../types/telegram';
+
+let webApp: TelegramWebApp | null = null;
+
+function initTelegram() {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (!WebApp) return false;
+ if (!WebApp.initDataUnsafe?.user && process.env.NODE_ENV === 'production') {
+ return false;
+ }
+ WebApp.ready();
+ WebApp.expand();
+ if (WebApp.setHeaderColor) {
+ WebApp.setHeaderColor('#1f2937');
+ }
+ if (WebApp.setBottomBarColor) {
+ WebApp.setBottomBarColor('#ffffff');
+ }
+ if (WebApp.MainButton) {
+ WebApp.MainButton.hide();
+ }
+ webApp = WebApp;
+ return true;
+ } catch (_error) {
+ return false;
+ }
+}
+
+function getTelegramWebApp(): TelegramWebApp | null {
+ if (!webApp && window?.Telegram?.WebApp) {
+ webApp = window.Telegram.WebApp;
+ webApp.ready();
+ webApp.expand();
+ }
+ return webApp;
+}
+
+function getTelegramUser() {
+ try {
+ return window.Telegram?.WebApp?.initDataUnsafe?.user || null;
+ } catch (_error) {
+ return null;
+ }
+}
+
+function showMainButton(text: string, onClick: () => void) {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.MainButton) {
+ WebApp.MainButton.setText(text);
+ WebApp.MainButton.show();
+ WebApp.MainButton.onClick(onClick);
+ }
+ } catch (_error) {
+ // ignore
+ }
+}
+
+function hideMainButton() {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.MainButton) {
+ WebApp.MainButton.hide();
+ }
+ } catch (_error) {
+ // ignore
+ }
+}
+
+function showBackButton(onClick: () => void) {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.BackButton) {
+ WebApp.BackButton.onClick(onClick);
+ WebApp.BackButton.show();
+ }
+ } catch (_error) {
+ // ignore
+ }
+}
+
+function hideBackButton() {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.BackButton) {
+ WebApp.BackButton.hide();
+ }
+ } catch (_error) {
+ // ignore
+ }
+}
+
+function processStarsPayment(amount: number, tournamentId: string): Promise {
+ return new Promise((resolve) => {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.openInvoice) {
+ const invoiceUrl = `https://t.me/invoice/stars?amount=${amount}&payload=${tournamentId}`;
+ WebApp.openInvoice(invoiceUrl, (status: string) => {
+ resolve(status === 'paid');
+ });
+ } else {
+ const confirmed = window.confirm(`Pay ${amount} Telegram Stars to join tournament?`);
+ resolve(confirmed);
+ }
+ });
+}
+
+function getAuthHeaders(): Record {
+ const initData = window.Telegram?.WebApp?.initData;
+ if (!initData) {
+ return {};
+ }
+ return { 'x-telegram-init-data': initData };
+}
+
+function showAlert(message: string) {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (WebApp?.showAlert) {
+ WebApp.showAlert(message);
+ } else {
+ alert(message);
+ }
+ } catch (_error) {
+ alert(message);
+ }
+}
+
+function hapticFeedback(type: 'impact' | 'notification' | 'selection' = 'impact') {
+ try {
+ const WebApp = window.Telegram?.WebApp;
+ if (!WebApp?.HapticFeedback) return;
+ if (type === 'impact') {
+ WebApp.HapticFeedback.impactOccurred('medium');
+ } else if (type === 'notification') {
+ WebApp.HapticFeedback.notificationOccurred('success');
+ } else {
+ WebApp.HapticFeedback.selectionChanged();
+ }
+ } catch (_error) {
+ // ignore
+ }
+}
+
+const telegramService = {
+ getAuthHeaders,
+ processStarsPayment,
+ showBackButton,
+ hideBackButton,
+};
+
+const TelegramContext = createContext(telegramService);
+
+export function TelegramProvider({ children }: { children: ReactNode }) {
+ useEffect(() => {
+ initTelegram();
+ }, []);
+ return {children};
+}
+
+export function useTelegram() {
+ return useContext(TelegramContext);
+}
+
+export {
+ initTelegram,
+ getTelegramWebApp,
+ getTelegramUser,
+ showMainButton,
+ hideMainButton,
+ showBackButton,
+ hideBackButton,
+ processStarsPayment,
+ getAuthHeaders,
+ showAlert,
+ hapticFeedback,
+};
diff --git a/client/src/services/websocket.tsx b/client/src/services/websocket.tsx
new file mode 100644
index 0000000..a0e92b7
--- /dev/null
+++ b/client/src/services/websocket.tsx
@@ -0,0 +1,149 @@
+import { createContext, useContext, type ReactNode } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import type { Tournament } from '@shared/schema';
+
+export type WebSocketMessage =
+ | { type: 'tournament_created'; tournament: Tournament }
+ | { type: 'tournament_updated'; tournament: Tournament }
+ | { type: 'tournament_deleted'; tournamentId: string }
+ | { type: 'tournament_registration'; tournament: Tournament; userId: string }
+ | { type: 'tournament_unregistration'; tournament: Tournament; userId: string };
+
+function useWebSocket(onMessage?: (message: WebSocketMessage) => void) {
+ const [isConnected, setIsConnected] = useState(false);
+ const wsRef = useRef(null);
+ const reconnectTimeoutRef = useRef();
+ const websocketCallbacks = useRef<((message: any) => void)[]>([]);
+
+ const addCallback = (callback: (message: any) => void) => {
+ websocketCallbacks.current.push(callback);
+ if (isConnected) {
+ callback({ type: 'connected' });
+ }
+ return () => {
+ websocketCallbacks.current = websocketCallbacks.current.filter((cb) => cb !== callback);
+ };
+ };
+
+ const reconnectAttemptsRef = useRef(0);
+ const maxReconnectAttempts = 5;
+ const baseReconnectDelay = 1000;
+
+ const connect = useCallback(() => {
+ try {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
+
+ wsRef.current = new WebSocket(wsUrl);
+
+ wsRef.current.onopen = () => {
+ setIsConnected(true);
+ reconnectAttemptsRef.current = 0;
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+ websocketCallbacks.current.forEach((callback) => callback({ type: 'connected' }));
+ };
+
+ wsRef.current.onmessage = (event) => {
+ try {
+ const message = JSON.parse(event.data) as WebSocketMessage;
+ onMessage?.(message);
+ websocketCallbacks.current.forEach((callback) =>
+ callback({ type: 'message', data: message }),
+ );
+ } catch (_error) {
+ websocketCallbacks.current.forEach((callback) =>
+ callback({ type: 'error', error: 'Failed to parse message' }),
+ );
+ }
+ };
+
+ wsRef.current.onclose = () => {
+ setIsConnected(false);
+ websocketCallbacks.current.forEach((callback) => callback({ type: 'disconnected' }));
+
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
+ const delay = Math.min(
+ baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current),
+ 30000,
+ );
+ const jitter = Math.random() * 1000;
+ const finalDelay = delay + jitter;
+ reconnectAttemptsRef.current++;
+ reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay);
+ } else {
+ websocketCallbacks.current.forEach((callback) =>
+ callback({ type: 'error', error: 'Connection failed after maximum attempts' }),
+ );
+ }
+ };
+
+ wsRef.current.onerror = () => {
+ setIsConnected(false);
+ websocketCallbacks.current.forEach((callback) =>
+ callback({ type: 'error', error: 'WebSocket connection error' }),
+ );
+ };
+ } catch (_error) {
+ setIsConnected(false);
+ websocketCallbacks.current.forEach((callback) =>
+ callback({ type: 'error', error: 'Failed to initiate WebSocket connection' }),
+ );
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
+ const delay = Math.min(
+ baseReconnectDelay * Math.pow(2, reconnectAttemptsRef.current),
+ 30000,
+ );
+ const jitter = Math.random() * 1000;
+ const finalDelay = delay + jitter;
+ reconnectAttemptsRef.current++;
+ reconnectTimeoutRef.current = window.setTimeout(connect, finalDelay);
+ }
+ }
+ }, [onMessage]);
+
+ useEffect(() => {
+ connect();
+
+ return () => {
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ };
+ }, [connect]);
+
+ const sendMessage = (message: WebSocketMessage) => {
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify(message));
+ }
+ };
+
+ return { isConnected, sendMessage, addCallback };
+}
+
+interface WebSocketContextValue {
+ isConnected: boolean;
+ sendMessage: (message: WebSocketMessage) => void;
+ addCallback: (callback: (message: any) => void) => () => void;
+}
+
+const WebSocketContext = createContext(null);
+
+export function WebSocketProvider({ children }: { children: ReactNode }) {
+ const ws = useWebSocket();
+ return {children};
+}
+
+export function useWebSocketService() {
+ const ctx = useContext(WebSocketContext);
+ if (!ctx) {
+ throw new Error('useWebSocketService must be used within a WebSocketProvider');
+ }
+ return ctx;
+}
+
+export { useWebSocket };