Skip to content

Commit fa275ed

Browse files
committed
refactor(project): Implement queue system in BuildServer
1 parent dc968e1 commit fa275ed

1 file changed

Lines changed: 132 additions & 87 deletions

File tree

packages/project/lib/build/BuildServer.js

Lines changed: 132 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import EventEmitter from "node:events";
22
import {createReaderCollectionPrioritized} from "@ui5/fs/resourceFactory";
33
import BuildReader from "./BuildReader.js";
44
import 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";
2729
class 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

Comments
 (0)