detached 8d5ef03

fix: race condition in SSE authentication and status updates

commit
8d5ef03c5b922f38c6806590952cc09b9e3a0788
author
Kit · · 2025-02-14 16:58 UTC
parent
ca9d24a
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +64 -20
M src/routes/+layout.svelte +20 -2
@@ -8,19 +8,29 @@
88 let isVisible = $state(true);
99 let statusInterval = $state<number | undefined>(undefined);
1010 let lastUserUpdate = $state<string | null>(null);
11+ let updateInProgress = $state(false);
1112
1213 async function updateUserStatus(status: 'online' | 'offline' | 'busy') {
13- if (!data.user?.id) return;
14+ if (!data.user?.id || updateInProgress) return;
1415
16+ updateInProgress = true;
1517 const timestamp = new Date().toISOString();
1618 console.debug('Updating user status:', {
1719 userId: data.user.id,
1820 status,
1921 timestamp,
20- trigger: new Error().stack?.split('\n')[2]?.trim()
22+ trigger: new Error().stack?.split('\n')[2]?.trim(),
23+ hasSession: !!data.session,
24+ cookiePresent: browser ? document.cookie.includes('session=') : false
2125 });
2226
2327 try {
28+ // Add a small delay to ensure cookie is set after login
29+ if (status === 'online' && !document.cookie.includes('session=')) {
30+ console.debug('Waiting for session cookie before status update...');
31+ await new Promise(resolve => setTimeout(resolve, 500));
32+ }
33+
2434 const response = await fetch('/api/status', {
2535 method: 'POST',
2636 headers: { 'Content-Type': 'application/json' },
@@ -32,9 +42,17 @@
3242
3343 if (!response.ok) {
3444 console.error('Failed to update user status:', await response.json());
45+ } else {
46+ console.debug('Status update successful:', {
47+ userId: data.user.id,
48+ status,
49+ cookiePresent: document.cookie.includes('session=')
50+ });
3551 }
3652 } catch (error) {
3753 console.error('Error updating user status:', error);
54+ } finally {
55+ updateInProgress = false;
3856 }
3957 }
4058
@@ -8,19 +8,29 @@
8 let isVisible = $state(true);8 let isVisible = $state(true);
9 let statusInterval = $state<number | undefined>(undefined);9 let statusInterval = $state<number | undefined>(undefined);
10 let lastUserUpdate = $state<string | null>(null);10 let lastUserUpdate = $state<string | null>(null);
11+ let updateInProgress = $state(false);
11 12
12 async function updateUserStatus(status: 'online' | 'offline' | 'busy') {13 async function updateUserStatus(status: 'online' | 'offline' | 'busy') {
13- if (!data.user?.id) return;14+ if (!data.user?.id || updateInProgress) return;
14 15
16+ updateInProgress = true;
15 const timestamp = new Date().toISOString();17 const timestamp = new Date().toISOString();
16 console.debug('Updating user status:', {18 console.debug('Updating user status:', {
17 userId: data.user.id,19 userId: data.user.id,
18 status,20 status,
19 timestamp,21 timestamp,
20- trigger: new Error().stack?.split('\n')[2]?.trim()22+ trigger: new Error().stack?.split('\n')[2]?.trim(),
23+ hasSession: !!data.session,
24+ cookiePresent: browser ? document.cookie.includes('session=') : false
21 });25 });
22 26
23 try {27 try {
28+ // Add a small delay to ensure cookie is set after login
29+ if (status === 'online' && !document.cookie.includes('session=')) {
30+ console.debug('Waiting for session cookie before status update...');
31+ await new Promise(resolve => setTimeout(resolve, 500));
32+ }
33+
24 const response = await fetch('/api/status', {34 const response = await fetch('/api/status', {
25 method: 'POST',35 method: 'POST',
26 headers: { 'Content-Type': 'application/json' },36 headers: { 'Content-Type': 'application/json' },
@@ -32,9 +42,17 @@
32 42
33 if (!response.ok) {43 if (!response.ok) {
34 console.error('Failed to update user status:', await response.json());44 console.error('Failed to update user status:', await response.json());
45+ } else {
46+ console.debug('Status update successful:', {
47+ userId: data.user.id,
48+ status,
49+ cookiePresent: document.cookie.includes('session=')
50+ });
35 }51 }
36 } catch (error) {52 } catch (error) {
37 console.error('Error updating user status:', error);53 console.error('Error updating user status:', error);
54+ } finally {
55+ updateInProgress = false;
38 }56 }
39 }57 }
40 58
M src/routes/api/sse/+server.ts +44 -18
@@ -95,9 +95,24 @@ if (typeof process !== 'undefined') {
9595 });
9696 }
9797
98-export const GET: RequestHandler = async ({ request, locals }) => {
98+export const GET: RequestHandler = async ({ request, locals, cookies }) => {
99+ // Add detailed debug logging for authentication state
100+ console.log('[SSE] Connection attempt:', {
101+ hasLocals: !!locals,
102+ hasUser: !!locals.user,
103+ userId: locals.user?.id,
104+ hasSessionCookie: !!cookies.get('session'),
105+ headers: Object.fromEntries(request.headers.entries())
106+ });
107+
99108 if (!locals.user) {
100- return new Response('Unauthorized', { status: 401 });
109+ console.log('[SSE] Authentication failed - no user in locals');
110+ return new Response('Unauthorized', {
111+ status: 401,
112+ headers: {
113+ 'Content-Type': 'application/json'
114+ }
115+ });
101116 }
102117
103118 const userId = locals.user.id;
@@ -107,22 +122,33 @@ export const GET: RequestHandler = async ({ request, locals }) => {
107122
108123 // Update user status to online and lastSeen timestamp when establishing connection
109124 const now = Date.now();
110- await db.update(users)
111- .set({
112- status: 'online',
113- lastSeen: now
114- })
115- .where(eq(users.id, userId));
116-
117- // Broadcast the status update
118- sseEmitter.emit('sse', {
119- type: 'userStatusUpdate',
120- data: {
121- userId,
122- status: 'online',
123- lastSeen: now
124- }
125- });
125+
126+ try {
127+ await db.update(users)
128+ .set({
129+ status: 'online',
130+ lastSeen: now
131+ })
132+ .where(eq(users.id, userId));
133+
134+ console.log('[SSE] User status updated to online:', {
135+ userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`,
136+ timestamp: new Date(now).toISOString()
137+ });
138+
139+ // Broadcast the status update
140+ sseEmitter.emit('sse', {
141+ type: 'userStatusUpdate',
142+ data: {
143+ userId,
144+ status: 'online',
145+ lastSeen: now
146+ }
147+ });
148+ } catch (error) {
149+ console.error('[SSE] Error updating user status:', error);
150+ // Continue with SSE setup even if status update fails
151+ }
126152
127153 const stream = new ReadableStream({
128154 start(controller) {
@@ -95,9 +95,24 @@ if (typeof process !== 'undefined') {
95 });95 });
96 }96 }
97 97
98-export const GET: RequestHandler = async ({ request, locals }) => {98+export const GET: RequestHandler = async ({ request, locals, cookies }) => {
99+ // Add detailed debug logging for authentication state
100+ console.log('[SSE] Connection attempt:', {
101+ hasLocals: !!locals,
102+ hasUser: !!locals.user,
103+ userId: locals.user?.id,
104+ hasSessionCookie: !!cookies.get('session'),
105+ headers: Object.fromEntries(request.headers.entries())
106+ });
107+
99 if (!locals.user) {108 if (!locals.user) {
100- return new Response('Unauthorized', { status: 401 });109+ console.log('[SSE] Authentication failed - no user in locals');
110+ return new Response('Unauthorized', {
111+ status: 401,
112+ headers: {
113+ 'Content-Type': 'application/json'
114+ }
115+ });
101 }116 }
102 117
103 const userId = locals.user.id;118 const userId = locals.user.id;
@@ -107,22 +122,33 @@ export const GET: RequestHandler = async ({ request, locals }) => {
107 122
108 // Update user status to online and lastSeen timestamp when establishing connection123 // Update user status to online and lastSeen timestamp when establishing connection
109 const now = Date.now();124 const now = Date.now();
110- await db.update(users)125+
111- .set({ 126+ try {
112- status: 'online',127+ await db.update(users)
113- lastSeen: now 128+ .set({
114- })129+ status: 'online',
115- .where(eq(users.id, userId));130+ lastSeen: now
116-131+ })
117- // Broadcast the status update132+ .where(eq(users.id, userId));
118- sseEmitter.emit('sse', {133+
119- type: 'userStatusUpdate',134+ console.log('[SSE] User status updated to online:', {
120- data: {135+ userId: `${userId.slice(0, 4)}...${userId.slice(-4)}`,
121- userId,136+ timestamp: new Date(now).toISOString()
122- status: 'online',137+ });
123- lastSeen: now138+
124- }139+ // Broadcast the status update
125- });140+ sseEmitter.emit('sse', {
141+ type: 'userStatusUpdate',
142+ data: {
143+ userId,
144+ status: 'online',
145+ lastSeen: now
146+ }
147+ });
148+ } catch (error) {
149+ console.error('[SSE] Error updating user status:', error);
150+ // Continue with SSE setup even if status update fails
151+ }
126 152
127 const stream = new ReadableStream({153 const stream = new ReadableStream({
128 start(controller) {154 start(controller) {

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