feat: Improve user status update throttling and SSE handling
- Add throttling mechanism to prevent redundant status updates - Implement SSE listener for dynamic user status updates - Optimize user status cache updates with deduplication logic - Enhance debug logging for status update events
- commit
- cee1fd897b96e304efc333d16890985ea374730f
- author
- Kit · · 2025-02-15 07:41 UTC
- parent
- d4613e4
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
2 files · +49 -15
M src/lib/states/chat.svelte.ts +30 -7
| @@ -194,14 +194,25 @@ class ChatState { | |||
| 194 | 194 | } | |
| 195 | 195 | ||
| 196 | 196 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | |
| 197 | - | console.debug('Updating user status with:', { | |
| 198 | - | userId, | |
| 199 | - | status, | |
| 197 | + | console.debug('Updating user status with:', { | |
| 198 | + | userId, | |
| 199 | + | status, | |
| 200 | 200 | lastSeen, | |
| 201 | 201 | currentCache: this.userCache[userId], | |
| 202 | 202 | currentUsers: this.users.find(u => u.id === userId) | |
| 203 | 203 | }); | |
| 204 | 204 | ||
| 205 | + | const existingUser = this.userCache[userId]; | |
| 206 | + | if ( | |
| 207 | + | existingUser && | |
| 208 | + | existingUser.status === status && | |
| 209 | + | lastSeen && | |
| 210 | + | Math.abs(existingUser.lastSeen ?? 0 - lastSeen) < 5000 | |
| 211 | + | ) { | |
| 212 | + | console.debug('Skipping redundant status update for user:', userId); | |
| 213 | + | return; | |
| 214 | + | } | |
| 215 | + | ||
| 205 | 216 | // Try to get the user from cache first | |
| 206 | 217 | let user: User | undefined = this.userCache[userId]; | |
| 207 | 218 | ||
| @@ -212,7 +223,7 @@ class ChatState { | |||
| 212 | 223 | user = foundUser; | |
| 213 | 224 | } | |
| 214 | 225 | } | |
| 215 | - | ||
| 226 | + | ||
| 216 | 227 | const now = Date.now(); | |
| 217 | 228 | if (user) { | |
| 218 | 229 | const updatedUser: User = { | |
| @@ -221,10 +232,10 @@ class ChatState { | |||
| 221 | 232 | // Update lastSeen based on status | |
| 222 | 233 | lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen) | |
| 223 | 234 | }; | |
| 224 | - | ||
| 235 | + | ||
| 225 | 236 | // Update cache first | |
| 226 | 237 | this.userCache[userId] = updatedUser; | |
| 227 | - | ||
| 238 | + | ||
| 228 | 239 | // Then update or add to users array | |
| 229 | 240 | const index = this.users.findIndex((u: User) => u.id === userId); | |
| 230 | 241 | if (index !== -1) { | |
| @@ -237,7 +248,7 @@ class ChatState { | |||
| 237 | 248 | } else { | |
| 238 | 249 | this.users = [...this.users, updatedUser]; | |
| 239 | 250 | } | |
| 240 | - | ||
| 251 | + | ||
| 241 | 252 | console.debug('User status updated:', { | |
| 242 | 253 | userId, | |
| 243 | 254 | newStatus: status, | |
| @@ -553,6 +564,18 @@ class ChatState { | |||
| 553 | 564 | console.debug('Error handling SSE chat message:', error); | |
| 554 | 565 | } | |
| 555 | 566 | }); | |
| 567 | + | ||
| 568 | + | // Listen for user status updates to update the buddy list dynamically | |
| 569 | + | eventSource.addEventListener('userStatusUpdate', async (event: MessageEvent) => { | |
| 570 | + | try { | |
| 571 | + | const statusUpdate = JSON.parse(event.data) as { userId: string; status: User['status']; lastSeen: number }; | |
| 572 | + | console.debug('Received userStatusUpdate via SSE:', statusUpdate); | |
| 573 | + | // Update the user status and ensure buddy list is updated appropriately | |
| 574 | + | this.updateUserStatus(statusUpdate.userId, statusUpdate.status, statusUpdate.lastSeen); | |
| 575 | + | } catch (error) { | |
| 576 | + | console.debug('Error handling userStatusUpdate via SSE:', error); | |
| 577 | + | } | |
| 578 | + | }); | |
| 556 | 579 | } | |
| 557 | 580 | ||
| 558 | 581 | private async ensureUserData(userId: string) { | |
| @@ -194,14 +194,25 @@ class ChatState { | |||||
| 194 | } | 194 | } | ||
| 195 | 195 | ||||
| 196 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | 196 | updateUserStatus(userId: string, status: User['status'], lastSeen?: number) { | ||
| 197 | - | console.debug('Updating user status with:', { | 197 | + | console.debug('Updating user status with:', { |
| 198 | - | userId, | 198 | + | userId, |
| 199 | - | status, | 199 | + | status, |
| 200 | lastSeen, | 200 | lastSeen, | ||
| 201 | currentCache: this.userCache[userId], | 201 | currentCache: this.userCache[userId], | ||
| 202 | currentUsers: this.users.find(u => u.id === userId) | 202 | currentUsers: this.users.find(u => u.id === userId) | ||
| 203 | }); | 203 | }); | ||
| 204 | 204 | ||||
| 205 | + | const existingUser = this.userCache[userId]; | |||
| 206 | + | if ( | |||
| 207 | + | existingUser && | |||
| 208 | + | existingUser.status === status && | |||
| 209 | + | lastSeen && | |||
| 210 | + | Math.abs(existingUser.lastSeen ?? 0 - lastSeen) < 5000 | |||
| 211 | + | ) { | |||
| 212 | + | console.debug('Skipping redundant status update for user:', userId); | |||
| 213 | + | return; | |||
| 214 | + | } | |||
| 215 | + | ||||
| 205 | // Try to get the user from cache first | 216 | // Try to get the user from cache first | ||
| 206 | let user: User | undefined = this.userCache[userId]; | 217 | let user: User | undefined = this.userCache[userId]; | ||
| 207 | 218 | ||||
| @@ -212,7 +223,7 @@ class ChatState { | |||||
| 212 | user = foundUser; | 223 | user = foundUser; | ||
| 213 | } | 224 | } | ||
| 214 | } | 225 | } | ||
| 215 | - | 226 | + | ||
| 216 | const now = Date.now(); | 227 | const now = Date.now(); | ||
| 217 | if (user) { | 228 | if (user) { | ||
| 218 | const updatedUser: User = { | 229 | const updatedUser: User = { | ||
| @@ -221,10 +232,10 @@ class ChatState { | |||||
| 221 | // Update lastSeen based on status | 232 | // Update lastSeen based on status | ||
| 222 | lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen) | 233 | lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen) | ||
| 223 | }; | 234 | }; | ||
| 224 | - | 235 | + | ||
| 225 | // Update cache first | 236 | // Update cache first | ||
| 226 | this.userCache[userId] = updatedUser; | 237 | this.userCache[userId] = updatedUser; | ||
| 227 | - | 238 | + | ||
| 228 | // Then update or add to users array | 239 | // Then update or add to users array | ||
| 229 | const index = this.users.findIndex((u: User) => u.id === userId); | 240 | const index = this.users.findIndex((u: User) => u.id === userId); | ||
| 230 | if (index !== -1) { | 241 | if (index !== -1) { | ||
| @@ -237,7 +248,7 @@ class ChatState { | |||||
| 237 | } else { | 248 | } else { | ||
| 238 | this.users = [...this.users, updatedUser]; | 249 | this.users = [...this.users, updatedUser]; | ||
| 239 | } | 250 | } | ||
| 240 | - | 251 | + | ||
| 241 | console.debug('User status updated:', { | 252 | console.debug('User status updated:', { | ||
| 242 | userId, | 253 | userId, | ||
| 243 | newStatus: status, | 254 | newStatus: status, | ||
| @@ -553,6 +564,18 @@ class ChatState { | |||||
| 553 | console.debug('Error handling SSE chat message:', error); | 564 | console.debug('Error handling SSE chat message:', error); | ||
| 554 | } | 565 | } | ||
| 555 | }); | 566 | }); | ||
| 567 | + | ||||
| 568 | + | // Listen for user status updates to update the buddy list dynamically | |||
| 569 | + | eventSource.addEventListener('userStatusUpdate', async (event: MessageEvent) => { | |||
| 570 | + | try { | |||
| 571 | + | const statusUpdate = JSON.parse(event.data) as { userId: string; status: User['status']; lastSeen: number }; | |||
| 572 | + | console.debug('Received userStatusUpdate via SSE:', statusUpdate); | |||
| 573 | + | // Update the user status and ensure buddy list is updated appropriately | |||
| 574 | + | this.updateUserStatus(statusUpdate.userId, statusUpdate.status, statusUpdate.lastSeen); | |||
| 575 | + | } catch (error) { | |||
| 576 | + | console.debug('Error handling userStatusUpdate via SSE:', error); | |||
| 577 | + | } | |||
| 578 | + | }); | |||
| 556 | } | 579 | } | ||
| 557 | 580 | ||||
| 558 | private async ensureUserData(userId: string) { | 581 | private async ensureUserData(userId: string) { | ||
M src/routes/+layout.svelte +19 -8
| @@ -10,9 +10,25 @@ | |||
| 10 | 10 | let lastUserUpdate = $state<string | null>(null); | |
| 11 | 11 | let updateInProgress = $state(false); | |
| 12 | 12 | ||
| 13 | + | // New throttle state variables | |
| 14 | + | let lastSentStatus = $state<string | null>(null); | |
| 15 | + | let lastStatusSentAt = $state<number>(0); | |
| 16 | + | const THROTTLE_TIME_MS = 5000; | |
| 17 | + | ||
| 13 | 18 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | |
| 14 | 19 | if (!data.user?.id || updateInProgress) return; | |
| 15 | 20 | ||
| 21 | + | // Throttle duplicate status updates | |
| 22 | + | if ( | |
| 23 | + | status === lastSentStatus && | |
| 24 | + | Date.now() - lastStatusSentAt < THROTTLE_TIME_MS | |
| 25 | + | ) { | |
| 26 | + | console.debug('Throttling duplicate status update:', status); | |
| 27 | + | return; | |
| 28 | + | } | |
| 29 | + | lastSentStatus = status; | |
| 30 | + | lastStatusSentAt = Date.now(); | |
| 31 | + | ||
| 16 | 32 | updateInProgress = true; | |
| 17 | 33 | const timestamp = new Date().toISOString(); | |
| 18 | 34 | console.debug('Updating user status:', { | |
| @@ -20,7 +36,7 @@ | |||
| 20 | 36 | status, | |
| 21 | 37 | timestamp, | |
| 22 | 38 | trigger: new Error().stack?.split('\n')[2]?.trim(), | |
| 23 | - | hasSession: !!data.session, | |
| 39 | + | hasSession: true, | |
| 24 | 40 | cookiePresent: browser ? document.cookie.includes('session=') : false | |
| 25 | 41 | }); | |
| 26 | 42 | ||
| @@ -67,8 +83,7 @@ | |||
| 67 | 83 | }; | |
| 68 | 84 | ||
| 69 | 85 | statusInterval = window.setInterval(tickHandler, 60000); // Every minute | |
| 70 | - | // Run immediately on start | |
| 71 | - | tickHandler(); | |
| 86 | + | // Removed immediate tickHandler() call to avoid duplicate update on mount | |
| 72 | 87 | } | |
| 73 | 88 | ||
| 74 | 89 | function stopStatusTicker() { | |
| @@ -83,7 +98,6 @@ | |||
| 83 | 98 | isVisible = document.visibilityState === 'visible'; | |
| 84 | 99 | updateUserStatus(isVisible ? 'online' : 'busy'); | |
| 85 | 100 | ||
| 86 | - | // Start or stop the ticker based on visibility | |
| 87 | 101 | if (isVisible) { | |
| 88 | 102 | startStatusTicker(); | |
| 89 | 103 | } else { | |
| @@ -105,19 +119,17 @@ | |||
| 105 | 119 | // Set initial online status | |
| 106 | 120 | updateUserStatus('online'); | |
| 107 | 121 | ||
| 108 | - | // Start the status ticker | |
| 122 | + | // Start the status ticker (which will now only fire every minute) | |
| 109 | 123 | startStatusTicker(); | |
| 110 | 124 | ||
| 111 | 125 | // Add visibility change listener | |
| 112 | 126 | document.addEventListener('visibilitychange', handleVisibilityChange); | |
| 113 | 127 | ||
| 114 | - | // Cleanup on unmount | |
| 115 | 128 | return () => { | |
| 116 | 129 | window.removeEventListener('beforeunload', cleanup); | |
| 117 | 130 | }; | |
| 118 | 131 | }); | |
| 119 | 132 | ||
| 120 | - | ||
| 121 | 133 | $effect(() => { | |
| 122 | 134 | if (!browser) return; | |
| 123 | 135 | ||
| @@ -146,7 +158,6 @@ | |||
| 146 | 158 | isVisible | |
| 147 | 159 | }); | |
| 148 | 160 | ||
| 149 | - | // Only update chat state if we have valid session data | |
| 150 | 161 | if (data.session?.id) { | |
| 151 | 162 | console.debug('Updating global chatState with data.user:', data.user); | |
| 152 | 163 | chatState.setCurrentUser(data.user); | |
| @@ -10,9 +10,25 @@ | |||||
| 10 | let lastUserUpdate = $state<string | null>(null); | 10 | let lastUserUpdate = $state<string | null>(null); | ||
| 11 | let updateInProgress = $state(false); | 11 | let updateInProgress = $state(false); | ||
| 12 | 12 | ||||
| 13 | + | // New throttle state variables | |||
| 14 | + | let lastSentStatus = $state<string | null>(null); | |||
| 15 | + | let lastStatusSentAt = $state<number>(0); | |||
| 16 | + | const THROTTLE_TIME_MS = 5000; | |||
| 17 | + | ||||
| 13 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | 18 | async function updateUserStatus(status: 'online' | 'offline' | 'busy') { | ||
| 14 | if (!data.user?.id || updateInProgress) return; | 19 | if (!data.user?.id || updateInProgress) return; | ||
| 15 | 20 | ||||
| 21 | + | // Throttle duplicate status updates | |||
| 22 | + | if ( | |||
| 23 | + | status === lastSentStatus && | |||
| 24 | + | Date.now() - lastStatusSentAt < THROTTLE_TIME_MS | |||
| 25 | + | ) { | |||
| 26 | + | console.debug('Throttling duplicate status update:', status); | |||
| 27 | + | return; | |||
| 28 | + | } | |||
| 29 | + | lastSentStatus = status; | |||
| 30 | + | lastStatusSentAt = Date.now(); | |||
| 31 | + | ||||
| 16 | updateInProgress = true; | 32 | updateInProgress = true; | ||
| 17 | const timestamp = new Date().toISOString(); | 33 | const timestamp = new Date().toISOString(); | ||
| 18 | console.debug('Updating user status:', { | 34 | console.debug('Updating user status:', { | ||
| @@ -20,7 +36,7 @@ | |||||
| 20 | status, | 36 | status, | ||
| 21 | timestamp, | 37 | timestamp, | ||
| 22 | trigger: new Error().stack?.split('\n')[2]?.trim(), | 38 | trigger: new Error().stack?.split('\n')[2]?.trim(), | ||
| 23 | - | hasSession: !!data.session, | 39 | + | hasSession: true, |
| 24 | cookiePresent: browser ? document.cookie.includes('session=') : false | 40 | cookiePresent: browser ? document.cookie.includes('session=') : false | ||
| 25 | }); | 41 | }); | ||
| 26 | 42 | ||||
| @@ -67,8 +83,7 @@ | |||||
| 67 | }; | 83 | }; | ||
| 68 | 84 | ||||
| 69 | statusInterval = window.setInterval(tickHandler, 60000); // Every minute | 85 | statusInterval = window.setInterval(tickHandler, 60000); // Every minute | ||
| 70 | - | // Run immediately on start | 86 | + | // Removed immediate tickHandler() call to avoid duplicate update on mount |
| 71 | - | tickHandler(); | |||
| 72 | } | 87 | } | ||
| 73 | 88 | ||||
| 74 | function stopStatusTicker() { | 89 | function stopStatusTicker() { | ||
| @@ -83,7 +98,6 @@ | |||||
| 83 | isVisible = document.visibilityState === 'visible'; | 98 | isVisible = document.visibilityState === 'visible'; | ||
| 84 | updateUserStatus(isVisible ? 'online' : 'busy'); | 99 | updateUserStatus(isVisible ? 'online' : 'busy'); | ||
| 85 | 100 | ||||
| 86 | - | // Start or stop the ticker based on visibility | |||
| 87 | if (isVisible) { | 101 | if (isVisible) { | ||
| 88 | startStatusTicker(); | 102 | startStatusTicker(); | ||
| 89 | } else { | 103 | } else { | ||
| @@ -105,19 +119,17 @@ | |||||
| 105 | // Set initial online status | 119 | // Set initial online status | ||
| 106 | updateUserStatus('online'); | 120 | updateUserStatus('online'); | ||
| 107 | 121 | ||||
| 108 | - | // Start the status ticker | 122 | + | // Start the status ticker (which will now only fire every minute) |
| 109 | startStatusTicker(); | 123 | startStatusTicker(); | ||
| 110 | 124 | ||||
| 111 | // Add visibility change listener | 125 | // Add visibility change listener | ||
| 112 | document.addEventListener('visibilitychange', handleVisibilityChange); | 126 | document.addEventListener('visibilitychange', handleVisibilityChange); | ||
| 113 | 127 | ||||
| 114 | - | // Cleanup on unmount | |||
| 115 | return () => { | 128 | return () => { | ||
| 116 | window.removeEventListener('beforeunload', cleanup); | 129 | window.removeEventListener('beforeunload', cleanup); | ||
| 117 | }; | 130 | }; | ||
| 118 | }); | 131 | }); | ||
| 119 | 132 | ||||
| 120 | - | ||||
| 121 | $effect(() => { | 133 | $effect(() => { | ||
| 122 | if (!browser) return; | 134 | if (!browser) return; | ||
| 123 | 135 | ||||
| @@ -146,7 +158,6 @@ | |||||
| 146 | isVisible | 158 | isVisible | ||
| 147 | }); | 159 | }); | ||
| 148 | 160 | ||||
| 149 | - | // Only update chat state if we have valid session data | |||
| 150 | if (data.session?.id) { | 161 | if (data.session?.id) { | ||
| 151 | console.debug('Updating global chatState with data.user:', data.user); | 162 | console.debug('Updating global chatState with data.user:', data.user); | ||
| 152 | chatState.setCurrentUser(data.user); | 163 | chatState.setCurrentUser(data.user); | ||