Skip to content

Commit f58c781

Browse files
committed
fix(@angular/build): safeguard Karma builder stream controller against closed state
When running Karma tests or when stream consumers (such as Architect test harness `executeOnce`) cancel the builder output stream early, both `ProgressNotifierReporter.onRunComplete` and `karma.Server` exit callbacks can attempt to enqueue results or close the `ReadableStreamController`. Under WHATWG Streams specification rules, calling `.enqueue()` or `.close()` on a controller whose `desiredSize` is `null` (closed or cancelled) throws `TypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closed`. This change checks `controller.desiredSize !== null` and wraps enqueue/close calls in a try-catch block to gracefully handle closed controllers.
1 parent 9671cc6 commit f58c781

3 files changed

Lines changed: 46 additions & 10 deletions

File tree

packages/angular/build/src/builders/karma/application_builder.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from
1111
import { randomUUID } from 'node:crypto';
1212
import { rmSync } from 'node:fs';
1313
import * as fs from 'node:fs/promises';
14-
import { createRequire } from 'node:module';
1514
import path from 'node:path';
1615
import { ReadableStream } from 'node:stream/web';
1716
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
@@ -66,8 +65,14 @@ export function execute(
6665
init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms);
6766
} catch (err) {
6867
if (err instanceof ApplicationBuildError) {
69-
controller.enqueue({ success: false, message: err.message });
70-
controller.close();
68+
if (controller.desiredSize !== null) {
69+
try {
70+
controller.enqueue({ success: false, message: err.message });
71+
controller.close();
72+
} catch {
73+
// Stream controller may already be closed or cancelled
74+
}
75+
}
7176

7277
return;
7378
}
@@ -85,8 +90,14 @@ export function execute(
8590

8691
// Close the stream once the Karma server returns.
8792
karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => {
88-
controller.enqueue({ success: exitCode === 0 });
89-
controller.close();
93+
if (controller.desiredSize !== null) {
94+
try {
95+
controller.enqueue({ success: exitCode === 0 });
96+
controller.close();
97+
} catch {
98+
// Stream controller may already be closed or cancelled
99+
}
100+
}
90101
});
91102

92103
await karmaServer.start();

packages/angular/build/src/builders/karma/progress-reporter.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,18 @@ export function injectKarmaReporter(
5959
break;
6060
}
6161

62+
if (controller.desiredSize === null) {
63+
break;
64+
}
65+
6266
if (buildOutput.kind === ResultKind.Failure) {
63-
controller.enqueue({ success: false, message: 'Build failed' });
67+
if (controller.desiredSize !== null) {
68+
try {
69+
controller.enqueue({ success: false, message: 'Build failed' });
70+
} catch {
71+
// Stream controller may already be closed or cancelled
72+
}
73+
}
6474
} else if (
6575
buildOutput.kind === ResultKind.Incremental ||
6676
buildOutput.kind === ResultKind.Full
@@ -81,10 +91,12 @@ export function injectKarmaReporter(
8191
}
8292

8393
onRunComplete = function (_browsers: unknown, results: RunCompleteInfo): void {
84-
if (results.exitCode === 0) {
85-
controller.enqueue({ success: true });
86-
} else {
87-
controller.enqueue({ success: false });
94+
if (controller.desiredSize !== null) {
95+
try {
96+
controller.enqueue({ success: results.exitCode === 0 });
97+
} catch {
98+
// Stream controller may already be closed or cancelled
99+
}
88100
}
89101
};
90102
}

packages/angular/build/src/builders/karma/tests/behavior/errors_spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,18 @@ describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
2828

2929
expect(result?.success).toBeFalse();
3030
});
31+
32+
it('handles stream cancellation gracefully in watch mode', async () => {
33+
harness.useTarget('test', {
34+
...BASE_OPTIONS,
35+
watch: true,
36+
});
37+
38+
const { result } = await harness.executeOnce({
39+
outputLogsOnFailure: false,
40+
});
41+
42+
expect(result?.success).toBeTrue();
43+
});
3144
});
3245
});

0 commit comments

Comments
 (0)