Files
flyer-crawler.projectium.com/docs/WEBSOCKET_USAGE.md
Torben Sorensen cf476e7afc
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m47s
ADR-022 - websocket notificaitons - also more test fixes with stores
2026-01-19 10:53:42 -08:00

9.9 KiB

WebSocket Real-Time Notifications - Usage Guide

This guide shows you how to use the WebSocket real-time notification system in your React components.

Quick Start

1. Enable Global Notifications

Add the NotificationToastHandler to your root App.tsx:

// src/App.tsx
import { Toaster } from 'react-hot-toast';
import { NotificationToastHandler } from './components/NotificationToastHandler';

function App() {
  return (
    <>
      {/* React Hot Toast container */}
      <Toaster position="top-right" />

      {/* WebSocket notification handler (renders nothing, handles side effects) */}
      <NotificationToastHandler
        enabled={true}
        playSound={false} // Set to true to play notification sounds
      />

      {/* Your app routes and components */}
      <YourAppContent />
    </>
  );
}

2. Add Notification Bell to Header

// src/components/Header.tsx
import { NotificationBell } from './components/NotificationBell';
import { useNavigate } from 'react-router-dom';

function Header() {
  const navigate = useNavigate();

  return (
    <header className="flex items-center justify-between p-4">
      <h1>Flyer Crawler</h1>

      <div className="flex items-center gap-4">
        {/* Notification bell with unread count */}
        <NotificationBell onClick={() => navigate('/notifications')} showConnectionStatus={true} />

        <UserMenu />
      </div>
    </header>
  );
}

3. Listen for Notifications in Components

// src/pages/DealsPage.tsx
import { useEventBus } from '../hooks/useEventBus';
import { useCallback, useState } from 'react';
import type { DealNotificationData } from '../types/websocket';

function DealsPage() {
  const [deals, setDeals] = useState([]);

  // Listen for new deal notifications
  const handleDealNotification = useCallback((data: DealNotificationData) => {
    console.log('New deals received:', data.deals);

    // Update your deals list
    setDeals((prev) => [...data.deals, ...prev]);

    // Or refetch from API
    // refetchDeals();
  }, []);

  useEventBus('notification:deal', handleDealNotification);

  return (
    <div>
      <h1>Deals</h1>
      {/* Render deals */}
    </div>
  );
}

Available Components

NotificationBell

A notification bell icon with unread count and connection status indicator.

Props:

  • onClick?: () => void - Callback when bell is clicked
  • showConnectionStatus?: boolean - Show green/red/yellow connection dot (default: true)
  • className?: string - Custom CSS classes

Example:

<NotificationBell
  onClick={() => navigate('/notifications')}
  showConnectionStatus={true}
  className="mr-4"
/>

ConnectionStatus

A simple status indicator showing if WebSocket is connected (no bell icon).

Example:

<ConnectionStatus />

NotificationToastHandler

Global handler that listens for WebSocket events and displays toasts. Should be rendered once at app root.

Props:

  • enabled?: boolean - Enable/disable toast notifications (default: true)
  • playSound?: boolean - Play sound on notifications (default: false)
  • soundUrl?: string - Custom notification sound URL

Example:

<NotificationToastHandler enabled={true} playSound={true} soundUrl="/custom-sound.mp3" />

Available Hooks

useWebSocket

Connect to the WebSocket server and manage connection state.

Options:

  • autoConnect?: boolean - Auto-connect on mount (default: true)
  • maxReconnectAttempts?: number - Max reconnect attempts (default: 5)
  • reconnectDelay?: number - Base reconnect delay in ms (default: 1000)
  • onConnect?: () => void - Callback on connection
  • onDisconnect?: () => void - Callback on disconnect
  • onError?: (error: Event) => void - Callback on error

Returns:

  • isConnected: boolean - Connection status
  • isConnecting: boolean - Connecting state
  • error: string | null - Error message if any
  • connect: () => void - Manual connect function
  • disconnect: () => void - Manual disconnect function
  • send: (message: WebSocketMessage) => void - Send message to server

Example:

const { isConnected, error, connect, disconnect } = useWebSocket({
  autoConnect: true,
  maxReconnectAttempts: 3,
  onConnect: () => console.log('Connected!'),
  onDisconnect: () => console.log('Disconnected!'),
});

return (
  <div>
    <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
    {error && <p>Error: {error}</p>}
    <button onClick={connect}>Reconnect</button>
  </div>
);

useEventBus

Subscribe to event bus events (used with WebSocket integration).

Parameters:

  • event: string - Event name to listen for
  • callback: (data?: T) => void - Callback function

Available Events:

  • 'notification:deal' - Deal notifications (DealNotificationData)
  • 'notification:system' - System messages (SystemMessageData)
  • 'notification:error' - Error messages ({ message: string; code?: string })

Example:

import { useEventBus } from '../hooks/useEventBus';
import type { DealNotificationData } from '../types/websocket';

function MyComponent() {
  useEventBus<DealNotificationData>('notification:deal', (data) => {
    console.log('Received deal:', data);
  });

  return <div>Listening for deals...</div>;
}

Message Types

Deal Notification

interface DealNotificationData {
  notification_id?: string;
  deals: Array<{
    item_name: string;
    best_price_in_cents: number;
    store_name: string;
    store_id: string;
  }>;
  user_id: string;
  message: string;
}

System Message

interface SystemMessageData {
  message: string;
  severity: 'info' | 'warning' | 'error';
}

Advanced Usage

Custom Notification Handling

If you don't want to use the default NotificationToastHandler, you can create your own:

import { useWebSocket } from '../hooks/useWebSocket';
import { useEventBus } from '../hooks/useEventBus';
import type { DealNotificationData } from '../types/websocket';

function CustomNotificationHandler() {
  const { isConnected } = useWebSocket({ autoConnect: true });

  useEventBus<DealNotificationData>('notification:deal', (data) => {
    // Custom handling - e.g., update Redux store
    dispatch(addDeals(data.deals));

    // Show custom UI
    showCustomNotification(data.message);
  });

  return null; // Or return your custom UI
}

Conditional WebSocket Connection

import { useWebSocket } from '../hooks/useWebSocket';
import { useAuth } from '../hooks/useAuth';

function ConditionalWebSocket() {
  const { user } = useAuth();

  // Only connect if user is logged in
  useWebSocket({
    autoConnect: !!user,
  });

  return null;
}

Send Messages to Server

import { useWebSocket } from '../hooks/useWebSocket';

function PingComponent() {
  const { send, isConnected } = useWebSocket();

  const sendPing = () => {
    send({
      type: 'ping',
      data: {},
      timestamp: new Date().toISOString(),
    });
  };

  return (
    <button onClick={sendPing} disabled={!isConnected}>
      Send Ping
    </button>
  );
}

Admin Monitoring

Get WebSocket Stats

Admin users can check WebSocket connection statistics:

# Get connection stats
curl -H "Authorization: Bearer <admin-token>" \
  http://localhost:3001/api/admin/websocket/stats

Response:

{
  "success": true,
  "data": {
    "totalUsers": 42,
    "totalConnections": 67
  }
}

Admin Dashboard Integration

import { useEffect, useState } from 'react';

function AdminWebSocketStats() {
  const [stats, setStats] = useState({ totalUsers: 0, totalConnections: 0 });

  useEffect(() => {
    const fetchStats = async () => {
      const response = await fetch('/api/admin/websocket/stats', {
        headers: { Authorization: `Bearer ${token}` },
      });
      const data = await response.json();
      setStats(data.data);
    };

    fetchStats();
    const interval = setInterval(fetchStats, 5000); // Poll every 5s

    return () => clearInterval(interval);
  }, []);

  return (
    <div className="p-4 border rounded">
      <h3>WebSocket Stats</h3>
      <p>Connected Users: {stats.totalUsers}</p>
      <p>Total Connections: {stats.totalConnections}</p>
    </div>
  );
}

Troubleshooting

Connection Issues

  1. Check JWT Token: WebSocket requires a valid JWT token in cookies or query string
  2. Check Server Logs: Look for WebSocket connection errors in server logs
  3. Check Browser Console: WebSocket errors are logged to console
  4. Verify Path: WebSocket server is at ws://localhost:3001/ws (or wss:// for HTTPS)

Not Receiving Notifications

  1. Check Connection Status: Use <ConnectionStatus /> to verify connection
  2. Verify Event Name: Ensure you're listening to the correct event (notification:deal, etc.)
  3. Check User ID: Notifications are sent to specific users - verify JWT user_id matches

High Memory Usage

  1. Connection Leaks: Ensure components using useWebSocket are properly unmounting
  2. Event Listeners: useEventBus automatically cleans up, but verify no manual listeners remain
  3. Check Stats: Use /api/admin/websocket/stats to monitor connection count

Testing

Unit Tests

import { renderHook } from '@testing-library/react';
import { useWebSocket } from '../hooks/useWebSocket';

describe('useWebSocket', () => {
  it('should connect automatically', () => {
    const { result } = renderHook(() => useWebSocket({ autoConnect: true }));
    expect(result.current.isConnecting).toBe(true);
  });
});

Integration Tests

See src/tests/integration/websocket.integration.test.ts for comprehensive integration tests.