@@ -34,11 +34,22 @@ 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: { resolve : ( ) => void ; reject : ( reason : Error ) => void } [ ] = [ ] ;
39+
40+ /** Current count of actively executing transformation tasks. */
41+ #activeTasks = 0 ;
42+
43+ /** Maximum number of transformation tasks allowed to execute concurrently. */
44+ #maxConcurrent: number ;
45+
3746 constructor (
3847 options : JavaScriptTransformerOptions ,
3948 readonly maxThreads : number ,
4049 private readonly cache ?: Cache < Uint8Array > ,
4150 ) {
51+ // Maintain 2 active tasks per worker thread to keep transformation pipelines fully saturated
52+ this . #maxConcurrent = Math . max ( 1 , maxThreads * 2 ) ;
4253 // Extract options to ensure only the named options are serialized and sent to the worker
4354 const {
4455 sourcemap,
@@ -55,6 +66,33 @@ export class JavaScriptTransformer {
5566 this . #fileCacheKeyBase = Buffer . from ( JSON . stringify ( this . #commonOptions) , 'utf-8' ) ;
5667 }
5768
69+ /**
70+ * Executes a transformation action using a semaphore-based backpressure throttle.
71+ * Prevents libuv thread pool saturation and excessive V8 heap accumulation.
72+ * @param action A callback that produces a promise for the transformation result.
73+ * @returns A promise resolving to the transformation result.
74+ */
75+ async #runWithThrottle< T > ( action : ( ) => Promise < T > ) : Promise < T > {
76+ if ( this . #activeTasks >= this . #maxConcurrent) {
77+ await new Promise < void > ( ( resolve , reject ) => {
78+ this . #pendingTasks. push ( { resolve, reject } ) ;
79+ } ) ;
80+ } else {
81+ this . #activeTasks++ ;
82+ }
83+
84+ try {
85+ return await action ( ) ;
86+ } finally {
87+ const next = this . #pendingTasks. shift ( ) ;
88+ if ( next ) {
89+ next . resolve ( ) ;
90+ } else {
91+ this . #activeTasks-- ;
92+ }
93+ }
94+ }
95+
5896 #ensureWorkerPool( ) : WorkerPool {
5997 if ( this . #workerPool) {
6098 return this . #workerPool;
@@ -90,56 +128,58 @@ export class JavaScriptTransformer {
90128 sideEffects ?: boolean ,
91129 instrumentForCoverage ?: boolean ,
92130 ) : 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' ) ;
131+ return this . #runWithThrottle( async ( ) => {
132+ const data = await readFile ( filename ) ;
133+
134+ let result ;
135+ let cacheKey ;
136+ if ( this . cache ) {
137+ // Create a cache key from the file data and options that effect the output.
138+ // NOTE: If additional options are added, this may need to be updated.
139+ // TODO: Consider xxhash or similar instead of SHA256
140+ const hash = createHash ( 'sha256' ) ;
141+ hash . update ( `${ ! ! skipLinker } --${ ! ! sideEffects } ` ) ;
142+ hash . update ( data ) ;
143+ hash . update ( this . #fileCacheKeyBase) ;
144+ cacheKey = hash . digest ( 'hex' ) ;
106145
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 ) {
134146 try {
135- await this . cache . put ( cacheKey , result ) ;
147+ result = await this . cache ?. get ( cacheKey ) ;
136148 } catch {
137- // Failure to store the value in the cache should not fail the transform
149+ // Failure to get the value should not fail the transform
138150 }
139151 }
140- }
141152
142- return result ;
153+ if ( result === undefined ) {
154+ // If there is no cache or no cached entry, process the file
155+ result = ( await this . #ensureWorkerPool( ) . run (
156+ {
157+ filename,
158+ data,
159+ skipLinker,
160+ sideEffects,
161+ instrumentForCoverage,
162+ ...this . #commonOptions,
163+ } ,
164+ {
165+ // The below is disable as with Yarn PNP this causes build failures with the below message
166+ // `Unable to deserialize cloned data`.
167+ transferList : process . versions . pnp ? undefined : [ data . buffer ] ,
168+ } ,
169+ ) ) as Uint8Array ;
170+
171+ // If there is a cache then store the result
172+ if ( this . cache && cacheKey ) {
173+ try {
174+ await this . cache . put ( cacheKey , result ) ;
175+ } catch {
176+ // Failure to store the value in the cache should not fail the transform
177+ }
178+ }
179+ }
180+
181+ return result ;
182+ } ) ;
143183 }
144184
145185 /**
@@ -171,21 +211,29 @@ export class JavaScriptTransformer {
171211 ) ;
172212 }
173213
174- return this . #ensureWorkerPool( ) . run ( {
175- filename,
176- data,
177- skipLinker,
178- sideEffects,
179- instrumentForCoverage,
180- ...this . #commonOptions,
181- } ) ;
214+ return this . #runWithThrottle( ( ) =>
215+ this . #ensureWorkerPool( ) . run ( {
216+ filename,
217+ data,
218+ skipLinker,
219+ sideEffects,
220+ instrumentForCoverage,
221+ ...this . #commonOptions,
222+ } ) ,
223+ ) ;
182224 }
183225
184226 /**
185227 * Stops all active transformation tasks and shuts down all workers.
186228 * @returns A void promise that resolves when closing is complete.
187229 */
188230 async close ( ) : Promise < void > {
231+ const pending = this . #pendingTasks;
232+ this . #pendingTasks = [ ] ;
233+ for ( const task of pending ) {
234+ task . reject ( new Error ( 'JavaScriptTransformer closed.' ) ) ;
235+ }
236+
189237 if ( this . #workerPool) {
190238 try {
191239 await this . #workerPool. destroy ( ) ;
0 commit comments