Skip to content

Commit 556d878

Browse files
committed
feat(utils): create helper function for sequential Promise.all
1 parent ba5d177 commit 556d878

File tree

7 files changed

+88
-45
lines changed

7 files changed

+88
-45
lines changed

packages/ci/src/lib/run-monorepo.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
22
import type { CoreConfig } from '@code-pushup/models';
33
import {
44
type ExcludeNullableProps,
5+
asyncSequential,
56
hasNoNullableProps,
67
} from '@code-pushup/utils';
78
import {
@@ -97,10 +98,7 @@ function runProjectsIndividually(
9798
env.settings.logger.info(
9899
`Running on ${projects.length} projects individually`,
99100
);
100-
return projects.reduce<Promise<ProjectRunResult[]>>(
101-
async (acc, project) => [...(await acc), await runOnProject(project, env)],
102-
Promise.resolve([]),
103-
);
101+
return asyncSequential(projects, project => runOnProject(project, env));
104102
}
105103

106104
async function runProjectsInBulk(
@@ -186,14 +184,11 @@ async function compareProjectsInBulk(
186184
}))
187185
.filter(hasNoNullableProps);
188186

189-
const projectComparisons = await projectsToCompare.reduce<
190-
Promise<Record<string, ProjectRunResult>>
191-
>(
192-
async (acc, args) => ({
193-
...(await acc),
194-
[args.project.name]: await compareReports(args),
195-
}),
196-
Promise.resolve({}),
187+
const projectComparisons = Object.fromEntries(
188+
await asyncSequential(projectsToCompare, async args => [
189+
args.project.name,
190+
await compareReports(args),
191+
]),
197192
);
198193

199194
return finalizeProjectReports(currProjectReports, projectComparisons);

packages/plugin-eslint/src/lib/meta/versions/legacy.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ESLint, Linter } from 'eslint';
2-
import { distinct, exists, toArray, ui } from '@code-pushup/utils';
2+
import {
3+
asyncSequential,
4+
distinct,
5+
exists,
6+
toArray,
7+
ui,
8+
} from '@code-pushup/utils';
39
import type { ESLintTarget } from '../../config.js';
410
import { setupESLint } from '../../setup.js';
511
import { type RuleData, isRuleOff, optionsFromRuleEntry } from '../parse.js';
@@ -10,12 +16,10 @@ export async function loadRulesForLegacyConfig({
1016
}: ESLintTarget): Promise<RuleData[]> {
1117
const eslint = await setupESLint(eslintrc);
1218

13-
const configs = await toArray(patterns).reduce(
14-
async (acc, pattern) => [
15-
...(await acc),
16-
(await eslint.calculateConfigForFile(pattern)) as Linter.LegacyConfig,
17-
],
18-
Promise.resolve<Linter.LegacyConfig[]>([]),
19+
const configs = await asyncSequential(
20+
toArray(patterns),
21+
pattern =>
22+
eslint.calculateConfigForFile(pattern) as Promise<Linter.LegacyConfig>,
1923
);
2024

2125
const rulesIds = distinct(

packages/plugin-eslint/src/lib/runner/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
RunnerFilesPaths,
88
} from '@code-pushup/models';
99
import {
10+
asyncSequential,
1011
createRunnerFiles,
1112
ensureDirectoryExists,
1213
filePathToCliArg,
@@ -17,7 +18,6 @@ import {
1718
import type { ESLintPluginRunnerConfig, ESLintTarget } from '../config.js';
1819
import { lint } from './lint.js';
1920
import { lintResultsToAudits, mergeLinterOutputs } from './transform.js';
20-
import type { LinterOutput } from './types.js';
2121

2222
export async function executeRunner({
2323
runnerConfigPath,
@@ -28,10 +28,7 @@ export async function executeRunner({
2828

2929
ui().logger.log(`ESLint plugin executing ${targets.length} lint targets`);
3030

31-
const linterOutputs = await targets.reduce(
32-
async (acc, target) => [...(await acc), await lint(target)],
33-
Promise.resolve<LinterOutput[]>([]),
34-
);
31+
const linterOutputs = await asyncSequential(targets, lint);
3532
const lintResults = mergeLinterOutputs(linterOutputs);
3633
const failedAudits = lintResultsToAudits(lintResults);
3734

packages/utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export {
6262
safeCheckout,
6363
toGitPath,
6464
} from './lib/git/git.js';
65-
export { groupByStatus } from './lib/group-by-status.js';
6665
export {
6766
hasNoNullableProps,
6867
isPromiseFulfilledResult,
@@ -72,6 +71,7 @@ export { logMultipleResults } from './lib/log-results.js';
7271
export { isVerbose, link, ui, type CliUi, type Column } from './lib/logging.js';
7372
export { mergeConfigs } from './lib/merge-configs.js';
7473
export { getProgressBar, type ProgressBar } from './lib/progress.js';
74+
export { asyncSequential, groupByStatus } from './lib/promises.js';
7575
export { generateRandomId } from './lib/random.js';
7676
export {
7777
CODE_PUSHUP_DOMAIN,

packages/utils/src/lib/group-by-status.unit.test.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

packages/utils/src/lib/group-by-status.ts renamed to packages/utils/src/lib/promises.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,18 @@ export function groupByStatus<T>(results: PromiseSettledResult<T>[]): {
1313
{ fulfilled: [], rejected: [] },
1414
);
1515
}
16+
17+
export async function asyncSequential<TInput, TOutput>(
18+
items: TInput[],
19+
work: (item: TInput) => Promise<TOutput>,
20+
): Promise<TOutput[]> {
21+
// for-loop used instead of reduce for performance
22+
const results: TOutput[] = [];
23+
// eslint-disable-next-line functional/no-loop-statements
24+
for (const item of items) {
25+
const result = await work(item);
26+
// eslint-disable-next-line functional/immutable-data
27+
results.push(result);
28+
}
29+
return results;
30+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe } from 'vitest';
2+
import { asyncSequential, groupByStatus } from './promises.js';
3+
4+
describe('groupByStatus', () => {
5+
it('should group results by status', () => {
6+
const results = [
7+
{ status: 'fulfilled', value: 'first' },
8+
{ status: 'rejected', reason: 'second' },
9+
{ status: 'fulfilled', value: 'third' },
10+
] as PromiseSettledResult<string>[];
11+
const grouped = groupByStatus(results);
12+
expect(grouped).toEqual({
13+
fulfilled: [
14+
{ status: 'fulfilled', value: 'first' },
15+
{ status: 'fulfilled', value: 'third' },
16+
],
17+
rejected: [{ status: 'rejected', reason: 'second' }],
18+
});
19+
});
20+
});
21+
22+
describe('asyncSequential', () => {
23+
it('should map async function to array', async () => {
24+
await expect(
25+
asyncSequential(['a', 'b', 'c'], x => Promise.resolve(x.toUpperCase())),
26+
).resolves.toEqual(['A', 'B', 'C']);
27+
});
28+
29+
it('should wait for previous item to resolve before processing next item', async () => {
30+
let counter = 0;
31+
const work = vi.fn().mockImplementation(
32+
() =>
33+
new Promise(resolve => {
34+
counter++;
35+
setTimeout(() => {
36+
resolve(counter);
37+
}, 10);
38+
}),
39+
);
40+
41+
const items = Array.from({ length: 4 });
42+
43+
await expect(asyncSequential(items, work)).resolves.toEqual([1, 2, 3, 4]);
44+
45+
counter = 0;
46+
const sequentialResult = await asyncSequential(items, work); // [1, 2, 3, 4]
47+
counter = 0;
48+
const parallelResult = await Promise.all(items.map(work)); // [4, 4, 4, 4]
49+
50+
expect(sequentialResult).not.toEqual(parallelResult);
51+
});
52+
});

0 commit comments

Comments
 (0)