Skip to content

Commit 5983b6c

Browse files
authored
Merge pull request #986 from Aukevanoost/fix/runBuilder-abort-controller
fix(nf): Added AbortController to runBuilder to cancel previous builds
2 parents 233e790 + 1629bee commit 5983b6c

12 files changed

Lines changed: 226 additions & 42 deletions

File tree

libs/native-federation-core/src/build.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export { loadFederationConfig } from './lib/core/load-federation-config';
2121
export { writeFederationInfo } from './lib/core/write-federation-info';
2222
export { writeImportMap } from './lib/core/write-import-map';
2323
export { MappedPath } from './lib/utils/mapped-paths';
24-
24+
export { RebuildQueue } from './lib/utils/rebuild-queue';
2525
export {
2626
findRootTsConfigJson,
2727
share,
@@ -33,4 +33,5 @@ export {
3333
} from './lib/core/federation-builder';
3434
export * from './lib/utils/build-result-map';
3535
export { hashFile } from './lib/utils/hash-file';
36+
export * from './lib/utils/errors';
3637
export { logger, setLogLevel } from './lib/utils/logger';

libs/native-federation-core/src/lib/core/build-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface BuildAdapterOptions {
3232
hash: boolean;
3333
platform?: 'browser' | 'node';
3434
optimizedMappings?: boolean;
35+
signal?: AbortSignal;
3536
}
3637

3738
export interface BuildResult {

libs/native-federation-core/src/lib/core/build-for-federation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { FederationOptions } from './federation-options';
1414
import { writeFederationInfo } from './write-federation-info';
1515
import { writeImportMap } from './write-import-map';
1616
import { logger } from '../utils/logger';
17+
import { AbortedError } from '../utils/errors';
1718

1819
export interface BuildParams {
1920
skipMappingsAndExposed: boolean;
2021
skipShared: boolean;
22+
signal?: AbortSignal;
2123
}
2224

2325
export const defaultBuildParams: BuildParams = {
@@ -35,6 +37,8 @@ export async function buildForFederation(
3537
externals: string[],
3638
buildParams = defaultBuildParams,
3739
): Promise<FederationInfo> {
40+
const signal = buildParams.signal;
41+
3842
let artefactInfo: ArtefactInfo | undefined;
3943

4044
if (!buildParams.skipMappingsAndExposed) {
@@ -43,11 +47,17 @@ export async function buildForFederation(
4347
config,
4448
fedOptions,
4549
externals,
50+
signal,
4651
);
4752
logger.measure(
4853
start,
4954
'[build artifacts] - To bundle all mappings and exposed.',
5055
);
56+
57+
if (signal?.aborted)
58+
throw new AbortedError(
59+
'[buildForFederation] After exposed-and-mappings bundle',
60+
);
5161
}
5262

5363
const exposedInfo = !artefactInfo
@@ -77,6 +87,10 @@ export async function buildForFederation(
7787
Object.keys(sharedBrowser).forEach((packageName) =>
7888
cachedSharedPackages.add(packageName),
7989
);
90+
if (signal?.aborted)
91+
throw new AbortedError(
92+
'[buildForFederation] After shared-browser bundle',
93+
);
8094
}
8195

8296
if (Object.keys(sharedServer).length > 0) {
@@ -96,6 +110,8 @@ export async function buildForFederation(
96110
Object.keys(sharedServer).forEach((packageName) =>
97111
cachedSharedPackages.add(packageName),
98112
);
113+
if (signal?.aborted)
114+
throw new AbortedError('[buildForFederation] After shared-node bundle');
99115
}
100116

101117
if (Object.keys(separateBrowser).length > 0) {
@@ -115,6 +131,10 @@ export async function buildForFederation(
115131
Object.keys(separateBrowser).forEach((packageName) =>
116132
cachedSharedPackages.add(packageName),
117133
);
134+
if (signal?.aborted)
135+
throw new AbortedError(
136+
'[buildForFederation] After separate-browser bundle',
137+
);
118138
}
119139

120140
if (Object.keys(separateServer).length > 0) {
@@ -135,6 +155,9 @@ export async function buildForFederation(
135155
cachedSharedPackages.add(packageName),
136156
);
137157
}
158+
159+
if (signal?.aborted)
160+
throw new AbortedError('[buildForFederation] After separate-node bundle');
138161
}
139162

140163
const sharedMappingInfo = !artefactInfo

libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { bundle } from '../utils/build-utils';
1111
import { logger } from '../utils/logger';
1212
import { normalize } from '../utils/normalize';
1313
import { FederationOptions } from './federation-options';
14+
import { AbortedError } from '../utils/errors';
1415

1516
export interface ArtefactInfo {
1617
mappings: SharedInfo[];
@@ -21,7 +22,14 @@ export async function bundleExposedAndMappings(
2122
config: NormalizedFederationConfig,
2223
fedOptions: FederationOptions,
2324
externals: string[],
25+
signal?: AbortSignal,
2426
): Promise<ArtefactInfo> {
27+
if (signal?.aborted) {
28+
throw new AbortedError(
29+
'[bundle-exposed-and-mappings] Aborted before bundling',
30+
);
31+
}
32+
2533
const shared = config.sharedMappings.map((sm) => {
2634
const entryPoint = sm.path;
2735
const tmp = sm.key.replace(/[^A-Za-z0-9]/g, '_');
@@ -53,9 +61,17 @@ export async function bundleExposedAndMappings(
5361
kind: 'mapping-or-exposed',
5462
hash,
5563
optimizedMappings: config.features.ignoreUnusedDeps,
64+
signal,
5665
});
66+
if (signal?.aborted) {
67+
throw new AbortedError(
68+
'[bundle-exposed-and-mappings] Aborted after bundle',
69+
);
70+
}
5771
} catch (error) {
58-
logger.error('Error building federation artefacts');
72+
if (!(error instanceof AbortedError)) {
73+
logger.error('Error building federation artefacts');
74+
}
5975
throw error;
6076
}
6177

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class AbortedError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = 'AbortedError';
5+
Object.setPrototypeOf(this, AbortedError.prototype);
6+
}
7+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { logger } from './logger';
2+
3+
interface BuildControl {
4+
controller: AbortController;
5+
buildFinished: { resolve: () => void; promise: Promise<void> };
6+
}
7+
8+
export class RebuildQueue {
9+
private activeBuilds: Map<number, BuildControl> = new Map();
10+
private buildCounter = 0;
11+
12+
async enqueue(
13+
rebuildFn: (signal: AbortSignal) => Promise<void>,
14+
): Promise<void> {
15+
const buildId = ++this.buildCounter;
16+
17+
const pendingCancellations = Array.from(this.activeBuilds.values()).map(
18+
(buildInfo) => {
19+
buildInfo.controller.abort();
20+
return buildInfo.buildFinished.promise;
21+
},
22+
);
23+
24+
if (pendingCancellations.length > 0) {
25+
logger.info(`Aborting ${pendingCancellations.length} bundling task(s)..`);
26+
}
27+
28+
if (pendingCancellations.length > 0) {
29+
await Promise.all(pendingCancellations);
30+
}
31+
32+
let buildFinished: () => void;
33+
const completionPromise = new Promise<void>((resolve) => {
34+
buildFinished = resolve;
35+
});
36+
37+
const control: BuildControl = {
38+
controller: new AbortController(),
39+
buildFinished: {
40+
resolve: buildFinished!,
41+
promise: completionPromise,
42+
},
43+
};
44+
this.activeBuilds.set(buildId, control);
45+
46+
try {
47+
await rebuildFn(control.controller.signal);
48+
} finally {
49+
control.buildFinished.resolve();
50+
this.activeBuilds.delete(buildId);
51+
}
52+
}
53+
54+
dispose(): void {
55+
for (const [_, buildInfo] of this.activeBuilds) {
56+
buildInfo.controller.abort();
57+
}
58+
this.activeBuilds.clear();
59+
}
60+
}

libs/native-federation-node/src/lib/utils/fstart.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ function resolveAndComposeImportMap(parsed) {
198198
);
199199
if (invalidKeys.length > 0) {
200200
console.warn(
201-
`Invalid top-level key${invalidKeys.length > 0 ? 's' : ''} in import map - ${invalidKeys.join(', ')}`,
201+
`Invalid top-level key${
202+
invalidKeys.length > 0 ? 's' : ''
203+
} in import map - ${invalidKeys.join(', ')}`,
202204
);
203205
}
204206
return {

libs/native-federation-runtime/src/lib/model/build-notifications-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export const BUILD_NOTIFICATIONS_ENDPOINT =
99
export enum BuildNotificationType {
1010
COMPLETED = 'federation-rebuild-complete',
1111
ERROR = 'federation-rebuild-error',
12+
CANCELLED = 'federation-rebuild-cancelled',
1213
}

libs/native-federation/src/builders/build/builder.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
logger,
2626
setBuildAdapter,
2727
setLogLevel,
28+
RebuildQueue,
29+
AbortedError,
2830
} from '@softarc/native-federation/build';
2931
import {
3032
createAngularBuildAdapter,
@@ -373,8 +375,9 @@ export async function* runBuilder(
373375
indexHtmlTransformer: transformIndexHtml(nfOptions),
374376
});
375377

378+
const rebuildQueue = new RebuildQueue();
379+
376380
try {
377-
// builderRun.output.subscribe(async (output) => {
378381
for await (const output of builderRun) {
379382
lastResult = output;
380383

@@ -399,8 +402,31 @@ export async function* runBuilder(
399402
// }
400403

401404
if (!first && (nfOptions.dev || watch)) {
402-
setTimeout(async () => {
403-
try {
405+
rebuildQueue
406+
.enqueue(async (signal: AbortSignal) => {
407+
if (signal?.aborted) {
408+
throw new AbortedError('Build canceled before starting');
409+
}
410+
411+
await new Promise((resolve, reject) => {
412+
const timeout = setTimeout(
413+
resolve,
414+
Math.max(10, nfOptions.rebuildDelay),
415+
);
416+
417+
if (signal) {
418+
const abortHandler = () => {
419+
clearTimeout(timeout);
420+
reject(new AbortedError('[builder] During delay.'));
421+
};
422+
signal.addEventListener('abort', abortHandler, { once: true });
423+
}
424+
});
425+
426+
if (signal?.aborted) {
427+
throw new AbortedError('[builder] Before federation build.');
428+
}
429+
404430
const start = process.hrtime();
405431
federationResult = await buildForFederation(
406432
config,
@@ -409,9 +435,14 @@ export async function* runBuilder(
409435
{
410436
skipMappingsAndExposed: false,
411437
skipShared: true,
438+
signal,
412439
},
413440
);
414441

442+
if (signal?.aborted) {
443+
throw new AbortedError('[builder] After federation build.');
444+
}
445+
415446
if (hasLocales && localeFilter) {
416447
translateFederationArtefacts(
417448
i18n,
@@ -421,27 +452,40 @@ export async function* runBuilder(
421452
);
422453
}
423454

455+
if (signal?.aborted) {
456+
throw new AbortedError(
457+
'[builder] After federation translations.',
458+
);
459+
}
460+
424461
logger.info('Done!');
425462

426-
// Notifies about build completion
427463
if (isLocalDevelopment) {
428464
federationBuildNotifier.broadcastBuildCompletion();
429465
}
430-
logger.measure(start, 'To rebuild nf.');
431-
} catch (error) {
432-
logger.error('Federation rebuild failed!');
433-
434-
// Notifies about build failure
435-
if (isLocalDevelopment) {
436-
federationBuildNotifier.broadcastBuildError(error);
466+
logger.measure(start, 'To rebuild the federation artifacts.');
467+
})
468+
.catch((error) => {
469+
if (error instanceof AbortedError) {
470+
logger.verbose(
471+
'Rebuild was canceled. Cancellation point: ' + error?.message,
472+
);
473+
federationBuildNotifier.broadcastBuildCancellation();
474+
} else {
475+
logger.error('Federation rebuild failed!');
476+
if (options.verbose) console.error(error);
477+
if (isLocalDevelopment) {
478+
federationBuildNotifier.broadcastBuildError(error);
479+
}
437480
}
438-
}
439-
}, nfOptions.rebuildDelay);
481+
});
440482
}
441483

442484
first = false;
443485
}
444486
} finally {
487+
rebuildQueue.dispose();
488+
445489
if (isLocalDevelopment) {
446490
federationBuildNotifier.stopEventServer();
447491
}

libs/native-federation/src/builders/build/federation-build-notifier.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ export class FederationBuildNotifier {
192192
});
193193
}
194194

195+
/**
196+
* Notifies about cancellation of a federation rebuild
197+
*/
198+
public broadcastBuildCancellation(): void {
199+
this._broadcastEvent({
200+
type: BuildNotificationType.CANCELLED,
201+
timestamp: Date.now(),
202+
});
203+
}
204+
195205
/**
196206
* Notifies about failed federation rebuild
197207
*/

0 commit comments

Comments
 (0)