Remove Websocket code since we are only using SSE

This commit is contained in:
Mo Tarbin
2025-09-27 13:38:45 -04:00
parent d28ecb509e
commit 9ff2359b4c
5 changed files with 0 additions and 593 deletions

View File

@@ -1,106 +0,0 @@
import { Circle, SignalWifi4Bar, SignalWifiOff } from '@mui/icons-material'
import { Box, Chip, Tooltip, Typography } from '@mui/joy'
import { useWebSocketContext } from '../contexts/WebSocketContext'
const WebSocketConnectionStatus = ({
variant = 'minimal',
showError = false,
sx = {},
}) => {
const { isConnected, isConnecting, error, getConnectionStatus } =
useWebSocketContext()
const getStatusColor = () => {
if (isConnected) return 'success'
if (isConnecting) return 'warning'
return 'danger'
}
const getStatusIcon = () => {
if (isConnected) return <SignalWifi4Bar />
if (isConnecting) return <Circle />
return <SignalWifiOff />
}
const getStatusText = () => {
if (isConnected) return 'Connected'
if (isConnecting) return 'Connecting...'
return 'Disconnected'
}
const getTooltipText = () => {
const status = getConnectionStatus()
if (error) return `Real-time updates: ${status} - ${error}`
if (!isConnected && !isConnecting) {
return `Real-time updates: ${status} - Join a circle to enable real-time updates`
}
return `Real-time updates: ${status}`
}
if (variant === 'minimal') {
return (
<Tooltip title={getTooltipText()} size='sm'>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
...sx,
}}
>
<Circle
sx={{
fontSize: 8,
color:
getStatusColor() === 'success'
? 'success.main'
: getStatusColor() === 'warning'
? 'warning.main'
: 'danger.main',
}}
/>
{showError && error && (
<Typography level='body-xs' color='danger'>
{error}
</Typography>
)}
</Box>
</Tooltip>
)
}
if (variant === 'chip') {
return (
<Tooltip title={getTooltipText()} size='sm'>
<Chip
color={getStatusColor()}
size='sm'
variant='soft'
startDecorator={getStatusIcon()}
sx={sx}
>
{getStatusText()}
</Chip>
</Tooltip>
)
}
// Full variant
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ...sx }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon()}
<Typography level='body-sm' color={getStatusColor()}>
{getStatusText()}
</Typography>
</Box>
{showError && error && (
<Typography level='body-xs' color='danger'>
{error}
</Typography>
)}
</Box>
)
}
export default WebSocketConnectionStatus

View File

@@ -1,143 +0,0 @@
import { Sync, SyncDisabled } from '@mui/icons-material'
import {
Box,
Card,
Chip,
FormControl,
FormHelperText,
FormLabel,
Switch,
Typography,
} from '@mui/joy'
import { useWebSocketContext } from '../contexts/WebSocketContext'
import { useUserProfile } from '../queries/UserQueries'
import { isPlusAccount } from '../utils/Helpers'
import WebSocketConnectionStatus from './WebSocketConnectionStatus'
const WebSocketSettings = () => {
const { data: userProfile } = useUserProfile()
const {
isConnected,
isConnecting,
error,
getConnectionStatus,
toggleWebSocketEnabled,
isWebSocketEnabled,
} = useWebSocketContext()
const handleToggle = () => {
if (!isPlusAccount(userProfile)) {
return // Don't allow toggle for non-Plus users
}
const currentlyEnabled = isWebSocketEnabled()
toggleWebSocketEnabled(!currentlyEnabled)
}
const getStatusDescription = () => {
if (!isPlusAccount(userProfile)) {
return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.'
}
if (!isWebSocketEnabled()) {
return 'Real-time updates are disabled. Enable to see live changes when you or other circle members complete, skip, or modify chores.'
}
if (isConnected) {
return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify chores."
}
if (isConnecting) {
return 'Connecting to real-time updates...'
}
if (error) {
return `Real-time updates are enabled but not working: ${error}`
}
return 'Real-time updates are enabled but not currently connected.'
}
return (
<Card sx={{ mt: 2, p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
{isWebSocketEnabled() && isPlusAccount(userProfile) ? (
<Sync color={isConnected ? 'success' : 'disabled'} />
) : (
<SyncDisabled color='disabled' />
)}
<Box sx={{ flex: 1 }}>
<Typography level='title-md'>
Real-time Updates
{!isPlusAccount(userProfile) && (
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
Plus Feature
</Chip>
)}
</Typography>
<Typography level='body-sm' color='neutral'>
Get instant notifications when chores are updated
</Typography>
</Box>
{isWebSocketEnabled() && isPlusAccount(userProfile) && (
<WebSocketConnectionStatus variant='chip' />
)}
</Box>
<FormControl orientation='horizontal' sx={{ mb: 2 }}>
<Box sx={{ flex: 1 }}>
<FormLabel>Enable Real-time Updates</FormLabel>
<FormHelperText sx={{ mt: 0 }}>
{getStatusDescription()}
</FormHelperText>
</Box>
<Switch
checked={Boolean(isWebSocketEnabled() && isPlusAccount(userProfile))}
onChange={handleToggle}
disabled={!isPlusAccount(userProfile)}
color={
isWebSocketEnabled() && isPlusAccount(userProfile)
? 'success'
: 'neutral'
}
variant='solid'
endDecorator={
isWebSocketEnabled() && isPlusAccount(userProfile) ? 'On' : 'Off'
}
slotProps={{ endDecorator: { sx: { minWidth: 24 } } }}
/>
</FormControl>
{isWebSocketEnabled() && isPlusAccount(userProfile) && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<Typography level='body-xs' color='neutral'>
Status:
</Typography>
<Chip
size='sm'
variant='soft'
color={
isConnected ? 'success' : isConnecting ? 'warning' : 'danger'
}
>
{getConnectionStatus()}
</Chip>
{error && (
<Typography level='body-xs' color='danger'>
{error}
</Typography>
)}
</Box>
)}
{!isPlusAccount(userProfile) && (
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
Real-time updates are not available in the Basic plan. Upgrade to Plus
to receive instant notifications when you or other circle members
complete, skip, or modify chores.
</Typography>
)}
</Card>
)
}
export default WebSocketSettings

View File

@@ -4,7 +4,6 @@ import QueryContext from './QueryContext'
import RouterContext from './RouterContext'
import SSEProvider from './SSEContext'
import ThemeContext from './ThemeContext'
import WebSocketProvider from './WebSocketContext'
const Contexts = ({ children }) => {
const contexts = [
@@ -13,7 +12,6 @@ const Contexts = ({ children }) => {
QueryContext,
NotificationProvider,
SSEProvider,
WebSocketProvider,
RouterContext,
]

View File

@@ -1,29 +0,0 @@
import { createContext, useContext } from 'react'
import { useWebSocket } from '../hooks/useWebSocket'
const WebSocketContext = createContext({
connectionState: 3, // CLOSED
isConnected: false,
isConnecting: false,
lastEvent: null,
error: null,
connect: () => {},
disconnect: () => {},
getConnectionStatus: () => 'disconnected',
})
export const useWebSocketContext = () => {
return useContext(WebSocketContext)
}
export const WebSocketProvider = ({ children }) => {
const webSocketState = useWebSocket()
return (
<WebSocketContext.Provider value={webSocketState}>
{children}
</WebSocketContext.Provider>
)
}
export default WebSocketProvider

View File

@@ -1,313 +0,0 @@
import { useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useRef, useState } from 'react'
import { apiManager, isTokenValid } from '../utils/TokenManager'
const WEBSOCKET_STATES = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
}
const RECONNECT_INTERVALS = [1000, 2000, 5000, 10000, 30000] // Progressive backoff
export const useWebSocket = () => {
const [connectionState, setConnectionState] = useState(
WEBSOCKET_STATES.CLOSED,
)
const [lastEvent, setLastEvent] = useState(null)
const [error, setError] = useState(null)
const wsRef = useRef(null)
const reconnectTimeoutRef = useRef(null)
const reconnectAttemptsRef = useRef(0)
const isManuallyClosedRef = useRef(false)
const queryClient = useQueryClient()
const getWebSocketUrl = useCallback(() => {
const token = localStorage.getItem('ca_token')
if (!token || !isTokenValid()) {
console.log('WebSocket: No valid authentication token')
return null
}
const apiUrl = apiManager.getApiURL()
// Convert HTTP/HTTPS to WebSocket protocol and remove /api/v1 suffix
let wsUrl = apiUrl.replace(/\/api\/v1$/, '')
if (wsUrl.startsWith('http://')) {
wsUrl = wsUrl.replace('http://', 'ws://')
} else if (wsUrl.startsWith('https://')) {
wsUrl = wsUrl.replace('https://', 'wss://')
} else {
const isHttps = window.location.protocol === 'https:'
wsUrl = `${isHttps ? 'wss:' : 'ws:'}//${wsUrl}`
}
// Let backend determine circle from authenticated user
wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}`
return wsUrl
}, [])
const handleWebSocketMessage = useCallback(
event => {
try {
const eventData = JSON.parse(event.data)
setLastEvent(eventData)
console.debug('WebSocket event received:', eventData.type, eventData)
// Handle different event types and update React Query cache accordingly
switch (eventData.type) {
case 'chore.created':
case 'chore.updated':
case 'chore.completed':
case 'chore.skipped':
case 'chore.deleted':
// Invalidate chores queries to refetch data
queryClient.invalidateQueries(['chores'])
// If it's a specific chore event, also invalidate that chore's details
if (eventData.data.chore?.id) {
queryClient.invalidateQueries(['chore', eventData.data.chore.id])
queryClient.invalidateQueries([
'choreDetails',
eventData.data.chore.id,
])
}
// expire the history so feed on dashboard gert updated :
// need to find a better way to do this as we don't need to do it with every single update for anything
// but not sure if i can do it with the fall-through switch case in javascript :)
queryClient.invalidateQueries(['choresHistory', 7])
break
case 'subtask.updated':
case 'subtask.completed':
// Invalidate the specific chore that contains this subtask
if (eventData.data.choreId) {
queryClient.invalidateQueries(['chore', eventData.data.choreId])
queryClient.invalidateQueries([
'choreDetails',
eventData.data.choreId,
])
}
// Also invalidate general chores list
queryClient.invalidateQueries(['chores'])
break
case 'heartbeat':
// Heartbeat events don't need cache invalidation
console.debug('Heartbeat!')
break
case 'connection.established':
console.log('WebSocket connection established')
setError(null)
break
case 'error':
console.error('WebSocket error event:', eventData.data)
setError(eventData.data.message || 'WebSocket error occurred')
break
default:
console.log('Unknown WebSocket event type:', eventData.type)
}
} catch (err) {
console.error('Failed to parse WebSocket message:', err)
setError('Failed to parse server message')
}
},
[queryClient],
)
const createWebSocketConnection = useCallback(
wsUrl => {
try {
setConnectionState(WEBSOCKET_STATES.CONNECTING)
isManuallyClosedRef.current = false
// Use query parameter authentication (token already included in URL)
wsRef.current = new WebSocket(wsUrl)
wsRef.current.onopen = () => {
setConnectionState(WEBSOCKET_STATES.OPEN)
setError(null)
reconnectAttemptsRef.current = 0
}
wsRef.current.onmessage = handleWebSocketMessage
wsRef.current.onerror = error => {
console.error('WebSocket error:', error)
setError('Connection error occurred')
}
} catch (err) {
console.error('Failed to create WebSocket connection:', err)
setError('Failed to establish connection')
setConnectionState(WEBSOCKET_STATES.CLOSED)
}
},
[handleWebSocketMessage],
)
const scheduleReconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
const attemptIndex = Math.min(
reconnectAttemptsRef.current,
RECONNECT_INTERVALS.length - 1,
)
const delay = RECONNECT_INTERVALS[attemptIndex]
console.log(
`Scheduling WebSocket reconnect in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1})`,
)
reconnectTimeoutRef.current = setTimeout(() => {
reconnectAttemptsRef.current++
// Trigger reconnection
const wsUrl = getWebSocketUrl()
if (wsUrl && wsRef.current?.readyState !== WEBSOCKET_STATES.OPEN) {
createWebSocketConnection(wsUrl)
}
}, delay)
}, [getWebSocketUrl, createWebSocketConnection])
// Set up the onclose handler separately to avoid circular dependency
useEffect(() => {
if (wsRef.current) {
wsRef.current.onclose = event => {
console.log('WebSocket connection closed:', event.code, event.reason)
setConnectionState(WEBSOCKET_STATES.CLOSED)
// Handle different close codes
if (event.code === 4000) {
setError('Authentication failed - please refresh the page')
return // Don't attempt to reconnect for auth failures
} else if (event.code === 4001) {
setError('Authorization failed - check circle access')
return // Don't attempt to reconnect for auth failures
}
// Attempt to reconnect if not manually closed
if (!isManuallyClosedRef.current && event.code !== 1000) {
scheduleReconnect()
}
}
}
}, [scheduleReconnect])
const connect = useCallback(() => {
if (wsRef.current?.readyState === WEBSOCKET_STATES.OPEN) {
console.log('WebSocket: Already connected')
return // Already connected
}
const wsUrl = getWebSocketUrl()
console.log('WebSocket connect - URL:', wsUrl)
if (!wsUrl) {
console.log(
'Cannot connect to WebSocket: missing URL, token, or user profile',
)
return
}
createWebSocketConnection(wsUrl)
}, [getWebSocketUrl, createWebSocketConnection])
const disconnect = useCallback(() => {
isManuallyClosedRef.current = true
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
reconnectTimeoutRef.current = null
}
if (wsRef.current) {
wsRef.current.close(1000, 'Manual disconnect')
wsRef.current = null
}
setConnectionState(WEBSOCKET_STATES.CLOSED)
}, [])
const toggleWebSocketEnabled = useCallback(
enabled => {
localStorage.setItem('websocket_enabled', enabled.toString())
if (enabled && isTokenValid()) {
connect()
} else {
disconnect()
}
},
[connect, disconnect],
)
const isWebSocketEnabled = useCallback(() => {
return localStorage.getItem('websocket_enabled') !== 'false'
}, [])
// Auto-connect when WebSocket is enabled and token is valid
useEffect(() => {
// Check if WebSocket is enabled in settings
const isWebSocketEnabledSetting =
localStorage.getItem('websocket_enabled') !== 'false'
console.log('WebSocket enabled in settings:', isWebSocketEnabledSetting)
if (isTokenValid() && isWebSocketEnabledSetting) {
console.log('WebSocket: Conditions met, attempting to connect')
connect()
} else {
console.log('WebSocket: Conditions not met, disconnecting')
disconnect()
}
// Cleanup on unmount
return () => {
disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Only run once on mount
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
}
}, [])
return {
connectionState,
isConnected: connectionState === WEBSOCKET_STATES.OPEN,
isConnecting: connectionState === WEBSOCKET_STATES.CONNECTING,
lastEvent,
error,
connect,
disconnect,
toggleWebSocketEnabled,
isWebSocketEnabled,
// Helper function to check connection status
getConnectionStatus: () => {
switch (connectionState) {
case WEBSOCKET_STATES.CONNECTING:
return 'connecting'
case WEBSOCKET_STATES.OPEN:
return 'connected'
case WEBSOCKET_STATES.CLOSING:
return 'disconnecting'
case WEBSOCKET_STATES.CLOSED:
default:
return 'disconnected'
}
},
}
}