Skip to content

fix(watcher): handle vim atomic save race on macOS#23566

Merged
Jarred-Sumner merged 14 commits into
mainfrom
jarred/darwin-watcher
Oct 14, 2025
Merged

fix(watcher): handle vim atomic save race on macOS#23566
Jarred-Sumner merged 14 commits into
mainfrom
jarred/darwin-watcher

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Oct 13, 2025

Copy link
Copy Markdown
Collaborator

Summary

Fixes a race condition on macOS where editing the entrypoint with vim's atomic save causes "Module not found" errors during hot reload.

Root Cause

On macOS, kqueue watches file descriptors/inodes, not paths. Vim's atomic save sequence:

  1. Rename a.js to a.js~ → kqueue reports NOTE_RENAME on watched fd
  2. Hot reloader immediately triggers reload
  3. New file hasn't been created yet → ENOENT error
  4. Vim re-creates a.js, and writes file contents into it
  5. Directory gets NOTE_WRITE but file already removed from watchlist
rename("a.js", "a.js~")                 = 0
openat(AT_FDCWD, "a.js", O_WRONLY|O_CREAT, 0664) = 3
ftruncate(3, 0)                         = 0
write(3, "foobar\n", 7)                 = 7
close(3)                                = 0

This is macOS-specific because:

  • kqueue: watches inodes, fd becomes stale when inode deleted
  • inotify (Linux): watches paths, gets IN.MOVED_TO (not IN.MOVE_SELF), so files stay in watchlist

Solution

When the entrypoint receives NOTE_RENAME on macOS:

  1. Set is_waiting_for_dir_change flag
  2. Skip immediate reload
  3. Wait for parent directory NOTE_WRITE event
  4. Use faccessat() to verify file exists
  5. Trigger reload

This only applies to the entrypoint because dependencies have buffering time during import graph traversal.

Test Plan

Manual testing with vim on macOS:

  1. Run bun --hot entrypoint.js
  2. Edit entrypoint with vim (:w)
  3. Verify no "Module not found" errors
  4. Verify hot reload succeeds

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

Jarred-Sumner and others added 2 commits October 12, 2025 21:13
On macOS, vim's atomic save sequence causes a race condition with kqueue:
1. Old file gets NOTE_RENAME (inode deleted by vim)
2. Hot reloader triggers reload immediately
3. New file hasn't been renamed into place yet → ENOENT error
4. Vim completes rename (a.js~ → a.js)
5. Directory gets NOTE_WRITE but file already removed from watchlist

This is macOS-specific because kqueue watches file descriptors/inodes, not paths.
When vim deletes the old inode, the fd becomes stale. On Linux, inotify watches
paths and receives IN.MOVED_TO (not IN.MOVE_SELF), so files aren't removed from
the watchlist during atomic saves.

Fix: When the entrypoint receives NOTE_RENAME on macOS, defer the reload until
the parent directory receives NOTE_WRITE. Then verify the file exists via
faccessat() before triggering reload. This only applies to the entrypoint since
dependencies have enough buffering time during import graph traversal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@robobun

robobun commented Oct 13, 2025

Copy link
Copy Markdown
Collaborator
Updated 10:01 PM PT - Oct 14th, 2025

@Jarred-Sumner, your commit d7532e1 has 1 failures in Build #29194 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 23566

That installs a local version of the PR into your bun-23566 executable, so you can run:

bun-23566 --bun

@coderabbitai

coderabbitai Bot commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds slow-path file watching and a macOS kqueue helper to the Watcher; integrates main-entry watching into the hot reloader and VM; changes hot-reload APIs to accept an optional entry_path and updates call sites; adds PathName.findExtname; tweaks faccessat path handling; updates CLAUDE.md import guidance.

Changes

Cohort / File(s) Summary of changes
Documentation
src/CLAUDE.md
Reworded import guidance: note the auto-formatter may move @import lines, prefer @import("bun"), and disallow @import("root").bun and relative @import("../bun.zig").
Watcher core
src/Watcher.zig
Added pub fn addFileDescriptorToKQueueWithoutChecks(this: *Watcher, fd: bun.FileDescriptor, watchlist_id: usize) void (macOS kqueue registration) and pub fn addFileByPathSlow(this: *Watcher, file_path: string, loader: options.Loader) bool (slow-path path watching); replaced inline kqueue setup with the new helper.
Hot reloader / ImportWatcher
src/bun.js/hot_reloader.zig, src/bun.js.zig
enableHotModuleReloading now accepts entry_path: ?[]const u8; added ImportWatcher.addFileByPathSlow; introduced MainFile with init; added directory-change / atomic-save deferral logic on macOS; switched loader lookup to Fs.PathName.findExtname; improved event/log messages.
Virtual Machine
src/bun.js/VirtualMachine.zig
Renamed handlePendingInternalPromiseRejectionreportExceptionInHotReloadedModuleIfNeeded; added addMainToWatcherIfNeeded() which calls bun_watcher.addFileByPathSlow for the main entry; adjusted watcher-related calls around promise handling.
Call-site updates
src/bundler/bundle_v2.zig, src/cli/test_command.zig
Updated calls to enableHotModuleReloading to include the additional entry_path argument (often passed as null).
Filesystem utilities
src/fs.zig
Added pub fn findExtname(_path: string) string to PathName to return the extension (or "") from the basename.
sys / path handling
src/sys.zig
In faccessat, the non-sentinel path branch now uses std.posix.toPosixPath(subpath) and passes &path to the recursive faccessat call instead of passing a temporary value.

Suggested reviewers

  • pfgithub

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description does not follow the repository’s required template by using custom headings (“## Summary”, “## Root Cause”, etc.) instead of the mandated “### What does this PR do?” and “### How did you verify your code works?”, which obscures the structured summary and test verification details reviewers expect. Because these sections are missing or renamed, the description fails to match the template. Conforming to the template ensures clarity, consistency, and that automated tools and reviewers can quickly locate the necessary information. Please update the PR description to adhere to the template by adding “### What does this PR do?” to summarize the changes and “### How did you verify your code works?” to describe the testing steps, removing or aligning existing sections accordingly to match the repository’s guidelines.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title succinctly identifies the area of the fix (watcher) and the specific issue (vim atomic save race) on the correct platform (macOS), accurately reflecting the primary change described by the author and following conventional commit style. It clearly conveys the motivation and scope without extraneous detail, enabling team members to understand the core change at a glance.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds macOS watcher helpers and a slow path for path-based file watching. Extends hot reloader to track the entry file, accept optional entry_path, and integrate watcher updates via VirtualMachine. Updates call sites to new signatures. Introduces PathName.findExtname and adjusts sys.faccessat path handling. Updates conventions doc.

Changes

Cohort / File(s) Summary
Documentation conventions
src/CLAUDE.md
Clarified import conventions: note auto-formatter may move bottom imports; prefer direct bun import examples; removed prior root/relative path guidance.
Watcher core (macOS, path-based slow watch)
src/Watcher.zig
Added addFileDescriptorToKQueueWithoutChecks for vnode kevent registration on macOS; refactored appendFileAssumeCapacity (mac path) to use it; introduced public addFileByPathSlow (mutex-guarded, O_EVTONLY on mac, de-duplicates) returning bool.
Hot reload/watcher integration
src/bun.js/hot_reloader.zig, src/bun.js/VirtualMachine.zig, src/bun.js.zig, src/bundler/bundle_v2.zig, src/cli/test_command.zig
HotReloader.enableHotModuleReloading now takes entry_path; ImportWatcher adds addFileByPathSlow; introduced MainFile state (dir/hash) and macOS rename stabilization logic; VM adds addMainToWatcherIfNeeded and renames handlePendingInternalPromiseRejection to reportExceptionInHotReloadedModuleIfNeeded; updated call sites to pass entry_path/null and use new VM method.
Filesystem helpers
src/fs.zig
Added PathName.findExtname(_path) to return extension (with dot) or empty string.
System path handling
src/sys.zig
In faccessat, switched to std.posix.toPosixPath for subpath and passed pointer in recursive call; logic otherwise unchanged.

Possibly related PRs

Suggested reviewers

  • nektro
  • dylan-conway

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description does not follow the repository’s template because it uses "## Summary" and "## Test Plan" instead of the required "### What does this PR do?" and "### How did you verify your code works?" headings and omits those exact sections. Please restructure the description to include separate sections titled "### What does this PR do?" with a summary of changes and "### How did you verify your code works?" with test verification steps as specified in the repository template.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the core change by referencing the watcher fix for vim’s atomic save race on macOS, which matches the content of the pull request.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db7bcd7 and b54f24d.

📒 Files selected for processing (9)
  • src/CLAUDE.md (1 hunks)
  • src/Watcher.zig (3 hunks)
  • src/bun.js.zig (3 hunks)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/bun.js/hot_reloader.zig (11 hunks)
  • src/bundler/bundle_v2.zig (1 hunks)
  • src/cli/test_command.zig (1 hunks)
  • src/fs.zig (1 hunks)
  • src/sys.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/fs.zig
  • src/bundler/bundle_v2.zig
  • src/bun.js/VirtualMachine.zig
  • src/sys.zig
  • src/Watcher.zig
  • src/bun.js.zig
  • src/cli/test_command.zig
  • src/bun.js/hot_reloader.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/fs.zig
  • src/bundler/bundle_v2.zig
  • src/bun.js/VirtualMachine.zig
  • src/sys.zig
  • src/Watcher.zig
  • src/bun.js.zig
  • src/cli/test_command.zig
  • src/bun.js/hot_reloader.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/VirtualMachine.zig
  • src/bun.js/hot_reloader.zig
🧠 Learnings (3)
📚 Learning: 2025-10-07T03:19:53.031Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/CLAUDE.md:0-0
Timestamp: 2025-10-07T03:19:53.031Z
Learning: Applies to src/**/*.zig : Use import("bun") instead of import("root").bun

Applied to files:

  • src/CLAUDE.md
📚 Learning: 2025-10-07T03:19:53.031Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/CLAUDE.md:0-0
Timestamp: 2025-10-07T03:19:53.031Z
Learning: Applies to src/**/*.zig : Place import directives at the bottom of Zig files

Applied to files:

  • src/CLAUDE.md
📚 Learning: 2025-08-30T09:09:18.384Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#22231
File: src/bundler/bundle_v2.zig:48-48
Timestamp: 2025-08-30T09:09:18.384Z
Learning: In Zig, when a module exports a top-level struct, import("./Module.zig") directly returns that struct type and can be used as a type alias without needing to access a field within the module. This is a common pattern in the Bun codebase.

Applied to files:

  • src/CLAUDE.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (4)
src/CLAUDE.md (1)

10-11: LGTM! Documentation updates align with coding guidelines.

The updated conventions correctly reflect the auto-formatter behavior and clarify the preferred import syntax. These changes are consistent with the retrieved learnings.

Based on learnings.

src/cli/test_command.zig (1)

1521-1522: LGTM! Correct adaptation to updated API signature.

Passing null for the entry_path parameter is appropriate in the test command context, where multiple test files are executed without a single designated entrypoint.

src/fs.zig (1)

1565-1568: LGTM! Clean and efficient implementation.

The method correctly extracts the file extension including the leading dot. The implementation is straightforward and handles the common cases well:

  • Files with extensions: returns ".ext"
  • Files without extensions: returns ""
  • Files with multiple dots: returns the last extension

Note: For hidden files starting with a dot (e.g., ".gitignore"), this returns the entire filename. This is consistent with the documented behavior of "finding the last dot and returning from there to the end."

src/bundler/bundle_v2.zig (1)

938-938: LGTM! API update aligns with hot reload enhancement.

The addition of null as the second parameter correctly reflects the updated enableHotModuleReloading signature. At this initialization point, entry points have not been enqueued yet (they are added later via enqueueEntryPoints), so passing null is appropriate. The watcher will receive the actual entry point path when files are added to the watch list.

Comment on lines +148 to +153
if (std.fs.path.dirname(file)) |dir| {
bun.assert(bun.isSliceInBuffer(dir, file));
bun.assert(file.len > dir.len + 1);
main.dir = file[0 .. dir.len + 1];
main.dir_hash = Watcher.getHash(dir);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix directory hash mismatch so the macOS rename slow-path ever fires.

MainFile.init hashes the dirname without its trailing separator, but Watcher.appendFileMaybeLock records directory hashes on dirWithTrailingSlash(). Because of the mismatch, this.main.dir_hash == current_hash never becomes true, so the NOTE_WRITE from the parent directory is ignored, is_waiting_for_dir_change stays stuck, and the entry file is removed from the watchlist without ever scheduling a reload. After the first vim-style atomic save the hot-reload loop stalls permanently. Please hash the exact slice stored in main.dir (with its separator) so the comparison matches and the reload resumes.

-                    main.dir_hash = Watcher.getHash(dir);
+                    main.dir_hash = if (main.dir.len > 0) Watcher.getHash(main.dir) else 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (std.fs.path.dirname(file)) |dir| {
bun.assert(bun.isSliceInBuffer(dir, file));
bun.assert(file.len > dir.len + 1);
main.dir = file[0 .. dir.len + 1];
main.dir_hash = Watcher.getHash(dir);
}
if (std.fs.path.dirname(file)) |dir| {
bun.assert(bun.isSliceInBuffer(dir, file));
bun.assert(file.len > dir.len + 1);
main.dir = file[0 .. dir.len + 1];
main.dir_hash = if (main.dir.len > 0) Watcher.getHash(main.dir) else 0;
}
🤖 Prompt for AI Agents
In src/bun.js/hot_reloader.zig around lines 148 to 153, MainFile.init currently
computes main.dir_hash from the dirname slice without its trailing separator but
stores main.dir as the dirname with the separator; update the logic so the hash
is computed over the exact slice stored in main.dir (i.e., include the trailing
separator) so the stored main.dir_hash matches the Watcher records; ensure you
still assert the slice is in the buffer and lengths are consistent before
assigning main.dir and then call Watcher.getHash on the same slice with the
separator.

Comment thread src/sys.zig

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/sys.zig (1)

3185-3186: Critical: Infinite recursion due to lost sentinel on pointer

This issue was already flagged in previous review comments. Passing &path loses the sentinel type information, causing the !has_sentinel branch to execute recursively until stack overflow.

Apply this diff to fix:

-        return faccessat(dir_fd, &path);
+        return faccessat(dir_fd, path);

Based on previous review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between db7bcd7 and b0de2bd.

📒 Files selected for processing (9)
  • src/CLAUDE.md (1 hunks)
  • src/Watcher.zig (3 hunks)
  • src/bun.js.zig (3 hunks)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/bun.js/hot_reloader.zig (11 hunks)
  • src/bundler/bundle_v2.zig (1 hunks)
  • src/cli/test_command.zig (1 hunks)
  • src/fs.zig (1 hunks)
  • src/sys.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/sys.zig
  • src/bun.js.zig
  • src/fs.zig
  • src/bun.js/VirtualMachine.zig
  • src/Watcher.zig
  • src/cli/test_command.zig
  • src/bundler/bundle_v2.zig
  • src/bun.js/hot_reloader.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/sys.zig
  • src/bun.js.zig
  • src/fs.zig
  • src/bun.js/VirtualMachine.zig
  • src/Watcher.zig
  • src/cli/test_command.zig
  • src/bundler/bundle_v2.zig
  • src/bun.js/hot_reloader.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/VirtualMachine.zig
  • src/bun.js/hot_reloader.zig
🧠 Learnings (4)
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/VirtualMachine.zig
📚 Learning: 2025-10-07T03:19:53.031Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/CLAUDE.md:0-0
Timestamp: 2025-10-07T03:19:53.031Z
Learning: Applies to src/**/*.zig : Use import("bun") instead of import("root").bun

Applied to files:

  • src/CLAUDE.md
📚 Learning: 2025-10-07T03:19:53.031Z
Learnt from: CR
PR: oven-sh/bun#0
File: src/CLAUDE.md:0-0
Timestamp: 2025-10-07T03:19:53.031Z
Learning: Applies to src/**/*.zig : Place import directives at the bottom of Zig files

Applied to files:

  • src/CLAUDE.md
📚 Learning: 2025-08-30T09:09:18.384Z
Learnt from: Jarred-Sumner
PR: oven-sh/bun#22231
File: src/bundler/bundle_v2.zig:48-48
Timestamp: 2025-08-30T09:09:18.384Z
Learning: In Zig, when a module exports a top-level struct, import("./Module.zig") directly returns that struct type and can be used as a type alias without needing to access a field within the module. This is a common pattern in the Bun codebase.

Applied to files:

  • src/CLAUDE.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (10)
src/bun.js.zig (3)

314-316: LGTM: pass entry_path to HMR enablers

Signature change is applied correctly.


331-332: LGTM: ensure main is added to watcher before ticking

Adds robustness for initial failures.


391-409: Ensure VM handler is null-safe

The repeated calls are fine, but reportExceptionInHotReloadedModuleIfNeeded must handle pending_internal_promise == null to avoid a crash.

Also applies to: 400-400, 407-407

src/Watcher.zig (1)

386-387: LGTM: reuse kqueue registration helper for files

Reduces duplication.

src/bun.js/hot_reloader.zig (6)

28-37: LGTM: expose slow-path add by path

Wrapper is minimal and correct.


121-157: LGTM: MainFile tracks dir/hash and macOS rename state correctly

Computes dir slice with trailing sep and hashes that exact slice; resolves prior mismatch.


238-252: LGTM: HMR enable accepts entry_path and initializes MainFile

Initialization is correct and side-effect free until watcher start.


367-400: LGTM: defer entry reload on macOS NOTE_RENAME for entrypoint

Correctly skips immediate reload, sets wait flag, and resumes on subsequent writes.


421-433: LGTM: dir NOTE_WRITE resumes entry reload after atomic save

faccessat against parent dir fd + basename is the right existence check.


481-481: Loader lookup relies on correct ext parsing

This path uses Fs.PathName.findExtname(changed_name). Ensure the ext helper uses basename only (see fs.zig comment) to avoid misclassification when directory names contain dots.

Also applies to: 537-539

Comment thread src/bun.js/VirtualMachine.zig
Comment thread src/bun.js/VirtualMachine.zig
Comment thread src/fs.zig
Comment thread src/Watcher.zig
Claude Bot and others added 2 commits October 13, 2025 04:47
- Fix faccessat recursion by passing sentinel slice directly
- Add null-check for pending_internal_promise to prevent crash
- Skip watcher add when main path is empty
- Fix findExtname to operate on basename only (avoid mis-parsing paths with dots in directories)
- Cast udata consistently in addFileDescriptorToKQueueWithoutChecks
- Fix duplicate-watch race in addFileByPathSlow by using addFile which rechecks under lock
- Preserve fast-path check to avoid opening file descriptor when already watching

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
On macOS, std.c.faccessat expects [*:0]const u8 (pointer to sentinel),
not [1023:0]u8 (array). Pass &path to properly coerce the array to a pointer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b0de2bd and c41cc8c.

📒 Files selected for processing (4)
  • src/Watcher.zig (3 hunks)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/fs.zig (1 hunks)
  • src/sys.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/VirtualMachine.zig
  • src/Watcher.zig
  • src/fs.zig
  • src/sys.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/VirtualMachine.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/VirtualMachine.zig
  • src/Watcher.zig
  • src/fs.zig
  • src/sys.zig
🧠 Learnings (1)
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/VirtualMachine.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (5)
src/sys.zig (1)

3185-3186: Past recursion issue resolved—code is correct.

The infinite-recursion bug (passing &path instead of path) identified in the previous review has been fixed. The current implementation correctly passes the sentinel slice itself, so the recursive call sees std.meta.sentinel([:0]u8) == 0 and skips re-normalization.

src/fs.zig (1)

1565-1575: Past directory-dot parsing issue resolved—logic is correct.

The implementation now properly extracts the basename before searching for the extension dot, preventing false matches on directory components like dir.ext/file. The dot > 0 check correctly excludes dotfiles (e.g., .gitignore""), aligning with NodeJSPathName.init behavior.

src/bun.js/VirtualMachine.zig (2)

680-695: Past review concern addressed: null-check added.

The null-check at lines 681-685 correctly addresses the previous review comment about potential null-deref. The function now safely handles both cases (promise present or absent) and ensures the main file is added to the watcher in both branches, which aligns with the hot-reload use case.


696-702: Past review concern addressed: empty-path guard added.

The guard at line 699 correctly addresses the previous review comment about avoiding operations on empty paths. The implementation properly checks watcher state, validates the path, and invokes the slow-path watcher registration with the appropriate loader.

src/Watcher.zig (1)

315-347: Past review concern addressed: udata cast to usize.

The cast at line 331 (@as(usize, @intCast(watchlist_id))) correctly addresses the previous review comment about consistent udata casting. The kqueue event registration is properly configured for macOS vnode monitoring.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b0de2bd and 922afd0.

📒 Files selected for processing (3)
  • src/Watcher.zig (3 hunks)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/fs.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/VirtualMachine.zig
  • src/fs.zig
  • src/Watcher.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/VirtualMachine.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/VirtualMachine.zig
  • src/fs.zig
  • src/Watcher.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format

Comment thread src/Watcher.zig
@robobun robobun force-pushed the jarred/darwin-watcher branch from bcfeb49 to 9ba2489 Compare October 13, 2025 05:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bcfeb49337b06f669dcc5448d4c681c60bc1eac7 and 9ba248986a6bade2e25ca8a8d6a9ea362f714f6e.

📒 Files selected for processing (1)
  • src/Watcher.zig (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/Watcher.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/Watcher.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (1)
src/Watcher.zig (1)

386-386: LGTM! Clean refactor.

Consolidating the kqueue registration into the helper function improves maintainability.

Comment thread src/Watcher.zig
When two threads race through addFileByPathSlow, the second thread can
open a file descriptor after the fast-path check but before calling addFile.
If the first thread already added the file, addFile returns success without
consuming the fd (on macOS without atomic_file_watcher), causing a leak.

Fix: Keep the fast-path check to avoid opening files unnecessarily, but
after addFile returns success, check if the stored fd differs from our
opened fd. If so, close our fd since it wasn't used.

Also prevent double-close when file is removed concurrently: only close
if the entry still exists in the watchlist. If it was removed, flushEvictions
already closed it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@robobun robobun force-pushed the jarred/darwin-watcher branch from 9ba2489 to 150dc37 Compare October 13, 2025 05:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ba248986a6bade2e25ca8a8d6a9ea362f714f6e and 150dc37.

📒 Files selected for processing (1)
  • src/Watcher.zig (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/Watcher.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/Watcher.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (1)
src/Watcher.zig (1)

386-386: LGTM: Clean refactoring.

The inline kqueue registration code has been properly extracted to the new helper function.

Comment thread src/Watcher.zig
…d improve race condition comments

Add comprehensive doc comment for addFileDescriptorToKQueueWithoutChecks explaining:
- Function purpose (kqueue registration on macOS)
- Preconditions callers must satisfy
- Silent error handling behavior

Expand race condition comments in addFileByPathSlow to document all three scenarios:
1. Entry removed after fd stored (don't close - already closed by flushEvictions)
2. Entry exists with different fd (close our unused fd to prevent leak)
3. Entry exists with same fd (don't close - our fd is stored)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 150dc37 and 0cb8d87.

📒 Files selected for processing (1)
  • src/Watcher.zig (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/Watcher.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/Watcher.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (2)
src/Watcher.zig (2)

315-356: LGTM: kqueue registration is correct.

The documentation is comprehensive, and the implementation correctly registers a vnode event with kqueue. Past review concerns about udata casting and documentation have been properly addressed.

Note: This function is macOS-specific but exposed as a public API on all platforms. While the function name clearly indicates its platform-specific nature (includes "KQueue"), and it's only called from macOS code paths in practice (line 395), consider whether to add a compile-time assertion or conditional compilation if cross-platform compilation with incorrect usage is a concern.


658-681: LGTM: fd lifecycle management is correct.

The race condition handling correctly addresses all three scenarios identified in past reviews:

  1. Entry removed after our fd was stored → don't close (prevents double-close)
  2. Entry exists with different fd → close our unused fd (prevents leak)
  3. Entry exists with same fd → don't close (our fd is now stored)

The conditional check at line 676 (maybe_idx != null and stored_fd.native() != fd.native()) properly distinguishes between these cases.

Comment thread src/Watcher.zig
Comment thread src/Watcher.zig
Add comprehensive doc comment for addFileByPathSlow explaining:
- Function purpose (lazy watch for files outside import graph)
- Platform behavior (opens O_EVTONLY fd on macOS)
- Thread-safety guarantees
- Return value semantics

Add validation to reject empty file_path before computing hash or opening file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/Watcher.zig
Comment on lines +648 to +656
{
this.mutex.lock();
const already_watched = this.indexOf(hash) != null;
this.mutex.unlock();

if (already_watched) {
return true;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this a nested block?

Comment thread src/Watcher.zig Outdated
Comment thread src/bun.js/VirtualMachine.zig Outdated
Comment on lines +682 to +686
if (promise_opt == null) {
this.addMainToWatcherIfNeeded();
return;
}
var promise = promise_opt.?;

@taylordotfish taylordotfish Oct 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

edit: see followup suggestion; this can be even simpler

Suggested change
if (promise_opt == null) {
this.addMainToWatcherIfNeeded();
return;
}
var promise = promise_opt.?;
var promise = promise_opt orelse {
this.addMainToWatcherIfNeeded();
return;
};

};
}

pub inline fn addFileByPathSlow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we sure this function needs to be semantically inlined?

Comment thread src/bun.js/VirtualMachine.zig Outdated
Comment thread src/bun.js/hot_reloader.zig Outdated
Comment thread src/bun.js/hot_reloader.zig Outdated
Comment thread src/bun.js/hot_reloader.zig Outdated
Jarred-Sumner and others added 5 commits October 14, 2025 14:56
Co-authored-by: taylor.fish <contact@taylor.fish>
Co-authored-by: taylor.fish <contact@taylor.fish>
Co-authored-by: taylor.fish <contact@taylor.fish>
Co-authored-by: taylor.fish <contact@taylor.fish>
Co-authored-by: taylor.fish <contact@taylor.fish>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d6e5223 and d7532e1.

📒 Files selected for processing (3)
  • src/Watcher.zig (3 hunks)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/bun.js/hot_reloader.zig (11 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/hot_reloader.zig
  • src/Watcher.zig
  • src/bun.js/VirtualMachine.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/hot_reloader.zig
  • src/bun.js/VirtualMachine.zig
src/**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, manage memory carefully and use defer for cleanup of allocations/resources

src/**/*.zig: Use the # prefix to declare private fields in Zig structs (e.g., struct { #foo: u32 })
Prefer decl literals when initializing values in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 })
Place @import directives at the bottom of Zig files
Use @import("bun") instead of @import("root").bun

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

Files:

  • src/bun.js/hot_reloader.zig
  • src/Watcher.zig
  • src/bun.js/VirtualMachine.zig
🔇 Additional comments (5)
src/Watcher.zig (2)

315-355: LGTM! Well-documented public API addition.

The kqueue registration helper is properly documented with clear preconditions and behavior notes. The implementation correctly sets up the kevent structure with appropriate flags and stores the watchlist_id in udata.

Based on learnings from past review comments, this implementation correctly addresses the type casting concern.


639-698: LGTM! Thread-safe slow-path watching with proper race handling.

The implementation correctly:

  • Validates input (empty path check)
  • Uses locking to prevent races when checking if already watched
  • Opens file descriptor only on macOS with O_EVTONLY flag
  • Handles three race scenarios properly:
    1. Entry removed (maybe_idx == null): fd was stored then closed by flushEvictions → don't close
    2. Entry exists with different fd: another thread added entry, addFile didn't use our fd → close ours
    3. Entry exists with same fd: our fd was stored → don't close

Based on learnings from past review comments, the race condition handling and documentation were refined in previous commits.

src/bun.js/hot_reloader.zig (2)

121-157: LGTM! Correct directory hash computation.

The MainFile.init implementation correctly addresses the hash mismatch issue flagged in past reviews:

  • Stores main.dir with the trailing separator (line 151)
  • Computes main.dir_hash from the exact slice stored in main.dir (line 152)

This ensures the hash matches what Watcher.appendFileMaybeLock records via dirWithTrailingSlash(), so the NOTE_WRITE event from the parent directory will be properly detected.

Based on learnings from past review comments, this correctly fixes the directory hash mismatch.


367-432: LGTM! Correct implementation of vim atomic save workaround.

The event handling logic correctly implements the race condition fix:

File events (lines 367-396):

  1. On NOTE_RENAME for entrypoint: set is_waiting_for_dir_change flag and skip reload
  2. On NOTE_WRITE for entrypoint after rename: clear flag and proceed with reload (file is back)

Directory events (lines 421-432):
3. On NOTE_WRITE for parent directory: verify file exists with faccessat(), clear flag, and trigger reload

The dual-path approach (file write or directory write) correctly handles both the vim atomic save case and simpler file modification cases.

Based on learnings from past review comments, the comments accurately describe the vim save process.

src/bun.js/VirtualMachine.zig (1)

680-696: LGTM! Clean hot-reload exception handling with main file watching.

Both functions are well-implemented:

reportExceptionInHotReloadedModuleIfNeeded (lines 680-688):

  • Uses defer to ensure addMainToWatcherIfNeeded() is always called
  • Uses orelse return for clean early exit when no pending promise
  • Properly handles rejected promises and marks them as handled

addMainToWatcherIfNeeded (lines 690-696):

  • Guards against empty path before calling the slow path watcher
  • Correctly resolves the loader from file extension

Based on learnings from past review comments, these implementations incorporate the suggested simplifications using defer and orelse.

Comment on lines +28 to +37
pub inline fn addFileByPathSlow(
this: ImportWatcher,
file_path: string,
loader: options.Loader,
) bool {
return switch (this) {
inline .hot, .watch => |w| w.addFileByPathSlow(file_path, loader),
else => true,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider removing the inline keyword.

The function is a simple delegation that switches on a union type. While inlining may provide minor performance benefits, the inline keyword forces semantic inlining which prevents the compiler from making optimization decisions. Unless profiling shows this is a hot path where inlining provides measurable benefit, consider removing inline and let the compiler decide.

As noted in past review comments, this was previously questioned by taylordotfish.

🤖 Prompt for AI Agents
In src/bun.js/hot_reloader.zig around lines 28 to 37, the function is declared
as "pub inline fn addFileByPathSlow" which forces semantic inlining; remove the
inline keyword so the compiler can decide whether to inline (change the
signature to "pub fn addFileByPathSlow(...)") and then rebuild and run tests to
ensure no performance regressions or regressions in behavior.

@Jarred-Sumner Jarred-Sumner merged commit bad726f into main Oct 14, 2025
57 of 61 checks passed
@Jarred-Sumner Jarred-Sumner deleted the jarred/darwin-watcher branch October 14, 2025 23:33
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.

3 participants