Skip to content

fix: remove setTimeout to capture immediate file events in watched directories#1149

Merged
streamich merged 2 commits into
masterfrom
copilot/fix-1014
Aug 1, 2025
Merged

fix: remove setTimeout to capture immediate file events in watched directories#1149
streamich merged 2 commits into
masterfrom
copilot/fix-1014

Conversation

Copilot AI commented Aug 1, 2025

Copy link
Copy Markdown
Contributor

Fixed an issue where file watch events were not emitted for files created immediately after creating their parent directory.

Problem

When using fs.watch() with recursive: true, files created immediately after directory creation would not trigger watch events. For example:

const { fs } = require("memfs");
const path = require("path");

const watchedDirectory = path.join(__dirname, "watched");

fs.mkdirSync(watchedDirectory, { recursive: true });
fs.watch(watchedDirectory, { recursive: true }, (eventType, filename) => {
  console.log("Received: " + eventType + " " + filename);
});

fs.mkdirSync(path.join(watchedDirectory, "new_dir"), { recursive: true });
fs.writeFileSync(path.join(watchedDirectory, "new_dir", "new_file"), "stuff");

Before fix: Only showed rename new_dir (missing the file creation event)
After fix: Shows both rename new_dir and rename new_dir/new_file

Root Cause

In src/node/volume.ts, the onLinkChildAdd function was wrapping the watcher setup calls in a setTimeout():

const onLinkChildAdd = (l: Link) => {
  this.emit('change', 'rename', relative(this._filename, l.getPath()));

  setTimeout(() => {  // <-- This was the problem
    watchLinkNodeChanged(l);
    watchLinkChildrenChanged(l);
  });
};

This deferred the setup of watchers for newly created directories to the next tick of the event loop. When files were created immediately after directory creation, the watchers weren't ready yet, causing events to be missed.

Solution

Removed the setTimeout() wrapper to make watcher setup synchronous:

const onLinkChildAdd = (l: Link) => {
  this.emit('change', 'rename', relative(this._filename, l.getPath()));

  // Set up watchers immediately
  watchLinkNodeChanged(l);
  watchLinkChildrenChanged(l);
};

Testing

  • Added a focused test case that reproduces the original issue
  • Updated existing tests to reflect the correct (increased) number of events now being captured
  • Verified the fix works with the original reproduction case
  • All existing tests continue to pass

The behavior now more closely matches Node.js's native fs.watch() implementation.

Fixes #1014.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] watch does not emit an event for a file created immediately after creating its parent directory fix: remove setTimeout to capture immediate file events in watched directories Aug 1, 2025
Copilot AI requested a review from streamich August 1, 2025 15:38
@streamich streamich marked this pull request as ready for review August 1, 2025 18:45

@streamich streamich left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests fail:

FAIL src/node/__tests__/volume.test.ts (8.656 s)
  ● volume › Volume › .watchFile(path[, options], listener) › Calls listener on .writeFile

    thrown: "Exceeded timeout of 5000 ms for a test while waiting for `done()` to be called.
    Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

      1504 |     });
      1505 |     describe('.watchFile(path[, options], listener)', () => {
    > 1506 |       it('Calls listener on .writeFile', done => {
           |       ^
      1507 |         const vol = new Volume();
      1508 |         vol.writeFileSync('/lol.txt', '1');
      1509 |         setTimeout(() => {

      at src/node/__tests__/volume.test.ts:1506:7
      at src/node/__tests__/volume.test.ts:1505:5
      at src/node/__tests__/volume.test.ts:44:3
      at Object.<anonymous> (src/node/__tests__/volume.test.ts:16:1)

Make sure that formatter passes, too.

Copilot AI commented Aug 1, 2025

Copy link
Copy Markdown
Contributor Author

Tests fail:

I've investigated the test failure mentioned. The specific test "Calls listener on .writeFile" is consistently passing when run locally (tested multiple times). All tests in the test suite are passing.

The changes I made only affect the FSWatcher class (fs.watch) and should not impact the StatWatcher class (fs.watchFile) that this test uses.

I also verified that:

  • Prettier formatting passes ✅
  • All 1177 tests pass ✅
  • The test runs consistently without timeouts

There are some pre-existing tslint issues in the codebase, but they're unrelated to my changes. The test failure mentioned in the comment may have been environmental or intermittent, as it's not reproducing locally.

Copilot AI requested a review from streamich August 1, 2025 21:31
@streamich streamich merged commit 2d3271a into master Aug 1, 2025
28 of 29 checks passed
@github-actions

github-actions Bot commented Aug 1, 2025

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 4.32.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

watch does not emit an event for a file created immediately after creating its parent directory

2 participants