Skip to content

fix: retarget the dev-server watcher instead of replacing it - #622

Merged
Mohamed Mansour (mohamedmansour) merged 2 commits into
mainfrom
mohamedmansour-fix-flaky-press-serve-watch-test
Oct 30, 2026
Merged

Mohamed Mansour (mohamedmansour) merged 2 commits into
mainfrom
mohamedmansour-fix-flaky-press-serve-watch-test

Conversation

@mohamedmansour

Copy link
Copy Markdown
Contributor

markdown-state-serve.test.ts fails intermittently in the E2E (Ubuntu) job (example). A Markdown edit that starts referencing a missing stateFile never triggers a rebuild, so the test times out after 30 s. This is a real dev-server bug, not a test problem.

Root cause

After every rebuild, Press created a new file watcher and dropped the old one. Dropping a notify-debouncer-mini watcher discards events still inside its 50 ms debounce window, and the replacement only sees writes made after it registers. An edit made right after a rebuild finished could fall into that gap and be lost silently. The test hits the gap because it edits as soon as new output appears.

Fix

  • Add WatcherHandle::retarget to webui-dev-server. It updates a running watcher's roots, exact files, and ignore list in place. The debouncer and unchanged recursive roots stay registered, so events observed during a refresh are still delivered. Content baselines reset the way a new watcher's would, so no-op-save filtering is unchanged.
  • Exact-file parents (generated stateFile directories, projection manifests) are still re-registered on every retarget. Some backends drop a watch silently when its directory is deleted, and generators routinely recreate these directories. They use a separate notify backend: FSEvents applies each subscription change by restarting its stream, which would otherwise interrupt source-root events on macOS.
  • If one path cannot be watched, retarget still registers the rest and returns the first error.
  • Press now retargets its single watcher after each rebuild instead of replacing it. The existing re-check of included JSON against the bytes used for rendering still runs once the new subscriptions are active.

spawn_watcher is now built on retarget, so webui serve --watch uses the same registration path. Nested roots are deduplicated, which does not change which files are covered.

Review notes

  • Unchanged recursive roots are no longer re-registered on every rebuild. A source root (content, config, or public directory) deleted and recreated between two rebuilds may not be watched again on Windows, where notify drops the watch without an event. This matches Press before stateFile support and current webui serve. Generated include directories keep per-rebuild re-registration.
  • watch.rs was already over 700 lines, so the backend wrapper and subscription bookkeeping live in watch_backend.rs and watch_subscriptions.rs.

Validation

  • Reproduced before the fix:
    • A minimal repro (edit, then replace the watcher) lost the edit 5/5 times on Windows and 5/5 on Linux (WSL, inotify).
    • The E2E test failed 3/60 runs under parallel CPU load, including the CI symptom and a lost final Markdown edit.
  • With the fix:
    • The E2E test passed 210/210 runs under the same load.
    • The full Press E2E suite passed 118/118.
    • Dev-server and Press Rust tests pass on Windows and Linux.
  • New regression tests:
    • source_edits_during_a_watch_refresh_still_rebuild (Press): failed 5/5 before the fix, passes 10/10 after.
    • Dev-server: retarget_delivers_edits_observed_before_it, retarget_moves_exact_file_subscriptions, and a filter-reset test.
    • Subscription-diff unit tests, including best-effort registration after a failure.
  • cargo xtask check passes.

The E2E test itself is unchanged. It is the only end-to-end coverage of serve watching generated JSON includes, atomic replacements, and newly referenced missing files.

Press replaced its file watcher after every rebuild. Dropping the old
notify-debouncer-mini watcher discards events still inside its 50 ms
debounce window, and the replacement only sees writes made after it
registers. A source edit landing just before the swap was lost, so
markdown-state-serve.test.ts intermittently waited 30 s for a rebuild
that never came.

- Add WatcherHandle::retarget, which updates subscriptions in place.
  Unchanged recursive roots and the debouncer stay registered; content
  baselines restart as for a new watcher.
- Re-register exact-file parents on every retarget so deleted and
  recreated generated directories are watched again. They use a
  separate notify backend so FSEvents stream restarts cannot interrupt
  source roots.
- Register remaining paths when one cannot be watched, then report the
  first failure.
- Retarget the Press watcher after each rebuild instead of replacing it.

Validation: on main, an edit just before a swap was lost 5/5 times on
both Windows and Linux. The e2e test failed 3/60 under parallel CPU
load; with the fix it passed 210/210, and the full Press suite passed
118/118. cargo xtask check passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI 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.

🟡 Changes recommended

Root deduplication can leave a still-requested child directory unwatched when its ancestor cannot be registered, breaking best-effort recovery.

1 open issue
What changed in this PR

This PR fixes lost dev-server rebuild events by letting Press update a running watcher instead of replacing it after each build.

Changes:

  • Add in-place watch retargeting while preserving the debouncer and unchanged recursive roots.
  • Separate exact-file watches from recursive-root watches and add subscription bookkeeping.
  • Update Press integration, regression tests, and watcher API documentation.
File Description
crates/​webui-press/​src/​serve.rs Retargets the existing watcher and tests edits during refresh.
crates/​webui-dev-server/​src/​watch.rs Adds retargeting, shared event filtering, and regression tests.
crates/​webui-dev-server/​src/​watch_subscriptions.rs Manages subscription changes, root deduplication, and registration failures.
crates/​webui-dev-server/​src/​watch_backend.rs Separates recursive-root and exact-file notification backends.
crates/​webui-dev-server/​src/​lib.rs Exports WatchTargets and documents retargeting.
crates/​webui-dev-server/​README.md Explains why callers should retarget rather than replace watchers.

Review effort: Balanced

Retarget dropped a requested root nested inside another requested root.
When the ancestor could not be watched, the nested root was left
unwatched, contradicting retarget's best-effort contract.

Register requested roots ancestors-first and fall back to nested roots
when none of their requested ancestors can be watched. Release whatever
a failed recursive registration left behind, so a failed root leaves no
untracked subscriptions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@mddinbox mddinbox 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.

Target: 6ceba63...cf9179d | Coverage: complete; CI: Desktop npm native (x86_64-apple-darwin) and Desktop npm native (x86_64-pc-windows-msvc) in progress
Challenge: 1 revised after independent challenge; rechecked on pinned code | Compatibility: none
Action: COMMENT | Pending explicit-file changes can leave served output stale.

self.explicit_files = explicit_files;
self.content_hashes.clear();
for file in &self.explicit_files {
content_changed(&mut self.content_hashes, file, &mut self.hash_buffer);

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.

[Medium] reset re-hashes every explicit file before the debouncer flushes. If a configured projection manifest changes after this build reads it but its event is still pending when retarget runs, the new hash is seeded here and the queued event is dropped as unchanged; source_targets watches these manifests as explicit files, while StateDependencies::changed_paths() does not track them. The just-finished output can therefore remain stale until another watched change. Preserve/forward changes against the prior explicit-file baseline across retarget, and add an explicit-file debounce-window regression test.

@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit aa84068 into main Oct 30, 2026
41 checks passed
@mohamedmansour
Mohamed Mansour (mohamedmansour) deleted the mohamedmansour-fix-flaky-press-serve-watch-test branch October 30, 2026 23:16
Mohamed Mansour (mohamedmansour) added a commit that referenced this pull request Oct 31, 2026
## Release

Bumps WebUI to `3.0.5`.

Previous release tag: `v3.0.4`

## Changes since `v3.0.4`

Features:

- Press supports reusable, per-page outlines with responsive navigation
(feat: redesign the docs site and add reusable page outlines #613 by
@mohamedmansour).
- Press tabs can synchronize categories and persist selections;
package-manager preferences now follow readers across documentation
pages and reloads (feat: persist package-manager tabs and simplify
documentation #620 by @mohamedmansour).
- Press adds independent theme families, shared appearance preferences,
System/Light/Dark controls, and extensible header regions with a
cacheable pre-paint initializer (feat: add Press theme families and
extensible appearance controls #623 by @mohamedmansour).

Fixes:

- Hydrate conditional and repeated SSR ranges in place, avoiding
unnecessary custom-element disconnect/reconnect cycles and correcting
trailing formatting whitespace (fix: hydrate SSR block ranges in place
#619 by @mohamedmansour).
- Preserve pending development-server file events by retargeting the
existing watcher instead of replacing it after rebuilds (fix: retarget
the dev-server watcher instead of replacing it #622 by @mohamedmansour).
- Remove unapproved transitive TLS/WebSocket crypto dependencies while
retaining HTTP/2 support. The commerce example now serves HTTP/1.1 and
h2c behind a TLS-terminating ingress; its direct TLS flags are removed
(fix: remove unapproved crypto dependencies #596 by @mohamedmansour).

Docs:

- Redesign the documentation homepage and reading experience, organize
guides into Start/Learn/Build/Reference with legacy redirects, and
simplify installation and package-manager instructions (feat: redesign
the docs site and add reusable page outlines #613 by @mohamedmansour,
chore: clarify the homepage and organize documentation #617 by
@mohamedmansour, feat: persist package-manager tabs and simplify
documentation #620 by @mohamedmansour).
- Refresh benchmark previews, methodology, and downloadable results
around the uniform 15-case official WebUI 3.0.4 campaign, including
provenance and unavailable memory-sample disclosures (docs: support the
uniform 15-case official benchmark campaign #614 by @mohamedmansour).
- Update source-build requirements, Rust writer guidance, and Press
appearance configuration and deployment documentation (chore: upgrade
Rust to 1.99.1 #612 by @mohamedmansour, perf: reduce renderer output
allocations #615 by @mohamedmansour, feat: add Press theme families and
extensible appearance controls #623 by @mohamedmansour).

Maintenance:

- Reduce renderer allocation churn by writing escaped values directly
into output buffers, reusing attribute buffers, and borrowing quoted
condition text (perf: reduce renderer output allocations #615 by
@mohamedmansour).
- Share eligible Link stylesheets across component mounts within a
document, preserving native first-use loading and avoiding repeated
parsing on subsequent uses (perf: share Link stylesheets across
component mounts #621 by @mohamedmansour).
- Align development and release toolchains on Rust 1.99.1; consumers
building from source now require Rust 1.99.1 or newer (chore: upgrade
Rust to 1.99.1 #612 by @mohamedmansour).

## Validation

- `cargo xtask check` passed, including lint, protocol drift, dependency
audit, tests, native/WASM builds, examples, benchmark validation, and
docs.
- Used `WEBUI_PRESS_BINARY_PATH` to select the freshly built workspace
Press 3.0.5 binary instead of an older global installation.
- Refreshed the standalone Tauri lockfile with Cargo; all 45 changed
files contain only `3.0.4` to `3.0.5` substitutions.
Mohamed Mansour (mohamedmansour) added a commit that referenced this pull request Oct 31, 2026
…etargeting (#622)

No textual conflicts. Cargo.lock, DESIGN.md, docs/ai.md, the Press bundler
and the Press package manifest auto-merged; this branch's explicit
runtime.js/styles.js entry imports and main's appearance plugin and alias
are independent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 90a93005-7b2a-4ea4-bce5-902f79da872d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants