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>
54 lines
1.8 KiB
TypeScript
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));
|
|
});
|
|
});
|
|
}
|