@@ -2,6 +2,8 @@ import EventEmitter from "node:events";
22import { createReaderCollectionPrioritized } from "@ui5/fs/resourceFactory" ;
33import BuildReader from "./BuildReader.js" ;
44import WatchHandler from "./helpers/WatchHandler.js" ;
5+ import { getLogger } from "@ui5/logger" ;
6+ const log = getLogger ( "build:BuildServer" ) ;
57
68/**
79 * Development server that provides access to built project resources with automatic rebuilding
@@ -27,7 +29,9 @@ import WatchHandler from "./helpers/WatchHandler.js";
2729class BuildServer extends EventEmitter {
2830 #graph;
2931 #projectBuilder;
30- #pCurrentBuild;
32+ #buildQueue = new Map ( ) ;
33+ #pendingBuildRequest = new Set ( ) ;
34+ #activeBuild = null ;
3135 #allReader;
3236 #rootReader;
3337 #dependenciesReader;
@@ -65,12 +69,13 @@ class BuildServer extends EventEmitter {
6569 this . #getReaderForProjects. bind ( this ) ) ;
6670
6771 if ( initialBuildIncludedDependencies . length > 0 ) {
68- this . #pCurrentBuild = projectBuilder . build ( {
69- includedDependencies : initialBuildIncludedDependencies ,
70- excludedDependencies : initialBuildExcludedDependencies
71- } ) . then ( ( builtProjects ) => {
72- this . #projectBuildFinished( builtProjects ) ;
73- } ) . catch ( ( err ) => {
72+ // Enqueue initial build dependencies
73+ for ( const projectName of initialBuildIncludedDependencies ) {
74+ if ( ! initialBuildExcludedDependencies . includes ( projectName ) ) {
75+ this . #pendingBuildRequest. add ( projectName ) ;
76+ }
77+ }
78+ this . #processBuildQueue( ) . catch ( ( err ) => {
7479 this . emit ( "error" , err ) ;
7580 } ) ;
7681 }
@@ -86,10 +91,19 @@ class BuildServer extends EventEmitter {
8691 } ) ;
8792 watchHandler . on ( "sourcesChanged" , ( changes ) => {
8893 // Inform project builder
94+
95+ log . verbose ( "Source changes detected: " , changes ) ;
96+
8997 const affectedProjects = this . #projectBuilder. resourcesChanged ( changes ) ;
9098
9199 for ( const projectName of affectedProjects ) {
100+ log . verbose ( `Invalidating built project '${ projectName } ' due to source changes` ) ;
92101 this . #projectReaders. delete ( projectName ) ;
102+ // If project is currently in build queue, re-enqueue it for rebuild
103+ if ( this . #buildQueue. has ( projectName ) ) {
104+ log . verbose ( `Re-enqueuing project '${ projectName } ' for rebuild` ) ;
105+ this . #pendingBuildRequest. add ( projectName ) ;
106+ }
93107 }
94108
95109 const changedResourcePaths = [ ...changes . values ( ) ] . flat ( ) ;
@@ -142,8 +156,8 @@ class BuildServer extends EventEmitter {
142156 * Gets a reader for a single project, building it if necessary
143157 *
144158 * Checks if the project has already been built and returns its reader from cache.
145- * If not built, waits for any in-progress build, then triggers a build for the
146- * requested project .
159+ * If not built, enqueues the project for building and returns a promise that
160+ * resolves when the reader is available .
147161 *
148162 * @param {string } projectName Name of the project to get reader for
149163 * @returns {Promise<@ui5/fs/AbstractReader> } Reader for the built project
@@ -152,73 +166,30 @@ class BuildServer extends EventEmitter {
152166 if ( this . #projectReaders. has ( projectName ) ) {
153167 return this . #projectReaders. get ( projectName ) ;
154168 }
155- if ( this . #pCurrentBuild) {
156- // If set, await currently running build
157- await this . #pCurrentBuild;
158- }
159- if ( this . #projectReaders. has ( projectName ) ) {
160- return this . #projectReaders. get ( projectName ) ;
161- }
162- this . #pCurrentBuild = this . #projectBuilder. build ( {
163- includedDependencies : [ projectName ]
164- } ) . catch ( ( err ) => {
165- this . emit ( "error" , err ) ;
166- } ) ;
167- const builtProjects = await this . #pCurrentBuild;
168- this . #projectBuildFinished( builtProjects ) ;
169-
170- // Clear current build promise
171- this . #pCurrentBuild = null ;
172-
173- return this . #projectReaders. get ( projectName ) ;
169+ return this . #enqueueBuild( projectName ) ;
174170 }
175171
176172 /**
177173 * Gets a combined reader for multiple projects, building them if necessary
178174 *
179- * Determines which projects need to be built, waits for any in-progress build,
180- * then triggers a build for any missing projects. Returns a prioritized collection
181- * reader combining all requested projects.
175+ * Enqueues all projects that need to be built and waits for all of them to complete.
176+ * Returns a prioritized collection reader combining all requested projects.
182177 *
183178 * @param {string[] } projectNames Array of project names to get readers for
184179 * @returns {Promise<@ui5/fs/ReaderCollection> } Combined reader for all requested projects
185180 */
186181 async #getReaderForProjects( projectNames ) {
187- let projectsRequiringBuild = [ ] ;
188- for ( const projectName of projectNames ) {
189- if ( ! this . #projectReaders. has ( projectName ) ) {
190- projectsRequiringBuild . push ( projectName ) ;
191- }
192- }
193- if ( projectsRequiringBuild . length === 0 ) {
194- // Projects already built
195- return this . #getReaderForCachedProjects( projectNames ) ;
196- }
197- if ( this . #pCurrentBuild) {
198- // If set, await currently running build
199- await this . #pCurrentBuild;
200- }
201- projectsRequiringBuild = [ ] ;
182+ // Enqueue all projects that aren't cached yet
183+ const buildPromises = [ ] ;
202184 for ( const projectName of projectNames ) {
203185 if ( ! this . #projectReaders. has ( projectName ) ) {
204- projectsRequiringBuild . push ( projectName ) ;
186+ buildPromises . push ( this . #enqueueBuild ( projectName ) ) ;
205187 }
206188 }
207- if ( projectsRequiringBuild . length === 0 ) {
208- // Projects already built
209- return this . #getReaderForCachedProjects ( projectNames ) ;
189+ // Wait for all builds to complete
190+ if ( buildPromises . length > 0 ) {
191+ await Promise . all ( buildPromises ) ;
210192 }
211- this . #pCurrentBuild = this . #projectBuilder. build ( {
212- includedDependencies : projectsRequiringBuild
213- } ) . catch ( ( err ) => {
214- this . emit ( "error" , err ) ;
215- } ) ;
216- const builtProjects = await this . #pCurrentBuild;
217- this . #projectBuildFinished( builtProjects ) ;
218-
219- // Clear current build promise
220- this . #pCurrentBuild = null ;
221-
222193 return this . #getReaderForCachedProjects( projectNames ) ;
223194 }
224195
@@ -245,32 +216,106 @@ class BuildServer extends EventEmitter {
245216 } ) ;
246217 }
247218
248- // async #getReaderForAllProjects() {
249- // if (this.#pCurrentBuild) {
250- // // If set, await initial build
251- // await this.#pCurrentBuild;
252- // }
253- // if (this.#allProjectsReader) {
254- // return this.#allProjectsReader;
255- // }
256- // this.#pCurrentBuild = this.#projectBuilder.build({
257- // includedDependencies: ["*"]
258- // }).catch((err) => {
259- // this.emit("error", err);
260- // });
261- // const builtProjects = await this.#pCurrentBuild;
262- // this.#projectBuildFinished(builtProjects);
263-
264- // // Clear current build promise
265- // this.#pCurrentBuild = null;
266-
267- // // Create a combined reader for all projects
268- // this.#allProjectsReader = createReaderCollectionPrioritized({
269- // name: "All projects build reader",
270- // readers: [...this.#projectReaders.values()]
271- // });
272- // return this.#allProjectsReader;
273- // }
219+ /**
220+ * Enqueues a project for building and returns a promise that resolves with its reader
221+ *
222+ * If the project is already queued, returns the existing promise. Otherwise, creates
223+ * a new promise, adds the project to the pending build queue, and triggers queue processing.
224+ *
225+ * @param {string } projectName Name of the project to enqueue
226+ * @returns {Promise<@ui5/fs/AbstractReader> } Promise that resolves with the project's reader
227+ */
228+ #enqueueBuild( projectName ) {
229+ // If already queued, return existing promise
230+ if ( this . #buildQueue. has ( projectName ) ) {
231+ return this . #buildQueue. get ( projectName ) . promise ;
232+ }
233+
234+ log . verbose ( `Enqueuing project '${ projectName } ' for build` ) ;
235+
236+ // Create new promise for this project
237+ let resolve ;
238+ let reject ;
239+ const promise = new Promise ( ( res , rej ) => {
240+ resolve = res ;
241+ reject = rej ;
242+ } ) ;
243+
244+ // Store promise and resolvers in the queue
245+ this . #buildQueue. set ( projectName , { promise, resolve, reject} ) ;
246+
247+ // Add to pending build requests
248+ this . #pendingBuildRequest. add ( projectName ) ;
249+
250+ // Trigger queue processing if no build is active
251+ if ( ! this . #activeBuild) {
252+ this . #processBuildQueue( ) . catch ( ( err ) => {
253+ this . emit ( "error" , err ) ;
254+ } ) ;
255+ }
256+
257+ return promise ;
258+ }
259+
260+ /**
261+ * Processes the build queue by batching pending projects and building them
262+ *
263+ * Runs while there are pending build requests. Collects all pending projects,
264+ * builds them in a single batch, resolves/rejects promises for built projects,
265+ * and handles errors with proper isolation.
266+ *
267+ * @returns {Promise<void> } Promise that resolves when queue processing is complete
268+ */
269+ async #processBuildQueue( ) {
270+ // Process queue while there are pending requests
271+ while ( this . #pendingBuildRequest. size > 0 ) {
272+ // Collect all pending projects for this batch
273+ const projectsToBuild = Array . from ( this . #pendingBuildRequest) ;
274+ this . #pendingBuildRequest. clear ( ) ;
275+
276+ log . verbose ( `Building projects: ${ projectsToBuild . join ( ", " ) } ` ) ;
277+
278+ // Set active build to prevent concurrent builds
279+ const buildPromise = this . #activeBuild = this . #projectBuilder. build ( {
280+ includedDependencies : projectsToBuild
281+ } ) ;
282+
283+ try {
284+ const builtProjects = await buildPromise ;
285+ this . #projectBuildFinished( builtProjects ) ;
286+
287+ // Resolve promises for all successfully built projects
288+ for ( const projectName of builtProjects ) {
289+ const queueEntry = this . #buildQueue. get ( projectName ) ;
290+ if ( queueEntry ) {
291+ const reader = this . #projectReaders. get ( projectName ) ;
292+ queueEntry . resolve ( reader ) ;
293+ // Only remove from queue if not re-enqueued during build
294+ if ( ! this . #pendingBuildRequest. has ( projectName ) ) {
295+ log . verbose ( `Project '${ projectName } ' build finished. Removing from build queue.` ) ;
296+ this . #buildQueue. delete ( projectName ) ;
297+ }
298+ }
299+ }
300+ } catch ( err ) {
301+ // Build failed - reject promises for projects that weren't built
302+ for ( const projectName of projectsToBuild ) {
303+ log . error ( `Project '${ projectName } ' build failed: ${ err . message } ` ) ;
304+ const queueEntry = this . #buildQueue. get ( projectName ) ;
305+ if ( queueEntry && ! this . #projectReaders. has ( projectName ) ) {
306+ queueEntry . reject ( err ) ;
307+ this . #buildQueue. delete ( projectName ) ;
308+ this . #pendingBuildRequest. delete ( projectName ) ;
309+ }
310+ }
311+ // Re-throw to be handled by caller
312+ throw err ;
313+ } finally {
314+ // Clear active build
315+ this . #activeBuild = null ;
316+ }
317+ }
318+ }
274319
275320 /**
276321 * Handles completion of a project build
0 commit comments