All files / src/hooks useNotificationsWithFallback.ts

38.15% Statements 66/173
100% Branches 0/0
0% Functions 0/1
38.15% Lines 66/173

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 1741x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                                                                                                                       1x 1x  
'use client';
 
/**
 * useNotificationsWithFallback Hook
 *
 * Hybrid notifications hook that uses WebSocket for real-time updates
 * when available, falling back to polling when WebSocket is unavailable.
 *
 * This is the recommended hook to use in components that need notifications,
 * as it provides seamless fallback behavior.
 */
 
import { useEffect, useCallback } from 'react';
import { useRealtimeNotifications } from './useRealtimeNotifications';
import { useNotifications } from './useNotifications';
import { useAppDispatch, useAppSelector } from '@/redux/store';
import {
  fetchNotifications,
  markNotificationAsRead,
  markAllNotificationsAsRead,
  selectNotifications,
  selectUnreadCount,
  selectNotificationsLoading,
  selectNotificationsError,
} from '@/redux/features/notificationsSlice';
import type { NotificationPayload } from '@/lib/notifications/types';
 
export type NotificationMode = 'realtime' | 'polling';
 
export interface UseNotificationsWithFallbackReturn {
  /** All notifications (from Redux state) */
  notifications: NotificationPayload[];
  /** Unread notification count */
  unreadCount: number;
  /** Whether notifications are loading */
  isLoading: boolean;
  /** Error message if any */
  error: string | null;
  /** Mark a single notification as read */
  markAsRead: (id: string) => Promise<void>;
  /** Mark all notifications as read */
  markAllAsRead: () => Promise<void>;
  /** Manually refresh notifications */
  refresh: () => Promise<void>;
  /** Current notification mode */
  mode: NotificationMode;
  /** Whether connected (realtime) or polling active */
  isActive: boolean;
  /** Whether WebSocket is connected */
  isRealtimeConnected: boolean;
}
 
export interface UseNotificationsWithFallbackOptions {
  /** Polling interval when in fallback mode (default: 30000ms) */
  pollingInterval?: number;
  /** Whether to show browser notifications (default: true) */
  showBrowserNotifications?: boolean;
}
 
const DEFAULT_OPTIONS: UseNotificationsWithFallbackOptions = {
  pollingInterval: 30000,
  showBrowserNotifications: true,
};
 
export function useNotificationsWithFallback(
  options: UseNotificationsWithFallbackOptions = {}
): UseNotificationsWithFallbackReturn {
  const opts = { ...DEFAULT_OPTIONS, ...options };
  const dispatch = useAppDispatch();

  // Real-time notifications via WebSocket
  const {
    isConnected: isRealtimeConnected,
    markAsRead: realtimeMarkAsRead,
  } = useRealtimeNotifications({
    showBrowserNotifications: opts.showBrowserNotifications,
  });

  // Polling-based notifications (as fallback)
  const polling = useNotifications({
    // Disable polling when connected to real-time
    pollingInterval: isRealtimeConnected ? 0 : opts.pollingInterval,
    // Only auto-fetch if not using realtime
    autoFetch: !isRealtimeConnected,
  });

  // Redux state (populated by either realtime or polling)
  const notifications = useAppSelector(selectNotifications);
  const unreadCount = useAppSelector(selectUnreadCount);
  const isLoading = useAppSelector(selectNotificationsLoading);
  const reduxError = useAppSelector(selectNotificationsError);

  // Determine current mode
  const mode: NotificationMode = isRealtimeConnected ? 'realtime' : 'polling';

  /**
   * Sync polling results to Redux when in polling mode
   */
  useEffect(() => {
    if (!isRealtimeConnected && polling.notifications.length > 0) {
      // When in polling mode and we have data, dispatch to Redux
      // This is handled by the fetchNotifications thunk when polling
    }
  }, [isRealtimeConnected, polling.notifications]);

  /**
   * Initial fetch to populate Redux state when realtime connects
   */
  useEffect(() => {
    if (isRealtimeConnected) {
      // When realtime connects, do an initial fetch to get existing notifications
      dispatch(fetchNotifications({}));
    }
  }, [isRealtimeConnected, dispatch]);

  /**
   * Mark a notification as read
   */
  const markAsRead = useCallback(
    async (id: string): Promise<void> => {
      if (isRealtimeConnected) {
        // Use realtime for cross-device sync
        realtimeMarkAsRead([id]);
        // Also update via API for persistence
        dispatch(markNotificationAsRead(id));
      } else {
        // Use polling-based approach
        await polling.markAsRead(id);
      }
    },
    [isRealtimeConnected, realtimeMarkAsRead, dispatch, polling]
  );

  /**
   * Mark all notifications as read
   */
  const markAllAsRead = useCallback(async (): Promise<void> => {
    if (isRealtimeConnected) {
      // Update via Redux thunk for API call
      dispatch(markAllNotificationsAsRead());
    } else {
      await polling.markAllAsRead();
    }
  }, [isRealtimeConnected, dispatch, polling]);

  /**
   * Manually refresh notifications
   */
  const refresh = useCallback(async (): Promise<void> => {
    if (isRealtimeConnected) {
      // Refetch from Redux
      dispatch(fetchNotifications({}));
    } else {
      await polling.refresh();
    }
  }, [isRealtimeConnected, dispatch, polling]);

  return {
    // Use Redux state when in realtime mode, local state when polling
    notifications: isRealtimeConnected ? notifications : polling.notifications,
    unreadCount: isRealtimeConnected ? unreadCount : polling.unreadCount,
    isLoading: isRealtimeConnected ? isLoading : polling.isLoading,
    error: isRealtimeConnected ? reduxError : polling.error,
    markAsRead,
    markAllAsRead,
    refresh,
    mode,
    isActive: isRealtimeConnected || polling.isActive,
    isRealtimeConnected,
  };
}
 
export default useNotificationsWithFallback;