Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle rename #276

Merged
merged 4 commits into from
Nov 29, 2023
Merged
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
23 changes: 19 additions & 4 deletions src/fileWatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,30 @@ export class FileWatchers {
private readonly watchers: FSWatcher[] = [];

static async of(root: string, path: string, names: string[], callback: (name: string) => void) {
const watchers = new FileWatchers();
const that = new FileWatchers();
const {watchers} = that;
for (const name of new Set(names)) {
const exactPath = resolvePath(root, path, name);
const watchPath = existsSync(exactPath) ? exactPath : Loader.find(root, resolvePath(path, name))?.path;
if (!watchPath) continue;
let currentStat = await maybeStat(watchPath);
let watcher: FSWatcher;
const index = watchers.length;
try {
watcher = watch(watchPath, async () => {
watcher = watch(watchPath, async function watched(type) {
// Re-initialize the watcher on the original path on rename.
if (type === "rename") {
watcher.close();
try {
watcher = watchers[index] = watch(watchPath, watched);
} catch (error) {
if (!isEnoent(error)) throw error;
console.error(`file no longer exists: ${path}`);
return;
}
setTimeout(() => watched("change"), 100); // delay to avoid a possibly-empty file
return;
}
const newStat = await maybeStat(watchPath);
// Ignore if the file was truncated or not modified.
if (currentStat?.mtimeMs === newStat?.mtimeMs || newStat?.size === 0) return;
Expand All @@ -27,9 +42,9 @@ export class FileWatchers {
if (!isEnoent(error)) throw error;
continue;
}
watchers.watchers.push(watcher);
watchers[index] = watcher;
}
return watchers;
return that;
}

close() {
Expand Down
2 changes: 1 addition & 1 deletion src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, options: {root: st
socket.terminate();
return;
}
setTimeout(() => watcher("change"), 150); // delay to avoid a possibly-empty file
setTimeout(() => watcher("change"), 100); // delay to avoid a possibly-empty file
break;
}
case "change": {
Expand Down
68 changes: 65 additions & 3 deletions test/fileWatchers-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert";
import {utimesSync} from "node:fs";
import {renameSync, unlinkSync, utimesSync, writeFileSync} from "node:fs";
import {FileWatchers} from "../src/fileWatchers.js";

describe("FileWatchers.of(root, path, names, callback)", () => {
Expand Down Expand Up @@ -164,12 +164,74 @@ describe("FileWatchers.of(root, path, names, callback)", () => {
watcher.close();
}
});
it("handles a file being renamed", async () => {
let names: Set<string>;
const watch = (name: string) => names.add(name);
try {
writeFileSync("test/input/build/files/temp.csv", "hello", "utf-8");
const watcher = await FileWatchers.of(root, "files.md", ["temp.csv"], watch);
try {
// First rename the file, while writing a new file to the same place.
names = new Set<string>();
await pause();
renameSync("test/input/build/files/temp.csv", "test/input/build/files/temp2.csv");
writeFileSync("test/input/build/files/temp.csv", "hello 2", "utf-8");
await pause(150); // avoid debounce
assert.deepStrictEqual(names, new Set(["temp.csv"]));

// Then test that writing to the original location watches the new file.
names = new Set<string>();
await pause();
writeFileSync("test/input/build/files/temp.csv", "hello 3", "utf-8");
await pause();
assert.deepStrictEqual(names, new Set(["temp.csv"]));
} finally {
watcher.close();
}
} finally {
cleanupSync("test/input/build/files/temp.csv");
cleanupSync("test/input/build/files/temp2.csv");
}
});
it("handles a file being renamed and removed", async () => {
let names: Set<string>;
const watch = (name: string) => names.add(name);
try {
writeFileSync("test/input/build/files/temp.csv", "hello", "utf-8");
const watcher = await FileWatchers.of(root, "files.md", ["file-top.csv", "temp.csv"], watch);
try {
// First delete the temp file. We don’t care if this is reported as a change or not.
names = new Set<string>();
await pause();
unlinkSync("test/input/build/files/temp.csv");
await pause(150);

// Then touch a different file to make sure the watcher is still alive.
names = new Set<string>();
touch("test/input/build/files/file-top.csv");
await pause();
assert.deepStrictEqual(names, new Set(["file-top.csv"]));
} finally {
watcher.close();
}
} finally {
cleanupSync("test/input/build/files/temp.csv");
}
});
});

async function pause(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 10));
async function pause(delay = 10): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, delay));
}

function touch(path: string, date = new Date()): void {
utimesSync(path, date, date);
}

function cleanupSync(path: string): void {
try {
unlinkSync(path);
} catch {
// ignore
}
}