detached cee1fd8

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 UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +49 -15
M src/lib/states/chat.svelte.ts +30 -7
@@ -194,14 +194,25 @@ class ChatState {
194194 }
195195
196196 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,
200200 lastSeen,
201201 currentCache: this.userCache[userId],
202202 currentUsers: this.users.find(u => u.id === userId)
203203 });
204204
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+
205216 // Try to get the user from cache first
206217 let user: User | undefined = this.userCache[userId];
207218
@@ -212,7 +223,7 @@ class ChatState {
212223 user = foundUser;
213224 }
214225 }
215-
226+
216227 const now = Date.now();
217228 if (user) {
218229 const updatedUser: User = {
@@ -221,10 +232,10 @@ class ChatState {
221232 // Update lastSeen based on status
222233 lastSeen: lastSeen ?? (status === 'offline' ? now : user.lastSeen)
223234 };
224-
235+
225236 // Update cache first
226237 this.userCache[userId] = updatedUser;
227-
238+
228239 // Then update or add to users array
229240 const index = this.users.findIndex((u: User) => u.id === userId);
230241 if (index !== -1) {
@@ -237,7 +248,7 @@ class ChatState {
237248 } else {
238249 this.users = [...this.users, updatedUser];
239250 }
240-
251+
241252 console.debug('User status updated:', {
242253 userId,
243254 newStatus: status,
@@ -553,6 +564,18 @@ class ChatState {
553564 console.debug('Error handling SSE chat message:', error);
554565 }
555566 });
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+ });
556579 }
557580
558581 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 first216 // 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 status232 // 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 first236 // Update cache first
226 this.userCache[userId] = updatedUser;237 this.userCache[userId] = updatedUser;
227-238+
228 // Then update or add to users array239 // 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 @@
1010 let lastUserUpdate = $state<string | null>(null);
1111 let updateInProgress = $state(false);
1212
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+
1318 async function updateUserStatus(status: 'online' | 'offline' | 'busy') {
1419 if (!data.user?.id || updateInProgress) return;
1520
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+
1632 updateInProgress = true;
1733 const timestamp = new Date().toISOString();
1834 console.debug('Updating user status:', {
@@ -20,7 +36,7 @@
2036 status,
2137 timestamp,
2238 trigger: new Error().stack?.split('\n')[2]?.trim(),
23- hasSession: !!data.session,
39+ hasSession: true,
2440 cookiePresent: browser ? document.cookie.includes('session=') : false
2541 });
2642
@@ -67,8 +83,7 @@
6783 };
6884
6985 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
7287 }
7388
7489 function stopStatusTicker() {
@@ -83,7 +98,6 @@
8398 isVisible = document.visibilityState === 'visible';
8499 updateUserStatus(isVisible ? 'online' : 'busy');
85100
86- // Start or stop the ticker based on visibility
87101 if (isVisible) {
88102 startStatusTicker();
89103 } else {
@@ -105,19 +119,17 @@
105119 // Set initial online status
106120 updateUserStatus('online');
107121
108- // Start the status ticker
122+ // Start the status ticker (which will now only fire every minute)
109123 startStatusTicker();
110124
111125 // Add visibility change listener
112126 document.addEventListener('visibilitychange', handleVisibilityChange);
113127
114- // Cleanup on unmount
115128 return () => {
116129 window.removeEventListener('beforeunload', cleanup);
117130 };
118131 });
119132
120-
121133 $effect(() => {
122134 if (!browser) return;
123135
@@ -146,7 +158,6 @@
146158 isVisible
147159 });
148160
149- // Only update chat state if we have valid session data
150161 if (data.session?.id) {
151162 console.debug('Updating global chatState with data.user:', data.user);
152163 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=') : false40 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 minute85 statusInterval = window.setInterval(tickHandler, 60000); // Every minute
70- // Run immediately on start86+ // 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 status119 // Set initial online status
106 updateUserStatus('online');120 updateUserStatus('online');
107 121
108- // Start the status ticker122+ // Start the status ticker (which will now only fire every minute)
109 startStatusTicker();123 startStatusTicker();
110 124
111 // Add visibility change listener125 // 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 isVisible158 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);

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close