@@ -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 */
2326export 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 ) ;
0 commit comments