Files
boocode/apps/server/src/services/file_index.ts
indifferentketchup 2f6be39efd chore: surface swallowed errors + remove dead session_renamed paths
Swallowed-error logging (audit Feature 3):
- file_index.ts:36-37 (git mtime probes): comment — best-effort, project
  may not be a git repo.
- useUserEvents.ts:44 / 53 (ws.close on error / unmount): comments —
  best-effort, socket may already be closing.
- RightRail.tsx:38 (localStorage write): comment — best-effort, quota or
  private mode.
- App.tsx:21 (api.sessions.get for RightRail projectId): replaced silent
  catch with console.warn.
- Session.tsx:38, 41 (session fetch + project list for breadcrumb):
  replaced silent catches with console.warn.

H1: ProjectSidebar.tsx:189 — dropped the local sessionEvents.emit
({type:'session_renamed'}) after PATCH. Server publishes via
broker.publishUser since v1.4; useUserEvents forwards.

H2: useSessionStream.ts session_renamed case removed (dead — no
server code path publishes session_renamed on the per-session WS
channel; only user channel via broker.publishUser). Also dropped the
session_renamed variant from WsFrame (in apps/web/src/api/types.ts)
to keep the discriminated-union switch exhaustive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 04:35:49 +00:00

54 lines
1.8 KiB
TypeScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { execFile } from 'node:child_process';
interface MtimeSnap {
root: number;
gitHead: number | null;
gitIndex: number | null;
}
interface CacheEntry {
files: string[];
mtimes: MtimeSnap;
}
const cache = new Map<string, CacheEntry>(); // keyed by projectId
// Concurrent calls with a cold/stale cache may both spawn rg. The result is
// deterministic so they overwrite identically — no data corruption, just a
// rare extra subprocess. Acceptable for single-user mode.
export async function getProjectFiles(projectId: string, projectRoot: string): Promise<string[]> {
const current = await snapMtimes(projectRoot);
const cached = cache.get(projectId);
if (cached && eqMtimes(cached.mtimes, current)) {
return cached.files;
}
const files = await runRgFiles(projectRoot);
cache.set(projectId, { files, mtimes: current });
return files;
}
async function snapMtimes(root: string): Promise<MtimeSnap> {
const rootStat = await fs.stat(root);
let gitHead: number | null = null;
let gitIndex: number | null = null;
// best-effort; ignore failure because the project may not be a git repo
try { gitHead = (await fs.stat(path.join(root, '.git', 'HEAD'))).mtimeMs; } catch {}
try { gitIndex = (await fs.stat(path.join(root, '.git', 'index'))).mtimeMs; } catch {}
return { root: rootStat.mtimeMs, gitHead, gitIndex };
}
function eqMtimes(a: MtimeSnap, b: MtimeSnap): boolean {
return a.root === b.root && a.gitHead === b.gitHead && a.gitIndex === b.gitIndex;
}
function runRgFiles(root: string): Promise<string[]> {
return new Promise((resolve, reject) => {
execFile('rg', ['--files'], { cwd: root, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
if (err) return reject(err);
resolve(stdout.split('\n').filter(Boolean));
});
});
}