Skip to content

refactor(project): Refresh dependency indices on dependency-set change#1469

Open
RandomByte wants to merge 3 commits into
mainfrom
fix/dependency-set-change-cache-staleness
Open

refactor(project): Refresh dependency indices on dependency-set change#1469
RandomByte wants to merge 3 commits into
mainfrom
fix/dependency-set-change-cache-staleness

Conversation

@RandomByte

Copy link
Copy Markdown
Member

Problem

Tasks that glob dependency resources over all available libraries go stale when the dependency set changes between two builds. buildThemes globs /resources/**/themes/**/*.less across every library, but any dependency-globbing task qualifies.

When a transitive dependency is added to or removed from the graph, no dependency resource changed, so dependencyResourcesChanged() reports nothing and #changedDependencyResourcePaths stays empty. The RESTORING_DEPENDENCY_INDICES branch in validateCache refreshed the indices only on a non-empty accumulator, so it skipped the refresh: the restored indices kept the previous build's dependency set, the result signature still matched, and the project was served from cache with a set that no longer matched the graph.

Fix

Persist a dependency-set identity with each project's source index and compare it at the next build:

  • The identity is a sha256 over the sorted ids of the project's transitive dependency closure.
  • It is computed in ProjectBuildContext from the graph and passed into validateCache, so ProjectBuildCache compares two opaque strings and never touches the graph.
  • On mismatch, the full _refreshDependencyIndices runs even without a propagated resource change. On match, the fast path is unchanged.
  • Stored as availableDependencies in the existing source index_cache row, next to tasks. #prepareSourceIndex also writes the row when only the identity changed, since a dependency-set change does not touch source files.

The closure comes from graph.getTransitiveDependencies (mapped to ids, since name and id can differ under npm aliasing), not the task-scoped required set: getRequiredDependencies() returns only direct dependencies for standard tasks (the reader expands transitively on its own) and flips all-or-nothing with task selection. The closure matches what the reader exposes and is stable across task selections.

Out of scope

Same-path version swaps: a dependency's version is not part of the consuming project's build signature, so that case is already stale for the same structural reason and is handled separately if it matters in practice.

Tests

Three regression cases (added library, removed library, generic non-theme JS glob), committed first as test.failing to reproduce the defect, then activated by the fix. Full @ui5/project unit suite passes (3331 passed, 3 skipped).

validateCache only calls _refreshDependencyIndices in the
RESTORING_DEPENDENCY_INDICES state when #changedDependencyResourcePaths is
non-empty. That accumulator is populated exclusively by
dependencyResourcesChanged(), which fires when a dependency *resource*
changes. When the dependency *set* changes across builds (a transitive
library added to or removed from the project graph), no resource is
reported, the accumulator stays empty, and the refresh is skipped. The
restored dependency index keeps reflecting the previous build's dependency
set, so a task that globs dependency resources (buildThemes over available
libraries is the prominent case, but any dependency-globbing task qualifies)
processes the stale set while the result cache still reports a hit.

Add three test.failing cases that seed a persisted dependency-request cache
recorded against an old dependency set, then validate against a reader
exposing the changed set, asserting the dependency-index signature converges
on the value a full refresh produces. Two cover buildThemes (added and
removed library); the third uses a generic JS glob to show the defect is not
theme-specific. They are marked test.failing so CI stays green and flips to a
hard error once the fix lands, prompting removal of the marker.
Tasks that glob dependency resources over all available libraries
(buildThemes globs /resources/**/themes/**/*.less across every library,
but any dependency-globbing task qualifies) go stale when a transitive
dependency is added to or removed from the graph between two builds. No
resource changed, so dependencyResourcesChanged() reports nothing and
#changedDependencyResourcePaths stays empty. The RESTORING_DEPENDENCY_INDICES
branch refreshed the indices only on a non-empty accumulator, so it skipped
the refresh: the restored indices kept the previous build's dependency set,
the result signature still matched, and the project was served from cache.

Persist a dependency-set identity with each project's source index and
compare it at the next build. The identity is a sha256 over the sorted
ids of the transitive dependency closure, computed in ProjectBuildContext
from the graph and passed into validateCache, so ProjectBuildCache
compares two opaque strings and never touches the graph. On mismatch, run
the full _refreshDependencyIndices even without a propagated resource
change; on match, keep the fast path.

The closure comes from graph.getTransitiveDependencies (mapped to ids,
since name and id can differ under npm aliasing), not the task-scoped
required set: getRequiredDependencies() returns only direct dependencies
for standard tasks (the reader expands transitively on its own) and flips
all-or-nothing with task selection. The closure matches what the reader
exposes and is stable across task selections.

The identity is stored as availableDependencies in the existing source
index_cache row, next to tasks. #prepareSourceIndex now also writes the
row when only the identity changed, since a dependency-set change does
not touch source files and would otherwise leave the persisted identity
stale.

Same-path version swaps stay out of scope: a dependency's version is not
part of the consuming project's build signature, so that case is already
stale for the same structural reason and is handled separately if needed.
The three cases were marked test.failing while the defect was unfixed.
validateCache now refreshes the dependency indices on a dependency-set
change, so drop the .failing markers.

Seed the persisted source index with an availableDependencies identity
for the previous build's dependency set, and pass a differing identity
into validateCache to model the added or removed transitive dependency.
The tests still compare the dependency-index signature against the value
a full refresh produces (computed independently via ResourceRequestManager).
@RandomByte
RandomByte force-pushed the fix/dependency-set-change-cache-staleness branch from d02cec9 to fc44204 Compare July 22, 2026 13:10
@RandomByte
RandomByte marked this pull request as ready for review July 22, 2026 13:15
@RandomByte
RandomByte requested a review from a team July 22, 2026 13:40
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