Skip to content

Commit 20370be

Browse files
committed
refactor(project): Fix derived trees unexpected upsert in parents
1 parent 05d0a34 commit 20370be

5 files changed

Lines changed: 293 additions & 30 deletions

File tree

packages/project/lib/build/cache/index/SharedHashTree.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default class SharedHashTree extends HashTree {
5656
}
5757

5858
for (const resource of resources) {
59-
this.registry.scheduleUpsert(resource, newIndexTimestamp);
59+
this.registry.scheduleUpsert(resource, newIndexTimestamp, this);
6060
}
6161
}
6262

@@ -99,6 +99,11 @@ export default class SharedHashTree extends HashTree {
9999
_root: derivedRoot
100100
});
101101

102+
// Register the derived tree with parent tree reference
103+
if (this.registry) {
104+
this.registry.register(derived, this);
105+
}
106+
102107
return derived;
103108
}
104109

packages/project/lib/build/cache/index/TreeRegistry.js

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ import {matchResourceMetadataStrict} from "../utils.js";
1717
* This approach ensures consistency when multiple trees represent filtered views of the same underlying data.
1818
*
1919
* @property {Set<import('./SharedHashTree.js').default>} trees - All registered HashTree/SharedHashTree instances
20-
* @property {Map<string, @ui5/fs/Resource>} pendingUpserts - Resource path to resource mappings for scheduled upserts
20+
* @property {Map<string, {resource: @ui5/fs/Resource, sourceTree: import('./SharedHashTree.js').default|null}>}
21+
* pendingUpserts - Resource path to resource and source tree mappings for scheduled upserts
2122
* @property {Set<string>} pendingRemovals - Resource paths scheduled for removal
23+
* @property {Map<import('./SharedHashTree.js').default, Set<import('./SharedHashTree.js').default>>} derivedTrees
24+
* Maps parent trees to their directly derived children
2225
*/
2326
export default class TreeRegistry {
2427
trees = new Set();
2528
pendingUpserts = new Map();
2629
pendingRemovals = new Set();
2730
pendingTimestampUpdate;
31+
derivedTrees = new Map(); // parent -> Set of derived trees
2832

2933
/**
3034
* Register a HashTree or SharedHashTree instance with this registry for coordinated updates.
@@ -33,9 +37,17 @@ export default class TreeRegistry {
3337
* Multiple trees can share the same underlying nodes through structural sharing.
3438
*
3539
* @param {import('./SharedHashTree.js').default} tree - HashTree or SharedHashTree instance to register
40+
* @param {import('./SharedHashTree.js').default} [parentTree] - Parent tree if this is a derived tree
3641
*/
37-
register(tree) {
42+
register(tree, parentTree = null) {
3843
this.trees.add(tree);
44+
45+
if (parentTree) {
46+
if (!this.derivedTrees.has(parentTree)) {
47+
this.derivedTrees.set(parentTree, new Set());
48+
}
49+
this.derivedTrees.get(parentTree).add(tree);
50+
}
3951
}
4052

4153
/**
@@ -48,6 +60,49 @@ export default class TreeRegistry {
4860
*/
4961
unregister(tree) {
5062
this.trees.delete(tree);
63+
64+
// Remove from derivedTrees mappings
65+
this.derivedTrees.delete(tree);
66+
for (const [, derivedSet] of this.derivedTrees) {
67+
derivedSet.delete(tree);
68+
}
69+
}
70+
71+
/**
72+
* Get all trees derived from a given tree (recursively).
73+
*
74+
* @param {import('./SharedHashTree.js').default} tree - The parent tree
75+
* @returns {Set<import('./SharedHashTree.js').default>} Set of all derived trees (direct and transitive)
76+
*/
77+
_getDerivedTrees(tree) {
78+
const result = new Set();
79+
const directDerived = this.derivedTrees.get(tree);
80+
81+
if (directDerived) {
82+
for (const derived of directDerived) {
83+
result.add(derived);
84+
// Recursively get trees derived from derived
85+
for (const transitive of this._getDerivedTrees(derived)) {
86+
result.add(transitive);
87+
}
88+
}
89+
}
90+
91+
return result;
92+
}
93+
94+
/**
95+
* Check if targetTree is the same as or derived from sourceTree.
96+
*
97+
* @param {import('./SharedHashTree.js').default} sourceTree - The source/parent tree
98+
* @param {import('./SharedHashTree.js').default} targetTree - The tree to check
99+
* @returns {boolean} True if targetTree is sourceTree or derived from it
100+
*/
101+
_isTreeOrDerived(sourceTree, targetTree) {
102+
if (sourceTree === targetTree) {
103+
return true;
104+
}
105+
return this._getDerivedTrees(sourceTree).has(targetTree);
51106
}
52107

53108
/**
@@ -57,15 +112,23 @@ export default class TreeRegistry {
57112
* any necessary parent directories). If it exists, its metadata will be updated if changed.
58113
* Scheduling an upsert cancels any pending removal for the same resource path.
59114
*
115+
* When sourceTree is specified, new resources will only be inserted into that tree and
116+
* any trees derived from it. Updates to existing resources will still propagate to all
117+
* trees that share the resource node.
118+
*
60119
* @param {@ui5/fs/Resource} resource - Resource instance to upsert
61-
* @param {number} newIndexTimestamp Timestamp at which the provided resources have been indexed
120+
* @param {number} [newIndexTimestamp] - Timestamp at which the provided resources have been indexed
121+
* @param {import('./SharedHashTree.js').default} [sourceTree] - Tree that initiated this upsert
122+
* (for controlling insert propagation)
62123
*/
63-
scheduleUpsert(resource, newIndexTimestamp) {
124+
scheduleUpsert(resource, newIndexTimestamp, sourceTree = null) {
64125
const resourcePath = resource.getOriginalPath();
65-
this.pendingUpserts.set(resourcePath, resource);
126+
this.pendingUpserts.set(resourcePath, {resource, sourceTree});
66127
// Cancel any pending removal for this path
67128
this.pendingRemovals.delete(resourcePath);
68-
this.pendingTimestampUpdate = newIndexTimestamp;
129+
if (newIndexTimestamp) {
130+
this.pendingTimestampUpdate = newIndexTimestamp;
131+
}
69132
}
70133

71134
/**
@@ -94,8 +157,8 @@ export default class TreeRegistry {
94157
*
95158
* Phase 2: Process upserts (inserts and updates)
96159
* - Group operations by parent directory for efficiency
97-
* - Create missing parent directories as needed
98-
* - Insert new resources or update existing ones
160+
* - For inserts: only create in source tree and its derived trees
161+
* - For updates: apply to all trees that share the resource node
99162
* - Skip updates for resources with unchanged metadata
100163
* - Track modified nodes to avoid duplicate updates to shared nodes
101164
*
@@ -201,39 +264,51 @@ export default class TreeRegistry {
201264
}
202265

203266
// 2. Handle upserts - group by directory
204-
const upsertsByDir = new Map(); // parentPath -> [{resourceName, resource, fullPath}]
267+
const upsertsByDir = new Map(); // parentPath -> [{resourceName, resource, fullPath, sourceTree}]
205268

206-
for (const [resourcePath, resource] of this.pendingUpserts) {
269+
for (const [resourcePath, {resource, sourceTree}] of this.pendingUpserts) {
207270
const parts = resourcePath.split(path.sep).filter((p) => p.length > 0);
208271
const resourceName = parts[parts.length - 1];
209272
const parentPath = parts.slice(0, -1).join(path.sep);
210273

211274
if (!upsertsByDir.has(parentPath)) {
212275
upsertsByDir.set(parentPath, []);
213276
}
214-
upsertsByDir.get(parentPath).push({resourceName, resource, fullPath: resourcePath});
277+
upsertsByDir.get(parentPath).push({resourceName, resource, fullPath: resourcePath, sourceTree});
215278
}
216279

217280
// Apply upserts
218281
for (const [parentPath, upserts] of upsertsByDir) {
219282
for (const tree of this.trees) {
220-
// Ensure parent directory exists
283+
// Check if parent directory exists in this tree
221284
let parentNode = tree._findNode(parentPath);
222-
if (!parentNode) {
223-
parentNode = this._ensureDirectoryPath(
224-
tree, parentPath.split(path.sep).filter((p) => p.length > 0));
225-
}
226-
227-
if (parentNode.type !== "directory") {
228-
continue;
229-
}
230285

231286
let dirModified = false;
232287
for (const upsert of upserts) {
233-
let resourceNode = parentNode.children.get(upsert.resourceName);
288+
let resourceNode = parentNode?.children?.get(upsert.resourceName);
234289

235290
if (!resourceNode) {
236-
// INSERT: Create new resource node
291+
// INSERT: Check derivation rules
292+
if (upsert.sourceTree !== null) {
293+
// Source tree specified - only insert into source tree and its derived trees
294+
if (!this._isTreeOrDerived(upsert.sourceTree, tree)) {
295+
// This tree is not the source tree or derived from it - skip insert
296+
continue;
297+
}
298+
}
299+
// If sourceTree is null, insert into all trees (backward compatibility)
300+
301+
// Ensure parent directory exists (only for trees we're inserting into)
302+
if (!parentNode) {
303+
parentNode = this._ensureDirectoryPath(
304+
tree, parentPath.split(path.sep).filter((p) => p.length > 0));
305+
}
306+
307+
if (parentNode.type !== "directory") {
308+
continue;
309+
}
310+
311+
// Create new resource node
237312
resourceNode = new TreeNode(upsert.resourceName, "resource", {
238313
integrity: await upsert.resource.getIntegrity(),
239314
lastModified: upsert.resource.getLastModified(),
@@ -297,7 +372,7 @@ export default class TreeRegistry {
297372
}
298373
}
299374

300-
if (dirModified) {
375+
if (dirModified && parentNode) {
301376
// Compute hashes for modified/new resources
302377
for (const upsert of upserts) {
303378
const resourceNode = parentNode.children.get(upsert.resourceName);

packages/project/test/lib/build/ProjectBuilder.integration.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,6 @@ test.serial("Build theme.library.e project multiple times", async (t) => {
351351
assertions: {
352352
projects: {"theme.library.e": {
353353
skippedTasks: ["buildThemes"]
354-
// NOTE: buildThemes currently gets NOT skipped -> TODO: fix
355354
}},
356355
}
357356
});

0 commit comments

Comments
 (0)