Repository navigation
Conversation
… passed The iterator queued every event and read neither option. A consumer that did not read kept every event, and overflow: 'error' never fired. When one of the two options is passed, the listener keeps at most maxQueue unread events (2048 when only overflow is passed). At the bound, overflow 'ignore' drops the event and emits the warning 'fs.watch maxQueue exceeded'. Overflow 'error' replaces the queue with ERR_FS_WATCH_QUEUE_OVERFLOW, and the next next() rejects with it. An error from the watcher then closes it. The first next() validates both options before the signal. The values and the messages are those of Node.js v26.3.0. Two things differ from Node.js. With neither option the queue has no bound, as before. A recursive watch applies both options on every platform. test-fs-promises-watch.js is now the upstream file with no change.
Collaborator
Author
|
Updated 6:09 PM PT - Oct 25th, 2026
✅ @robobun, your commit 35a0f2ec26cd5d8e01a264620e276373f7e73c90 passed in 🧪 To try this PR locally: bunx bun-pr 46937That installs a local version of the PR into your bun-46937 --bun |
Collaborator
Author
This was referenced Oct 25, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
fs.promises.watch()reads neithermaxQueuenoroverflow:overflow: 'error'never rejects withERR_FS_WATCH_QUEUE_OVERFLOW, and bad values pass.src/js/node/fs.promises.ts:90on the base).Fix
1164ab606a).maxQueueunread events (default 2048). After that,'ignore'drops events with a warning and'error'rejectsnext(). The firstnext()validates both.test/js/node/watch/fs.watch.test.ts(12 new tests, 9 fail on the base),test/js/node/watch/, 38 Node.js watch tests. Self-reviewed: 15 concerns raised, 14 addressed.Background
next()takes from that queue.next(). The generator of fs.promises.watch: implement as a real async generator #34718 has none. If fs.promises.watch: implement as a real async generator #34718 is declined, this PR ports to main.Downsides
maxQueuebelow 8 can overflow on POSIX when the consumer reads every event (10 of 10 bursts at 1, 4, 7). The native watcher calls the listener up to 8 times per task (fs.watch: run nextTicks and microtasks between the events of one batch #43715).overflow: 'throw'from @types/node. Withrecursive: trueoff macOS and Windows, Node.js reads neither option. Bun applies both.watch()call. Release binary: +1,031 B.Notes
Repro (Linux x64)
179664ea89, #347181164ab606a{"distinctDelivered":200,"thrown":null}{"distinctDelivered":1,"thrown":"ERR_FS_WATCH_QUEUE_OVERFLOW"}{"distinctDelivered":1,"thrown":"ERR_FS_WATCH_QUEUE_OVERFLOW"}Where this PR equals Node.js v26.3.0
maxQueueandoverflowvalues give the same code and message, in the same order, at the firstnext().node.maxQueue: 16: 16 kept (plus the one that the waitingnext()took), 9,983 warnings.test/js/node/test/parallel/test-fs-promises-watch.jsis now the upstream file with no change. It does not end on main or on fs.promises.watch: implement as a real async generator #34718. Only its three validation cases need this PR. Node.js has no test for the bound.Where this PR differs from Node.js
recursive: trueon Linux with a badmaxQueueoroverflownext()recursive: trueon Linux at the boundmaxQueue1, 2, 4, 7, a consumer that reads every event, a burst of 50mkdirSync,overflow: 'error'maxQueue8, 16, 2048, the same burst{ persistent: "x", maxQueue: "y" }options.persistentoptions.maxQueue(test.todo)next()AbortErrorlanding-prs.mdcalls a changed default a breaking change. To take Node.js's default, changeconst bounded = maxQueue !== undefined || overflow !== undefined;totrue, and the testwith neither option every event is keptto Node.js's numbers.recursive: Node.js sends a recursive watch off macOS and Windows to a watcher written in JS (promises.js#L1948-L1952). Bun's recursive watcher is native on every platform, so one rule applies.persistentandmaxQueue: the native watcher checkspersistentandrecursiveafter the JS checks. node:fs: apply node'spersistent/recursiverules in the JS entry points of the watch APIs #46057 moves those checks to JS. Expected merge: its option block first, then the two validators of this PR.Why there is no microtask checkpoint at the bound
maxQueue1 to 7 gave 50 of 50 in every run.Bun.build()with an async plugin), the second event of a rename came after a later event:["before","late","after"]in 5 of 5 runs. fs.promises.watch: implement as a real async generator #34718 gives["before","after","late"].Costs, measured
Release builds (ThinLTO) of the base
1164ab606aand of this PR, Linux x64.perf,strace,valgrindandbloatyare not installed. The numbers come from aperf_event_opencounter (instructions:u, main thread),size -Aandbun:jscheapStats.size -A.bun_builtins+1,031 B, no other section changesBUN_JSC_useJIT=0(8 runs)maxQueuepassed, JIT (8 runs, this PR against its own default listener)watch()call, neither option, JIT (10 runs)BUN_JSC_useJIT=0(6 runs)watch()call with both options, JITwatch()call, neither optionwatch()call, both optionsvalidateOneOf)FSWatchTaskPosix::enqueue). This PR bounds the JS queue only.'change'with anullfilename) is dropped like every other event.Tests
hungand ends after 4 s.the queue keeps the first maxQueue unread eventsand the watcher-error tests are Linux only.test/js/bun/module-graph/module-graph-isolation.test.tsusesfs.promises.watch. Its 5fsPromisesWatchtests pass alone. In a full debug run of that file, 3 or 4 tests time out on this machine, with main'sfs.promises.tstoo.Self-review
recursive, for the default of 2048, forignore, for the discarded queue and for one warning per drop are new.Related
persistent/recursiverules in the JS entry points of the watch APIs #46057 edit the same function.maxQueuebelow 8 exact.no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/watch/fs.watch.test.ts