mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
perf(cli): tell the preview about a file edit 30 ms after it lands, not 300 (#4348)
* perf(cli): tell the preview about a file edit 30 ms after it lands, not 300 * perf(cli): keep bulk writes coalesced while lone saves go out fast * style(cli): name the watcher delay * perf(cli): throttle watcher flushes instead of switching to a long wait
This commit is contained in:
@@ -67,7 +67,9 @@ describe("createProjectWatcher", () => {
|
||||
mockWatcher.emit("change", "change", "scene-a.html");
|
||||
mockWatcher.emit("change", "change", "scene-b.html");
|
||||
mockWatcher.emit("change", "change", "scene-a.html");
|
||||
vi.advanceTimersByTime(300);
|
||||
vi.advanceTimersByTime(29);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
|
||||
expect(listener.mock.calls).toEqual([["scene-a.html"], ["scene-b.html"]]);
|
||||
projectWatcher.close();
|
||||
@@ -97,6 +99,32 @@ describe("createProjectWatcher", () => {
|
||||
expect(() => projectWatcher?.close()).not.toThrow();
|
||||
});
|
||||
|
||||
it("flushes at most once per 300 ms while writes keep coming", () => {
|
||||
vi.useFakeTimers();
|
||||
const projectWatcher = createProjectWatcher("/fake/project/dir");
|
||||
const listener = vi.fn();
|
||||
projectWatcher.addListener(listener);
|
||||
|
||||
mockWatcher.emit("change", "change", "a.html");
|
||||
vi.advanceTimersByTime(30);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockWatcher.emit("change", "change", "b.html");
|
||||
vi.advanceTimersByTime(100);
|
||||
mockWatcher.emit("change", "change", "c.html");
|
||||
vi.advanceTimersByTime(199);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(listener.mock.calls.slice(1)).toEqual([["b.html"], ["c.html"]]);
|
||||
|
||||
// A save that lands late in the window waits only for quiet, not a whole window.
|
||||
vi.advanceTimersByTime(290);
|
||||
mockWatcher.emit("change", "change", "d.html");
|
||||
vi.advanceTimersByTime(30);
|
||||
expect(listener).toHaveBeenLastCalledWith("d.html");
|
||||
projectWatcher.close();
|
||||
});
|
||||
|
||||
// Regression: fs.watch can fail asynchronously (e.g. EMFILE from exhausted
|
||||
// OS watch handles) via an 'error' event, not a thrown exception. An
|
||||
// EventEmitter 'error' with no listener crashes the whole process — this
|
||||
|
||||
@@ -26,7 +26,10 @@ const WATCHER_EXCLUDED_DIRS = new Set([
|
||||
"outputs",
|
||||
"renders",
|
||||
]);
|
||||
const DEBOUNCE_MS = 300;
|
||||
// A save reaches the preview QUIET_MS after the writes go quiet, but at most once per BURST_MS,
|
||||
// so a checkout or a multi-file tool doesn't start a rebuild for every file.
|
||||
const QUIET_MS = 30;
|
||||
const BURST_MS = 300;
|
||||
|
||||
export function shouldWatchProjectFile(filename: string): boolean {
|
||||
if (!filename) return false;
|
||||
@@ -112,6 +115,7 @@ export function createProjectWatcher(projectDir: string): ProjectWatcher {
|
||||
const listeners = new Set<FileChangeListener>();
|
||||
const pendingPaths = new Set<string>();
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let lastFlushAt = Number.NEGATIVE_INFINITY;
|
||||
let closeTree: (() => void) | null = null;
|
||||
|
||||
try {
|
||||
@@ -130,16 +134,18 @@ export function createProjectWatcher(projectDir: string): ProjectWatcher {
|
||||
|
||||
pendingPaths.add(relativePath);
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
const delay = Math.max(QUIET_MS, lastFlushAt + BURST_MS - Date.now());
|
||||
debounceTimer = setTimeout(() => {
|
||||
const changedPaths = [...pendingPaths];
|
||||
pendingPaths.clear();
|
||||
debounceTimer = null;
|
||||
lastFlushAt = Date.now();
|
||||
for (const changedPath of changedPaths) {
|
||||
for (const fn of listeners) {
|
||||
fn(changedPath);
|
||||
}
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
}, delay);
|
||||
});
|
||||
} catch {
|
||||
// fs.watch may fail on some platforms — degrade gracefully (no auto-refresh)
|
||||
|
||||
Reference in New Issue
Block a user