Skip to content

Commit 52a202c

Browse files
committed
feat(concurrent): support max concurrent installs
Support max concurrent installs in the programmatic API
1 parent cc273ae commit 52a202c

File tree

2 files changed

+53
-3
lines changed

2 files changed

+53
-3
lines changed

src/LocalInstaller.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EventEmitter } from 'events';
22
import { promises as fs } from 'fs';
3+
import os from 'os';
34
import path from 'path';
45
import { helpers } from './helpers.ts';
56
import type { InstallTarget, PackageJson } from './index.ts';
@@ -17,7 +18,8 @@ export interface Env {
1718

1819
export interface Options {
1920
npmEnv?: Env;
20-
packageManager: PackageManager | undefined;
21+
packageManager?: PackageManager;
22+
concurrent?: number;
2123
}
2224

2325
export interface ListByPackage {
@@ -82,7 +84,13 @@ export class LocalInstaller extends EventEmitter {
8284

8385
private async installAll(installTargets: InstallTarget[]): Promise<void> {
8486
this.emit('install_start', this.sourcesByTarget);
85-
await Promise.all(installTargets.map((target) => this.installOne(target)));
87+
// Install targets with max concurrent operations
88+
const batchSize = this.options.concurrent ?? os.cpus().length;
89+
for (let i = 0; i < installTargets.length; i += batchSize) {
90+
const batch = installTargets.slice(i, i + batchSize);
91+
await Promise.all(batch.map((target) => this.installOne(target)));
92+
}
93+
8694
this.emit('install_end');
8795
}
8896

test/unit/LocalInstaller.spec.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'chai';
2-
import type { ResultPromise } from 'execa';
2+
import type { Options, Result, ResultPromise } from 'execa';
33
import { promises as fs } from 'fs';
44
import os from 'os';
55
import { resolve } from 'path';
@@ -267,6 +267,44 @@ describe('LocalInstaller install', () => {
267267
});
268268
});
269269

270+
describe('with concurrent', () => {
271+
beforeEach(() => {
272+
sut = new LocalInstaller(
273+
{ '/a': ['c'], '/b': ['c'] },
274+
{ packageManager: 'npm', concurrent: 1 },
275+
);
276+
stubPackageJson({
277+
'/a': 'a',
278+
'/b': 'b',
279+
c: 'c',
280+
});
281+
helper.rmStub.resolves();
282+
});
283+
284+
it('should install with the specified concurrency', async () => {
285+
const calls: ((res: Result<Options>) => void)[] = [];
286+
helper.execStub.callsFake((file, args) => {
287+
if(file === 'npm' && args?.includes('pack')) {
288+
return Promise.resolve(createExecaResult()) as ResultPromise;
289+
}
290+
return new Promise(res => {
291+
calls.push(res);
292+
}) as ResultPromise;
293+
});
294+
295+
const onGoingInstall = sut.install();
296+
await tick();
297+
await tick();
298+
expect(calls).to.have.lengthOf(1);
299+
calls[0](createExecaResult());
300+
await tick();
301+
await tick();
302+
expect(calls).to.have.lengthOf(2);
303+
calls[1](createExecaResult());
304+
await onGoingInstall
305+
});
306+
});
307+
270308
describe('when readFile errors', () => {
271309
it('should propagate the error', () => {
272310
helper.readFileStub.rejects(new Error('file error'));
@@ -320,3 +358,7 @@ describe('LocalInstaller install', () => {
320358
});
321359
};
322360
});
361+
362+
function tick(): Promise<void> {
363+
return new Promise((res) => process.nextTick(res));
364+
}

0 commit comments

Comments
 (0)