diff --git a/packages/client-typescript/src/cli/rocketride.ts b/packages/client-typescript/src/cli/rocketride.ts index a11befbcf..9e2d2ac7c 100644 --- a/packages/client-typescript/src/cli/rocketride.ts +++ b/packages/client-typescript/src/cli/rocketride.ts @@ -1452,8 +1452,7 @@ export class RocketRideCLI { }); // Upload files - progress events come through event subscription - // Server handles concurrency automatically - const results = await this.client!.sendFiles(fileObjects, taskToken!); + const results = await this.client!.sendFiles(fileObjects, taskToken!, this.args.max_concurrent); const endTime = Date.now(); diff --git a/packages/client-typescript/src/client/client.ts b/packages/client-typescript/src/client/client.ts index fc1fafd09..4ac49466e 100644 --- a/packages/client-typescript/src/client/client.ts +++ b/packages/client-typescript/src/client/client.ts @@ -1371,9 +1371,11 @@ export class RocketRideClient extends DAPClient { objinfo?: Record; mimetype?: string; }>, - token: string + token: string, + maxConcurrent = 5 ): Promise { const results: UPLOAD_RESULT[] = new Array(files.length); + const concurrency = Math.max(1, Math.floor(maxConcurrent)); /** * Helper function to send upload events through the event system. @@ -1483,16 +1485,20 @@ export class RocketRideClient extends DAPClient { results[index] = finalResult; }; - // Create a promise for every file - let server handle queuing - const uploadPromises = files.map((fileData, index) => - uploadFile(fileData, index).catch((err) => { - // Ensure errors don't kill the whole batch - console.error(`Upload failed for ${fileData.file.name}:`, err); - }) - ); + let nextIndex = 0; + const workers = Array.from({ length: Math.min(concurrency, files.length) }, async () => { + while (nextIndex < files.length) { + const index = nextIndex; + nextIndex += 1; + const fileData = files[index]!; + await uploadFile(fileData, index).catch((err) => { + // Ensure errors don't kill the whole batch + console.error(`Upload failed for ${fileData.file.name}:`, err); + }); + } + }); - // Wait for all uploads to complete - await Promise.all(uploadPromises); + await Promise.all(workers); return results; } diff --git a/packages/client-typescript/tests/RocketRideClient.test.ts b/packages/client-typescript/tests/RocketRideClient.test.ts index c613111b7..b363776a4 100644 --- a/packages/client-typescript/tests/RocketRideClient.test.ts +++ b/packages/client-typescript/tests/RocketRideClient.test.ts @@ -2213,6 +2213,37 @@ describe('RocketRideClient URI normalization', () => { }); }); +describe('RocketRideClient sendFiles concurrency', () => { + it('honors maxConcurrent', async () => { + const client = new RocketRideClient({ auth: 'test-key', uri: 'http://localhost:5565' }); + let active = 0; + let maxActive = 0; + + (client as any).pipe = jest.fn(async () => { + active += 1; + maxActive = Math.max(maxActive, active); + return { + open: jest.fn(async () => undefined), + write: jest.fn(async () => undefined), + close: jest.fn(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + active -= 1; + return undefined; + }), + }; + }); + + const files = Array.from({ length: 5 }, (_, index) => ({ + file: new File([`file-${index}`], `file-${index}.txt`, { type: 'text/plain' }), + })); + + await client.sendFiles(files, 'task-token', 2); + + expect((client as any).pipe).toHaveBeenCalledTimes(5); + expect(maxActive).toBeLessThanOrEqual(2); + }); +}); + export async function isServerAvailable(): Promise { try { const client = new RocketRideClient({ diff --git a/packages/shell-api/versions/v0.d.ts b/packages/shell-api/versions/v0.d.ts index 6ceb1d746..298bf9ca0 100644 --- a/packages/shell-api/versions/v0.d.ts +++ b/packages/shell-api/versions/v0.d.ts @@ -2862,7 +2862,7 @@ export declare class RocketRideClient extends DAPClient { file: File; objinfo?: Record; mimetype?: string; - }>, token: string): Promise; + }>, token: string, maxConcurrent?: number): Promise; /** * Ask a question to RocketRide's AI and get an intelligent response. */