Skip to content

fix(builder): hand replay dedupe to core and watch shared-mapping dirs - #112

Merged
Aukevanoost merged 5 commits into
mainfrom
issues/94
Aug 4, 2026
Merged

fix(builder): hand replay dedupe to core and watch shared-mapping dirs#112
Aukevanoost merged 5 commits into
mainfrom
issues/94

Conversation

@Aukevanoost

@Aukevanoost Aukevanoost commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Bumps @softarc/native-federation to ^4.4.0, which takes ownership of replay dedupe and collapses watch handles per directory.

  • Delete stale-watch-event-filter. Core's dedupe keys on mtime and byte size and holds a grace window, so it lets through a second save landing inside one mtime tick; the adapter's mtime-only copy dropped exactly that event on coarse-granularity filesystems (WSL2 drvfs, NFS, gRPC-FUSE, HFS+). Keeping both would run the weaker filter second.
  • Pass paths to syncNfFileWatcher directly, retiring the { keys() } shim core widened WatchSources to accept.
  • Drop the dirty-buffer refill in onChange: core now buffers every change unconditionally, including when onChange is set.
  • Watch sharedMappingDirs, and wake for them too. A file created since the last build is in no compiled-inputs watch set, so registering the watch alone left the change buffered with nothing to consume it — the file is not imported yet, so Angular's iterator never emits for it.
  • Never wake for the federation's own output. A wake dir is the directory of a mapping's resolved entry point, so a mapping whose entry point sits beside the build output yields a wake dir containing it — and a rebuild writes there with a fresh mtime, which is no replay, so core delivers it and that write drives the next rebuild, forever. shouldWakeFederation rejects the event path instead of dropping the dir from wakeDirs: the same dir also holds the sources the wake set exists to cover.
  • Clear federationFresh beside advanceAngular, so the failed-build and first-output branches can no longer leave it set for an unrelated Angular output.
  • Extract the trigger decisions into watch-decisions.ts and cover the wake-up and rebuild-skip behaviour with specs.
  • Drop the NG_BUILD_PARALLEL_TS verbose log from the remote builder, matching fix(builder): re-apply NG_BUILD_* settings when @angular/build is already loaded #118's removal on the build side. setup-builder-env-variables.ts reports the condition directly via replayNgBuildEnv.

federation-source-files.ts stays: core owns replay dedupe, not the Angular cache expansion that encodes which compilation path recorded the tracked files.

Verified on the nx playground

nx serve mfe1, whose @internal/* mapping is a real shared lib, against a local build of this branch with core 4.4.0.

  • The [native-federation] Dev server serves stale shared-mapping bundles until restart (workspace lib edits never trigger federation rebuild) #94 symptom is gone. An edit to console-logger.service.ts reached dist/mfe1/browser/_internal_logging.js in ~1s.
  • The new-file wake path works. Creating a source file that nothing imports — in no compiled-inputs set, so Angular's iterator never emits for it — woke a federation rebuild.
  • The output-wake loop is real. With the output moved inside a mapping dir (--outputPath=libs/internal/src/logging/__out), one source edit produced 14 → 29 → 44 → 66 → 88 rebuilds over 60s and was still climbing at ~1.5/s. With the guard on and nothing else changed: 1 rebuild, then idle. The same 49 output-file events are delivered either way; the guard is what stops them waking the loop.

How narrow that last precondition is, since the trigger is easy to overstate: wildcard mappings resolve per used entry point (@internal/* yields libs/internal/src/logging, never libs/internal/src), and ignoreUnusedDeps — on by default — prunes unused mappings before sharedMappingDirs sees them. It takes a used, non-wildcard mapping whose entry-point directory contains the build output, e.g. '@shared': ['src/index.ts'] in a project that also emits under src. Rare, but it pins a core until the dev server is killed.

Two things this deliberately does not close. The remote builder has no wake set — every buffered path rebuilds — so it keeps the same output-directory exposure the guard above closes on the build side; that predates this PR and wants its own issue. And the sharedMappingDirs → addPaths wiring is covered only by the pure predicate's specs, not end to end in either builder.

Bumps @softarc/native-federation to ^4.4.0, which takes ownership of
replay dedupe and collapses watch handles per directory.

- Delete stale-watch-event-filter. Core's dedupe keys on mtime *and*
  byte size and holds a grace window, so it lets through a second save
  landing inside one mtime tick; the adapter's mtime-only copy dropped
  exactly that event on coarse-granularity filesystems (WSL2 drvfs, NFS,
  gRPC-FUSE, HFS+). Keeping both would run the weaker filter second.
- Pass paths to syncNfFileWatcher directly, retiring the { keys() } shim
  core widened WatchSources to accept.
- Drop the dirty-buffer refill in onChange: core now buffers every change
  unconditionally, including when onChange is set.
- Watch sharedMappingDirs, and wake for them too. A file created since
  the last build is in no compiled-inputs watch set, so registering the
  watch alone left the change buffered with nothing to consume it — the
  file is not imported yet, so Angular's iterator never emits for it.
- Clear federationFresh beside advanceAngular, so the failed-build and
  first-output branches can no longer leave it set for an unrelated
  Angular output.
- Extract the trigger decisions into watch-decisions.ts and cover the
  wake-up and rebuild-skip behaviour with specs.

federation-source-files.ts stays: core owns replay dedupe, not the
Angular cache expansion that encodes which compilation path recorded
the tracked files.
The wake-up predicate spliced in `path.sep`, but core delivers watch
paths through `toPosix`, so no wake dir could ever match on Windows.
Pre-existing for `linkedDirs`; Item 5 routed `sharedMappingDirs` through
the same comparison, which would have shipped new-file pickup broken
there.

Core already exports the predicate for this — `isUnderAnyDir` normalizes
both sides, and its docstring exists to stop exactly this splice. The
`watchedFiles` lookup keeps `path.normalize`, which does resolve a
delivered posix path on Windows.

Covered by specs that pass backslash dirs, so the regression reproduces
off Windows.
Brings in #118, which re-applies the NG_BUILD_* settings onto an
already-loaded @angular/build.

Both conflicts were the same import block, with a deletion on each side:
main removed describeFederationCache, this branch removed
createStaleWatchEventFilter. Resolved to honour both, leaving only
federationSourceFiles.

#118 dropped the NG_BUILD_PARALLEL_TS verbose log from build/builder.ts
but not from remote/builder.ts, where its comment then pointed at two
deleted symbols. Removed here.

No work item on this branch is invalidated: #118 governs env-var timing,
these govern the watcher. It does change which TS compilation path an Nx
workspace takes, so the end-to-end verification needs re-running.
With `sharedMappings` unset core promotes every tsconfig `paths` entry to a
mapping, so an entry point near the workspace root makes `sharedMappingDirs`
yield a dir containing the federation output. A rebuild writes there with a
fresh mtime — not a replay, so core delivers the event — and the wake set
turned that write into the next rebuild, forever.

Filter the event path against outputPath rather than dropping the dir from
wakeDirs: that dir also holds the sources the wake set exists to cover.
A wake dir is the dirname of a mapping's *resolved* entry point, not of a raw
tsconfig `paths` entry: wildcards expand per used entry point (`@internal/*`
yields libs/internal/src/logging, never libs/internal/src) and ignoreUnusedDeps
prunes unused mappings before sharedMappingDirs sees them. The old wording
implied a workspace-root wake dir was routine.

Record the measurement behind the guard's spec: with the output moved into a
mapping dir, one source edit produced 88 rebuilds in 60s and was still climbing,
against 1 with the guard on.
@Aukevanoost
Aukevanoost merged commit 598b2c6 into main Aug 4, 2026
1 check passed
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.

1 participant