@@ -29,7 +29,7 @@ import * as errorReporter from '../shared/errorReporter';
2929const readFileAsync = promisify ( fs . readFile ) ;
3030const writeFileAsync = promisify ( fs . writeFile ) ;
3131
32- interface VMContext {
32+ export interface VMContext {
3333 context : Context ;
3434 sharedConsoleHistory : SharedConsoleHistory ;
3535 lastUsed : number ; // Track when this VM was last used
@@ -101,84 +101,14 @@ function manageVMPoolSize() {
101101 }
102102}
103103
104- /**
105- *
106- * @param renderingRequest JS Code to execute for SSR
107- * @param filePath
108- * @param vmCluster
109- */
110- export async function runInVM (
111- renderingRequest : string ,
112- filePath : string ,
113- vmCluster ?: typeof cluster ,
114- ) : Promise < RenderResult > {
115- const { serverBundleCachePath } = getConfig ( ) ;
116-
117- try {
118- // Wait for VM creation if it's in progress
119- if ( vmCreationPromises . has ( filePath ) ) {
120- await vmCreationPromises . get ( filePath ) ;
121- }
122-
123- // Get the correct VM context based on the provided bundle path
124- const vmContext = getVMContext ( filePath ) ;
125-
126- if ( ! vmContext ) {
127- throw new Error ( `No VM context found for bundle ${ filePath } ` ) ;
128- }
129-
130- // Update last used timestamp
131- vmContext . lastUsed = Date . now ( ) ;
132-
133- const { context, sharedConsoleHistory } = vmContext ;
134-
135- if ( log . level === 'debug' ) {
136- // worker is nullable in the primary process
137- const workerId = vmCluster ?. worker ?. id ;
138- log . debug ( `worker ${ workerId ? `${ workerId } ` : '' } received render request for bundle ${ filePath } with code
139- ${ smartTrim ( renderingRequest ) } `) ;
140- const debugOutputPathCode = path . join ( serverBundleCachePath , 'code.js' ) ;
141- log . debug ( `Full code executed written to: ${ debugOutputPathCode } ` ) ;
142- await writeFileAsync ( debugOutputPathCode , renderingRequest ) ;
143- }
144-
145- let result = sharedConsoleHistory . trackConsoleHistoryInRenderRequest ( ( ) => {
146- context . renderingRequest = renderingRequest ;
147- try {
148- return vm . runInContext ( renderingRequest , context ) as RenderCodeResult ;
149- } finally {
150- context . renderingRequest = undefined ;
151- }
152- } ) ;
153-
154- if ( isReadableStream ( result ) ) {
155- const newStreamAfterHandlingError = handleStreamError ( result , ( error ) => {
156- const msg = formatExceptionMessage ( renderingRequest , error , 'Error in a rendering stream' ) ;
157- errorReporter . message ( msg ) ;
158- } ) ;
159- return newStreamAfterHandlingError ;
160- }
161- if ( typeof result !== 'string' ) {
162- const objectResult = await result ;
163- result = JSON . stringify ( objectResult ) ;
164- }
165- if ( log . level === 'debug' ) {
166- log . debug ( `result from JS:
167- ${ smartTrim ( result ) } `) ;
168- const debugOutputPathResult = path . join ( serverBundleCachePath , 'result.json' ) ;
169- log . debug ( `Wrote result to file: ${ debugOutputPathResult } ` ) ;
170- await writeFileAsync ( debugOutputPathResult , result ) ;
171- }
172-
173- return result ;
174- } catch ( exception ) {
175- const exceptionMessage = formatExceptionMessage ( renderingRequest , exception ) ;
176- log . debug ( 'Caught exception in rendering request' , exceptionMessage ) ;
177- return Promise . resolve ( { exceptionMessage } ) ;
104+ export class VMContextNotFoundError extends Error {
105+ constructor ( bundleFilePath : string ) {
106+ super ( `VMContext not found for bundle: ${ bundleFilePath } ` ) ;
107+ this . name = 'VMContextNotFoundError' ;
178108 }
179109}
180110
181- export async function buildVM ( filePath : string ) : Promise < VMContext > {
111+ async function buildVM ( filePath : string ) : Promise < VMContext > {
182112 // Return existing promise if VM is already being created
183113 if ( vmCreationPromises . has ( filePath ) ) {
184114 return vmCreationPromises . get ( filePath ) as Promise < VMContext > ;
@@ -200,12 +130,7 @@ export async function buildVM(filePath: string): Promise<VMContext> {
200130 additionalContext !== null && additionalContext . constructor === Object ;
201131 const sharedConsoleHistory = new SharedConsoleHistory ( ) ;
202132
203- const runOnOtherBundle = async ( bundleTimestamp : string | number , renderingRequest : string ) => {
204- const bundlePath = getRequestBundleFilePath ( bundleTimestamp ) ;
205- return runInVM ( renderingRequest , bundlePath , cluster ) ;
206- } ;
207-
208- const contextObject = { sharedConsoleHistory, runOnOtherBundle } ;
133+ const contextObject = { sharedConsoleHistory } ;
209134
210135 if ( supportModules ) {
211136 // IMPORTANT: When adding anything to this object, update:
@@ -349,6 +274,120 @@ export async function buildVM(filePath: string): Promise<VMContext> {
349274 return vmCreationPromise ;
350275}
351276
277+ async function getOrBuildVMContext ( bundleFilePath : string , buildVmsIfNeeded : boolean ) : Promise < VMContext > {
278+ const vmContext = getVMContext ( bundleFilePath ) ;
279+ if ( vmContext ) {
280+ return vmContext ;
281+ }
282+
283+ const vmCreationPromise = vmCreationPromises . get ( bundleFilePath ) ;
284+ if ( vmCreationPromise ) {
285+ return vmCreationPromise ;
286+ }
287+
288+ if ( buildVmsIfNeeded ) {
289+ return buildVM ( bundleFilePath ) ;
290+ }
291+
292+ throw new VMContextNotFoundError ( bundleFilePath ) ;
293+ }
294+
295+ export type ExecutionContext = {
296+ runInVM : (
297+ renderingRequest : string ,
298+ bundleFilePath : string ,
299+ vmCluster ?: typeof cluster ,
300+ ) => Promise < RenderResult > ;
301+ getVMContext : ( bundleFilePath : string ) => VMContext | undefined ;
302+ } ;
303+
304+ export async function buildExecutionContext (
305+ bundlePaths : string [ ] ,
306+ buildVmsIfNeeded : boolean ,
307+ ) : Promise < ExecutionContext > {
308+ const mapBundleFilePathToVMContext = new Map < string , VMContext > ( ) ;
309+ await Promise . all (
310+ bundlePaths . map ( async ( bundleFilePath ) => {
311+ const vmContext = await getOrBuildVMContext ( bundleFilePath , buildVmsIfNeeded ) ;
312+ vmContext . lastUsed = Date . now ( ) ;
313+ mapBundleFilePathToVMContext . set ( bundleFilePath , vmContext ) ;
314+ } ) ,
315+ ) ;
316+ const sharedExecutionContext = new Map ( ) ;
317+
318+ const runInVM = async ( renderingRequest : string , bundleFilePath : string , vmCluster ?: typeof cluster ) => {
319+ try {
320+ const { serverBundleCachePath } = getConfig ( ) ;
321+ const vmContext = mapBundleFilePathToVMContext . get ( bundleFilePath ) ;
322+ if ( ! vmContext ) {
323+ throw new VMContextNotFoundError ( bundleFilePath ) ;
324+ }
325+
326+ // Update last used timestamp
327+ vmContext . lastUsed = Date . now ( ) ;
328+
329+ const { context, sharedConsoleHistory } = vmContext ;
330+
331+ if ( log . level === 'debug' ) {
332+ // worker is nullable in the primary process
333+ const workerId = vmCluster ?. worker ?. id ;
334+ log . debug ( `worker ${ workerId ? `${ workerId } ` : '' } received render request for bundle ${ bundleFilePath } with code
335+ ${ smartTrim ( renderingRequest ) } ` ) ;
336+ const debugOutputPathCode = path . join ( serverBundleCachePath , 'code.js' ) ;
337+ log . debug ( `Full code executed written to: ${ debugOutputPathCode } ` ) ;
338+ await writeFileAsync ( debugOutputPathCode , renderingRequest ) ;
339+ }
340+
341+ let result = sharedConsoleHistory . trackConsoleHistoryInRenderRequest ( ( ) => {
342+ context . renderingRequest = renderingRequest ;
343+ context . sharedExecutionContext = sharedExecutionContext ;
344+ context . runOnOtherBundle = ( bundleTimestamp : string | number , newRenderingRequest : string ) => {
345+ const otherBundleFilePath = getRequestBundleFilePath ( bundleTimestamp ) ;
346+ return runInVM ( otherBundleFilePath , newRenderingRequest , vmCluster ) ;
347+ } ;
348+
349+ try {
350+ return vm . runInContext ( renderingRequest , context ) as RenderCodeResult ;
351+ } finally {
352+ context . renderingRequest = undefined ;
353+ context . sharedExecutionContext = undefined ;
354+ context . runOnOtherBundle = undefined ;
355+ }
356+ } ) ;
357+
358+ if ( isReadableStream ( result ) ) {
359+ const newStreamAfterHandlingError = handleStreamError ( result , ( error ) => {
360+ const msg = formatExceptionMessage ( renderingRequest , error , 'Error in a rendering stream' ) ;
361+ errorReporter . message ( msg ) ;
362+ } ) ;
363+ return newStreamAfterHandlingError ;
364+ }
365+ if ( typeof result !== 'string' ) {
366+ const objectResult = await result ;
367+ result = JSON . stringify ( objectResult ) ;
368+ }
369+ if ( log . level === 'debug' ) {
370+ log . debug ( `result from JS:
371+ ${ smartTrim ( result ) } ` ) ;
372+ const debugOutputPathResult = path . join ( serverBundleCachePath , 'result.json' ) ;
373+ log . debug ( `Wrote result to file: ${ debugOutputPathResult } ` ) ;
374+ await writeFileAsync ( debugOutputPathResult , result ) ;
375+ }
376+
377+ return result ;
378+ } catch ( exception ) {
379+ const exceptionMessage = formatExceptionMessage ( renderingRequest , exception ) ;
380+ log . debug ( 'Caught exception in rendering request' , exceptionMessage ) ;
381+ return Promise . resolve ( { exceptionMessage } ) ;
382+ }
383+ } ;
384+
385+ return {
386+ getVMContext : ( bundleFilePath : string ) => mapBundleFilePathToVMContext . get ( bundleFilePath ) ,
387+ runInVM,
388+ } ;
389+ }
390+
352391export function resetVM ( ) {
353392 // Clear all VM contexts
354393 vmContexts . clear ( ) ;
0 commit comments