Skip to content

Per-tab watcher leaks when a tab's path changes mid-attach #25

Description

@anbturki

Code analysis only: confirmed by reading the code, not reproduced live. Filed for completeness after the v0.9.1 sync work. No user-visible symptom is known today.

Problem

The per-tab file watcher leaks when a tab's path changes while its watch() call is still in flight.

In src/hooks/useTabs.ts:445-490:

  1. Effect run N creates a slot for tab id T with a placeholder unwatch: () => {} (line 461) and starts watch(pathA).
  2. The tab's path changes to B before that promise resolves (openInActive reuses the tab id). The next effect run calls entry.unwatch(), which is still the no-op placeholder, deletes the map entry, and installs a fresh slot for the same id.
  3. When watch(pathA) finally resolves, the guard !wantedIds.has(tab.id) || !watchers.has(tab.id) (line 483) is false on both counts, because the id is still wanted and the map now holds slot B. So slot.unwatch = unwatch (line 487) assigns the real unwatch onto the orphaned slot A object, which nothing references.

The pathA watcher (an FSEvents stream plus a debouncer thread) then lives until the process exits.

Impact

No incorrect content: the callback re-checks current.path !== tab.path against the captured old path and bails, so stale events cannot overwrite a tab. The cost is resource accumulation over a long session with frequent tab-path switching. FSEvents streams are a finite resource, and unbounded accumulation is the documented mechanism behind watches silently failing to start later in a process's life.

The race window is one watch IPC round-trip, so it needs fast successive path changes on the same tab to trigger, which is why it resists a deterministic repro.

Proposed fix

Track a generation/token per slot and have the resolved watch compare its own token against the map's current slot before adopting it; if the token does not match, immediately unwatch() and drop it. That closes the window without changing the effect's structure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions