Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/server/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const eventsRoutes: FastifyPluginAsyncTypebox = async (fastify) => {

// Set up file watcher for live updates
let fileWatcher: FSWatcher | null = null
let closed = false
const repoPath = fastify.config.repositoryPath

// Debounced broadcast to avoid flooding on rapid file changes
Expand All @@ -82,6 +83,9 @@ const eventsRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
// Load gitignore patterns for filtering
const ig = await loadGitignore(repoPath, fastify.log)

// Server may have shut down while we were scanning
if (closed) return

fileWatcher = chokidar.watch(repoPath, {
ignored: (filePath: string) => {
// Get path relative to repo root for gitignore matching
Expand Down Expand Up @@ -121,11 +125,15 @@ const eventsRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
}
}

// Start watching immediately (could optimize to start only when clients connect)
await startWatching()
// Start watching in the background to avoid blocking plugin registration.
// On large repos, loadGitignore can exceed Fastify's default plugin timeout.
startWatching().catch((err) => {
fastify.log.warn({ err }, 'Unexpected error starting file watcher')
})

// Clean up on server close
fastify.addHook('onClose', async () => {
closed = true
// Close all active SSE connections
fastify.log.info({ clients: activeConnections.size }, 'Closing active SSE connections')
for (const connection of activeConnections) {
Expand Down
35 changes: 13 additions & 22 deletions src/server/utils/gitignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Utilities for loading and parsing .gitignore files
*/
import ignore, { type Ignore } from 'ignore'
import { readdir, readFile, stat } from 'node:fs/promises'
import { readdir, readFile } from 'node:fs/promises'
import { dirname, join, relative, sep } from 'node:path'
import type { FastifyBaseLogger } from 'fastify'

Expand All @@ -22,33 +22,24 @@ export async function findGitignoreFiles (dir: string, log?: Logger): Promise<st

while (dirIndex < dirsToWalk.length) {
const currentDir = dirsToWalk[dirIndex++]
let entries: string[]
try {
entries = await readdir(currentDir)
} catch (err) {
log?.warn({ err, path: currentDir }, 'Failed to read directory while scanning for .gitignore files')
continue
}

for (const entry of entries) {
const fullPath = join(currentDir, entry)
const entries = await readdir(currentDir, { withFileTypes: true })

// Skip .git and node_modules for performance
if (entry === '.git' || entry === 'node_modules') continue
for (const entry of entries) {
// Skip .git and node_modules for performance
if (entry.name === '.git' || entry.name === 'node_modules') continue

if (entry === '.gitignore') {
gitignoreFiles.push(fullPath)
continue
}
if (entry.name === '.gitignore') {
gitignoreFiles.push(join(currentDir, entry.name))
continue
}

try {
const stats = await stat(fullPath)
if (stats.isDirectory()) {
dirsToWalk.push(fullPath)
if (entry.isDirectory()) {
dirsToWalk.push(join(currentDir, entry.name))
}
} catch {
// Skip entries we can't stat (common for broken symlinks)
}
} catch (err) {
log?.warn({ err, path: currentDir }, 'Failed to read directory while scanning for .gitignore files')
}
}

Expand Down