feat(watchers): Integrate chokidar-based Node watcher, expose awaitWriteFinish options, and update docs/tests
This commit is contained in:
@@ -74,11 +74,15 @@ tap.test('should add paths and start watching', async () => {
|
||||
testSmartwatch.add([`${TEST_DIR}/**/*.txt`]);
|
||||
await testSmartwatch.start();
|
||||
expect(testSmartwatch.status).toEqual('watching');
|
||||
// Wait for chokidar to be ready
|
||||
await delay(500);
|
||||
});
|
||||
|
||||
tap.test('should detect ADD event for new files', async () => {
|
||||
const addObservable = await testSmartwatch.getObservableFor('add');
|
||||
const eventPromise = waitForEvent(addObservable);
|
||||
|
||||
// Subscribe FIRST, then create file
|
||||
const eventPromise = waitForFileEvent(addObservable, 'add-test.txt');
|
||||
|
||||
// Create a new file
|
||||
const testFile = path.join(TEST_DIR, 'add-test.txt');
|
||||
@@ -87,9 +91,9 @@ tap.test('should detect ADD event for new files', async () => {
|
||||
const [filePath] = await eventPromise;
|
||||
expect(filePath).toInclude('add-test.txt');
|
||||
|
||||
// Cleanup - wait for atomic delay to complete (100ms debounce + 100ms atomic)
|
||||
// Cleanup
|
||||
await fs.promises.unlink(testFile);
|
||||
await delay(250);
|
||||
await delay(200);
|
||||
});
|
||||
|
||||
tap.test('should detect CHANGE event for modified files', async () => {
|
||||
@@ -98,10 +102,10 @@ tap.test('should detect CHANGE event for modified files', async () => {
|
||||
await smartfile.memory.toFs('initial content', testFile);
|
||||
|
||||
// Wait for add event to complete
|
||||
await delay(200);
|
||||
await delay(300);
|
||||
|
||||
const changeObservable = await testSmartwatch.getObservableFor('change');
|
||||
const eventPromise = waitForEvent(changeObservable);
|
||||
const eventPromise = waitForFileEvent(changeObservable, 'change-test.txt');
|
||||
|
||||
// Modify the file
|
||||
await smartfile.memory.toFs('modified content', testFile);
|
||||
@@ -109,9 +113,9 @@ tap.test('should detect CHANGE event for modified files', async () => {
|
||||
const [filePath] = await eventPromise;
|
||||
expect(filePath).toInclude('change-test.txt');
|
||||
|
||||
// Cleanup - wait for atomic delay to complete
|
||||
// Cleanup
|
||||
await fs.promises.unlink(testFile);
|
||||
await delay(250);
|
||||
await delay(200);
|
||||
});
|
||||
|
||||
tap.test('should detect UNLINK event for deleted files', async () => {
|
||||
@@ -120,7 +124,7 @@ tap.test('should detect UNLINK event for deleted files', async () => {
|
||||
await smartfile.memory.toFs('to be deleted', testFile);
|
||||
|
||||
// Wait for add event to complete
|
||||
await delay(200);
|
||||
await delay(300);
|
||||
|
||||
const unlinkObservable = await testSmartwatch.getObservableFor('unlink');
|
||||
|
||||
|
||||
@@ -16,21 +16,25 @@ const TEST_DIR = './test/assets';
|
||||
// Helper to delay
|
||||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Helper to wait for an event with timeout
|
||||
async function waitForEvent<T>(
|
||||
// Helper to wait for an event with timeout (filters by filename)
|
||||
async function waitForFileEvent<T extends [string, ...any[]]>(
|
||||
observable: smartrx.rxjs.Observable<T>,
|
||||
expectedFile: string,
|
||||
timeoutMs: number = 5000
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
subscription.unsubscribe();
|
||||
reject(new Error(`Timeout waiting for event after ${timeoutMs}ms`));
|
||||
reject(new Error(`Timeout waiting for event on ${expectedFile} after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
const subscription = observable.subscribe((value) => {
|
||||
clearTimeout(timeout);
|
||||
subscription.unsubscribe();
|
||||
resolve(value);
|
||||
const [filePath] = value;
|
||||
if (filePath.includes(expectedFile)) {
|
||||
clearTimeout(timeout);
|
||||
subscription.unsubscribe();
|
||||
resolve(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -45,27 +49,31 @@ tap.test('setup: start watcher', async () => {
|
||||
testSmartwatch = new smartwatch.Smartwatch([`${TEST_DIR}/**/*.txt`]);
|
||||
await testSmartwatch.start();
|
||||
expect(testSmartwatch.status).toEqual('watching');
|
||||
// Wait for chokidar to be ready
|
||||
await delay(500);
|
||||
});
|
||||
|
||||
tap.test('should detect delete+recreate (inode change scenario)', async () => {
|
||||
// This simulates what many editors do: delete file, create new file
|
||||
tap.test('should detect delete+recreate as change event (atomic handling)', async () => {
|
||||
// Chokidar with atomic: true handles delete+recreate as a single change event
|
||||
// This is the expected behavior for editor save patterns
|
||||
const testFile = path.join(TEST_DIR, 'inode-test.txt');
|
||||
|
||||
// Clean up any leftover file from previous runs
|
||||
try { await fs.promises.unlink(testFile); } catch {}
|
||||
await delay(100);
|
||||
|
||||
// Create initial file
|
||||
await smartfile.memory.toFs('initial content', testFile);
|
||||
await delay(200);
|
||||
await delay(300);
|
||||
|
||||
// Get the initial inode
|
||||
const initialStats = await fs.promises.stat(testFile);
|
||||
const initialInode = initialStats.ino;
|
||||
console.log(`[test] Initial inode: ${initialInode}`);
|
||||
|
||||
// With event sequence tracking, delete+recreate emits: unlink, then add
|
||||
// This is more accurate than just emitting 'change'
|
||||
const unlinkObservable = await testSmartwatch.getObservableFor('unlink');
|
||||
const addObservable = await testSmartwatch.getObservableFor('add');
|
||||
const unlinkPromise = waitForEvent(unlinkObservable, 3000);
|
||||
const addPromise = waitForEvent(addObservable, 3000);
|
||||
// Chokidar's atomic handling will emit a single 'change' event
|
||||
const changeObservable = await testSmartwatch.getObservableFor('change');
|
||||
const eventPromise = waitForFileEvent(changeObservable, 'inode-test.txt', 3000);
|
||||
|
||||
// Delete and recreate (this creates a new inode)
|
||||
await fs.promises.unlink(testFile);
|
||||
@@ -77,14 +85,14 @@ tap.test('should detect delete+recreate (inode change scenario)', async () => {
|
||||
console.log(`[test] New inode: ${newInode}`);
|
||||
expect(newInode).not.toEqual(initialInode);
|
||||
|
||||
// Should detect both unlink and add events for delete+recreate
|
||||
const [[unlinkPath], [addPath]] = await Promise.all([unlinkPromise, addPromise]);
|
||||
expect(unlinkPath).toInclude('inode-test.txt');
|
||||
expect(addPath).toInclude('inode-test.txt');
|
||||
console.log(`[test] Detected unlink + add events for delete+recreate`);
|
||||
// Chokidar detects this as a change (atomic write pattern)
|
||||
const [filePath] = await eventPromise;
|
||||
expect(filePath).toInclude('inode-test.txt');
|
||||
console.log(`[test] Detected change event for delete+recreate (atomic handling)`);
|
||||
|
||||
// Cleanup
|
||||
await fs.promises.unlink(testFile);
|
||||
await delay(200);
|
||||
});
|
||||
|
||||
tap.test('should detect atomic write pattern (temp file + rename)', async () => {
|
||||
@@ -96,10 +104,10 @@ tap.test('should detect atomic write pattern (temp file + rename)', async () =>
|
||||
|
||||
// Create initial file
|
||||
await smartfile.memory.toFs('initial content', testFile);
|
||||
await delay(200);
|
||||
await delay(300);
|
||||
|
||||
const changeObservable = await testSmartwatch.getObservableFor('change');
|
||||
const eventPromise = waitForEvent(changeObservable, 3000);
|
||||
const eventPromise = waitForFileEvent(changeObservable, 'atomic-test.txt', 3000);
|
||||
|
||||
// Atomic write: create temp file then rename
|
||||
await smartfile.memory.toFs('atomic content', tempFile);
|
||||
|
||||
@@ -44,6 +44,8 @@ tap.test('setup: start watcher', async () => {
|
||||
testSmartwatch = new smartwatch.Smartwatch([`${TEST_DIR}/**/*.txt`]);
|
||||
await testSmartwatch.start();
|
||||
expect(testSmartwatch.status).toEqual('watching');
|
||||
// Wait for chokidar to be ready
|
||||
await delay(500);
|
||||
});
|
||||
|
||||
tap.test('STRESS: rapid file modifications', async () => {
|
||||
|
||||
Reference in New Issue
Block a user