v0.1.1 88fcae1

+server.ts RawHistoryFolder

1import type { RequestHandler } from './$types';
2import { sseEmitter } from '$lib/sseEmitter';
3import db from '$lib/db/db.server';
4import { users } from '$lib/db/schema';
5import { eq, and, lt } from 'drizzle-orm';
6
7console.debug('SSE connection initiated');
8
9// Constants for timeout checks
10const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
11const CHECK_INTERVAL_MS = 60 * 1000; // Check every minute
12
13// Initialize periodic check for timed out users
14const timeoutInterval = setInterval(async () => {
15    const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS;
16    console.debug('Checking for timed out users, threshold:', new Date(timeoutThreshold));
17
18    try {
19        // Find and update users who are marked as online but haven't been seen recently
20        const now = Date.now();
21        const result = await db.update(users)
22            .set({ 
23                status: 'offline',
24                lastSeen: now
25            })
26            .where(
27                and(
28                    eq(users.status, 'online'),
29                    lt(users.lastSeen, timeoutThreshold)
30                )
31            )
32            .returning({ id: users.id });
33
34        // Broadcast status updates for timed out users
35        result.forEach(user => {
36            sseEmitter.emit('sse', {
37                type: 'userStatusUpdate',
38                data: {
39                    userId: user.id,
40                    status: 'offline',
41                    lastSeen: now
42                }
43            });
44            console.debug('User timed out and marked offline:', user.id);
45        });
46    } catch (error) {
47        console.debug('Error during timeout check:', error);
48    }
49}, CHECK_INTERVAL_MS);
50
51// Ensure the interval is cleared if the module is reloaded
52process.on('beforeExit', () => {
53    if (timeoutInterval) clearInterval(timeoutInterval);
54});
55
56export const GET: RequestHandler = async ({ locals }) => {
57    if (!locals.user) {
58        return new Response('Unauthorized', { status: 401 });
59    }
60
61    const userId = locals.user.id;
62    const encoder = new TextEncoder();
63    let keepAliveInterval: ReturnType<typeof setInterval>;
64    let onSSE: ((event: { type: string; data: unknown }) => void) | undefined;
65
66    // Update user status to online and lastSeen timestamp when establishing connection
67    const now = Date.now();
68    await db.update(users)
69        .set({ 
70            status: 'online',
71            lastSeen: now 
72        })
73        .where(eq(users.id, userId));
74
75    // Broadcast the status update
76    sseEmitter.emit('sse', {
77        type: 'userStatusUpdate',
78        data: {
79            userId,
80            status: 'online',
81            lastSeen: now
82        }
83    });
84
85    const stream = new ReadableStream({
86        start(controller) {
87            const sendEvent = (event: { type: string; data: unknown }) => {
88                try {
89                    const payload = `event: ${event.type}\n` +
90                                    `data: ${JSON.stringify(event.data)}\n\n`;
91                    controller.enqueue(encoder.encode(payload));
92                    console.debug('Sent SSE event:', event);
93                } catch (err) {
94                    console.debug('Error sending SSE event:', err);
95                }
96            };
97
98            onSSE = (event: { type: string; data: unknown }) => {
99                sendEvent(event);
100            };
101
102            sseEmitter.addListener('sse', onSSE);
103            console.debug('Added SSE listener');
104
105            // Send initial connection message
106            controller.enqueue(encoder.encode('data: Connected\n\n'));
107
108            // Keep connection alive and update lastSeen: send a comment every 20 seconds
109            keepAliveInterval = setInterval(async () => {
110                controller.enqueue(encoder.encode(':\n\n'));
111                // Update lastSeen timestamp on each keepalive
112                await db.update(users)
113                    .set({ lastSeen: Date.now() })
114                    .where(eq(users.id, userId));
115            }, 20000);
116        },
117        async cancel(reason) {
118            if (keepAliveInterval) clearInterval(keepAliveInterval);
119            if (onSSE) sseEmitter.removeListener('sse', onSSE);
120            console.debug('SSE connection cancelled:', reason);
121
122            // Update user status to offline in the database
123            const now = Date.now();
124            await db.update(users)
125                .set({ status: 'offline', lastSeen: now })
126                .where(eq(users.id, userId));
127            
128            // Broadcast the status update
129            sseEmitter.emit('sse', { 
130                type: 'userStatusUpdate', 
131                data: { 
132                    userId, 
133                    status: 'offline', 
134                    lastSeen: now 
135                } 
136            });
137        }
138    });
139
140    return new Response(stream, {
141        headers: {
142            'Content-Type': 'text/event-stream',
143            'Cache-Control': 'no-cache',
144            'Connection': 'keep-alive'
145        }
146    });
147}; 

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