@@ -34,11 +34,20 @@ export class JavaScriptTransformer {
3434 #commonOptions: Required < JavaScriptTransformerOptions > ;
3535 #fileCacheKeyBase: Uint8Array ;
3636
37+ /** Queue of pending transformation tasks waiting for an active concurrency slot. */
38+ #pendingTasks: ( ( ) => void ) [ ] = [ ] ;
39+ /** Current count of actively executing transformation tasks. */
40+ #activeTasks = 0 ;
41+ /** Maximum number of transformation tasks allowed to execute concurrently. */
42+ #maxConcurrent: number ;
43+
3744 constructor (
3845 options : JavaScriptTransformerOptions ,
3946 readonly maxThreads : number ,
4047 private readonly cache ?: Cache < Uint8Array > ,
4148 ) {
49+ // Maintain 2 active tasks per worker thread to keep transformation pipelines fully saturated
50+ this . #maxConcurrent = maxThreads * 2 ;
4251 // Extract options to ensure only the named options are serialized and sent to the worker
4352 const {
4453 sourcemap,
@@ -55,6 +64,33 @@ export class JavaScriptTransformer {
5564 this . #fileCacheKeyBase = Buffer . from ( JSON . stringify ( this . #commonOptions) , 'utf-8' ) ;
5665 }
5766
67+ /**
68+ * Executes a transformation action using a semaphore-based backpressure throttle.
69+ * Prevents libuv thread pool saturation and excessive V8 heap accumulation.
70+ * @param action A callback that produces a promise for the transformation result.
71+ * @returns A promise resolving to the transformation result.
72+ */
73+ async #runWithThrottle< T > ( action : ( ) => Promise < T > ) : Promise < T > {
74+ if ( this . #activeTasks >= this . #maxConcurrent) {
75+ await new Promise < void > ( ( resolve ) => {
76+ this . #pendingTasks. push ( resolve ) ;
77+ } ) ;
78+ } else {
79+ this . #activeTasks++ ;
80+ }
81+
82+ try {
83+ return await action ( ) ;
84+ } finally {
85+ const next = this . #pendingTasks. shift ( ) ;
86+ if ( next ) {
87+ next ( ) ;
88+ } else {
89+ this . #activeTasks-- ;
90+ }
91+ }
92+ }
93+
5894 #ensureWorkerPool( ) : WorkerPool {
5995 if ( this . #workerPool) {
6096 return this . #workerPool;
@@ -90,56 +126,58 @@ export class JavaScriptTransformer {
90126 sideEffects ?: boolean ,
91127 instrumentForCoverage ?: boolean ,
92128 ) : Promise < Uint8Array > {
93- const data = await readFile ( filename ) ;
94-
95- let result ;
96- let cacheKey ;
97- if ( this . cache ) {
98- // Create a cache key from the file data and options that effect the output.
99- // NOTE: If additional options are added, this may need to be updated.
100- // TODO: Consider xxhash or similar instead of SHA256
101- const hash = createHash ( 'sha256' ) ;
102- hash . update ( `${ ! ! skipLinker } --${ ! ! sideEffects } ` ) ;
103- hash . update ( data ) ;
104- hash . update ( this . #fileCacheKeyBase) ;
105- cacheKey = hash . digest ( 'hex' ) ;
129+ return this . #runWithThrottle( async ( ) => {
130+ const data = await readFile ( filename ) ;
131+
132+ let result ;
133+ let cacheKey ;
134+ if ( this . cache ) {
135+ // Create a cache key from the file data and options that effect the output.
136+ // NOTE: If additional options are added, this may need to be updated.
137+ // TODO: Consider xxhash or similar instead of SHA256
138+ const hash = createHash ( 'sha256' ) ;
139+ hash . update ( `${ ! ! skipLinker } --${ ! ! sideEffects } ` ) ;
140+ hash . update ( data ) ;
141+ hash . update ( this . #fileCacheKeyBase) ;
142+ cacheKey = hash . digest ( 'hex' ) ;
106143
107- try {
108- result = await this . cache ?. get ( cacheKey ) ;
109- } catch {
110- // Failure to get the value should not fail the transform
111- }
112- }
113-
114- if ( result === undefined ) {
115- // If there is no cache or no cached entry, process the file
116- result = ( await this . #ensureWorkerPool( ) . run (
117- {
118- filename,
119- data,
120- skipLinker,
121- sideEffects,
122- instrumentForCoverage,
123- ...this . #commonOptions,
124- } ,
125- {
126- // The below is disable as with Yarn PNP this causes build failures with the below message
127- // `Unable to deserialize cloned data`.
128- transferList : process . versions . pnp ? undefined : [ data . buffer ] ,
129- } ,
130- ) ) as Uint8Array ;
131-
132- // If there is a cache then store the result
133- if ( this . cache && cacheKey ) {
134144 try {
135- await this . cache . put ( cacheKey , result ) ;
145+ result = await this . cache ?. get ( cacheKey ) ;
136146 } catch {
137- // Failure to store the value in the cache should not fail the transform
147+ // Failure to get the value should not fail the transform
138148 }
139149 }
140- }
141150
142- return result ;
151+ if ( result === undefined ) {
152+ // If there is no cache or no cached entry, process the file
153+ result = ( await this . #ensureWorkerPool( ) . run (
154+ {
155+ filename,
156+ data,
157+ skipLinker,
158+ sideEffects,
159+ instrumentForCoverage,
160+ ...this . #commonOptions,
161+ } ,
162+ {
163+ // The below is disable as with Yarn PNP this causes build failures with the below message
164+ // `Unable to deserialize cloned data`.
165+ transferList : process . versions . pnp ? undefined : [ data . buffer ] ,
166+ } ,
167+ ) ) as Uint8Array ;
168+
169+ // If there is a cache then store the result
170+ if ( this . cache && cacheKey ) {
171+ try {
172+ await this . cache . put ( cacheKey , result ) ;
173+ } catch {
174+ // Failure to store the value in the cache should not fail the transform
175+ }
176+ }
177+ }
178+
179+ return result ;
180+ } ) ;
143181 }
144182
145183 /**
@@ -171,14 +209,16 @@ export class JavaScriptTransformer {
171209 ) ;
172210 }
173211
174- return this . #ensureWorkerPool( ) . run ( {
175- filename,
176- data,
177- skipLinker,
178- sideEffects,
179- instrumentForCoverage,
180- ...this . #commonOptions,
181- } ) ;
212+ return this . #runWithThrottle( ( ) =>
213+ this . #ensureWorkerPool( ) . run ( {
214+ filename,
215+ data,
216+ skipLinker,
217+ sideEffects,
218+ instrumentForCoverage,
219+ ...this . #commonOptions,
220+ } ) ,
221+ ) ;
182222 }
183223
184224 /**
0 commit comments