@@ -32,6 +32,9 @@ interface VMContext {
3232// Store contexts by their bundle file paths
3333const vmContexts = new Map < string , VMContext > ( ) ;
3434
35+ // Track VM creation promises to handle concurrent buildVM requests
36+ const vmCreationPromises = new Map < string , Promise < boolean > > ( ) ;
37+
3538/**
3639 * Returns all bundle paths that have a VM context
3740 */
@@ -42,7 +45,7 @@ export function hasVMContextForBundle(bundlePath: string) {
4245/**
4346 * Get a specific VM context by bundle path
4447 */
45- function getVMContext ( bundlePath : string ) : VMContext | undefined {
48+ export function getVMContext ( bundlePath : string ) : VMContext | undefined {
4649 return vmContexts . get ( bundlePath ) ;
4750}
4851
@@ -93,6 +96,11 @@ function manageVMPoolSize() {
9396}
9497
9598export async function buildVM ( filePath : string ) {
99+ // Return existing promise if VM is already being created
100+ if ( vmCreationPromises . has ( filePath ) ) {
101+ return vmCreationPromises . get ( filePath ) ;
102+ }
103+
96104 // Check if VM for this bundle already exists
97105 const vmContext = vmContexts . get ( filePath ) ;
98106 if ( vmContext ) {
@@ -101,142 +109,154 @@ export async function buildVM(filePath: string) {
101109 return Promise . resolve ( true ) ;
102110 }
103111
104- try {
105- const { supportModules, stubTimers, additionalContext } = getConfig ( ) ;
106- const additionalContextIsObject = additionalContext !== null && additionalContext . constructor === Object ;
107- const sharedConsoleHistory = new SharedConsoleHistory ( ) ;
108- const contextObject = { sharedConsoleHistory } ;
109-
110- if ( supportModules ) {
111- // IMPORTANT: When adding anything to this object, update:
112- // 1. docs/node-renderer/js-configuration.md
113- // 2. packages/node-renderer/src/shared/configBuilder.ts
114- extendContext ( contextObject , {
115- Buffer,
116- TextDecoder,
117- TextEncoder,
118- URLSearchParams,
119- ReadableStream,
120- process,
121- setTimeout,
122- setInterval,
123- setImmediate,
124- clearTimeout,
125- clearInterval,
126- clearImmediate,
127- queueMicrotask,
128- } ) ;
129- }
112+ // Create a new promise for this VM creation
113+ const vmCreationPromise = ( async ( ) => {
114+ try {
115+ const { supportModules, stubTimers, additionalContext } = getConfig ( ) ;
116+ const additionalContextIsObject =
117+ additionalContext !== null && additionalContext . constructor === Object ;
118+ const sharedConsoleHistory = new SharedConsoleHistory ( ) ;
119+ const contextObject = { sharedConsoleHistory } ;
120+
121+ if ( supportModules ) {
122+ // IMPORTANT: When adding anything to this object, update:
123+ // 1. docs/node-renderer/js-configuration.md
124+ // 2. packages/node-renderer/src/shared/configBuilder.ts
125+ extendContext ( contextObject , {
126+ Buffer,
127+ TextDecoder,
128+ TextEncoder,
129+ URLSearchParams,
130+ ReadableStream,
131+ process,
132+ setTimeout,
133+ setInterval,
134+ setImmediate,
135+ clearTimeout,
136+ clearInterval,
137+ clearImmediate,
138+ queueMicrotask,
139+ } ) ;
140+ }
130141
131- if ( additionalContextIsObject ) {
132- extendContext ( contextObject , additionalContext ) ;
133- }
134- const context = vm . createContext ( contextObject ) ;
135-
136- // Store the new context with timestamp
137- vmContexts . set ( filePath , {
138- context,
139- sharedConsoleHistory,
140- lastUsed : Date . now ( ) ,
141- } ) ;
142-
143- // Manage pool size after adding new VM
144- manageVMPoolSize ( ) ;
145-
146- // Create explicit reference to global context, just in case (some libs can use it):
147- vm . runInContext ( 'global = this' , context ) ;
148-
149- // Reimplement console methods for replaying on the client:
150- vm . runInContext (
151- `
152- console = {
153- get history() {
154- return sharedConsoleHistory.getConsoleHistory();
155- },
156- set history(value) {
157- // Do nothing. It's just for the backward compatibility.
158- },
159- };
160- ['error', 'log', 'info', 'warn'].forEach(function (level) {
161- console[level] = function () {
162- var argArray = Array.prototype.slice.call(arguments);
163- if (argArray.length > 0) {
164- argArray[0] = '[SERVER] ' + argArray[0];
165- }
166- sharedConsoleHistory.addToConsoleHistory({level: level, arguments: argArray});
142+ if ( additionalContextIsObject ) {
143+ extendContext ( contextObject , additionalContext ) ;
144+ }
145+ const context = vm . createContext ( contextObject ) ;
146+
147+ // Create explicit reference to global context, just in case (some libs can use it):
148+ vm . runInContext ( 'global = this' , context ) ;
149+
150+ // Reimplement console methods for replaying on the client:
151+ vm . runInContext (
152+ `
153+ console = {
154+ get history() {
155+ return sharedConsoleHistory.getConsoleHistory();
156+ },
157+ set history(value) {
158+ // Do nothing. It's just for the backward compatibility.
159+ },
167160 };
168- });` ,
169- context ,
170- ) ;
161+ ['error', 'log', 'info', 'warn'].forEach(function (level) {
162+ console[level] = function () {
163+ var argArray = Array.prototype.slice.call(arguments);
164+ if (argArray.length > 0) {
165+ argArray[0] = '[SERVER] ' + argArray[0];
166+ }
167+ sharedConsoleHistory.addToConsoleHistory({level: level, arguments: argArray});
168+ };
169+ });` ,
170+ context ,
171+ ) ;
172+
173+ // Define global getStackTrace() function:
174+ vm . runInContext (
175+ `
176+ function getStackTrace() {
177+ var stack;
178+ try {
179+ throw new Error('');
180+ }
181+ catch (error) {
182+ stack = error.stack || '';
183+ }
184+ stack = stack.split('\\n').map(function (line) { return line.trim(); });
185+ return stack.splice(stack[0] == 'Error' ? 2 : 1);
186+ }` ,
187+ context ,
188+ ) ;
171189
172- // Define global getStackTrace() function:
173- vm . runInContext (
174- `
175- function getStackTrace() {
176- var stack;
177- try {
178- throw new Error('');
190+ if ( stubTimers ) {
191+ // Define timer polyfills:
192+ vm . runInContext ( `function setInterval() {}` , context ) ;
193+ vm . runInContext ( `function setTimeout() {}` , context ) ;
194+ vm . runInContext ( `function setImmediate() {}` , context ) ;
195+ vm . runInContext ( `function clearTimeout() {}` , context ) ;
196+ vm . runInContext ( `function clearInterval() {}` , context ) ;
197+ vm . runInContext ( `function clearImmediate() {}` , context ) ;
198+ vm . runInContext ( `function queueMicrotask() {}` , context ) ;
179199 }
180- catch (error) {
181- stack = error.stack || '';
200+
201+ // Run bundle code in created context:
202+ const bundleContents = await readFileAsync ( filePath , 'utf8' ) ;
203+
204+ // If node-specific code is provided then it must be wrapped into a module wrapper. The bundle
205+ // may need the `require` function, which is not available when running in vm unless passed in.
206+ if ( additionalContextIsObject || supportModules ) {
207+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
208+ vm . runInContext ( m . wrap ( bundleContents ) , context ) (
209+ exports ,
210+ require ,
211+ module ,
212+ filePath ,
213+ path . dirname ( filePath ) ,
214+ ) ;
215+ } else {
216+ vm . runInContext ( bundleContents , context ) ;
182217 }
183- stack = stack.split('\\n').map(function (line) { return line.trim(); });
184- return stack.splice(stack[0] == 'Error' ? 2 : 1);
185- }` ,
186- context ,
187- ) ;
188218
189- if ( stubTimers ) {
190- // Define timer polyfills:
191- vm . runInContext ( `function setInterval() {}` , context ) ;
192- vm . runInContext ( `function setTimeout() {}` , context ) ;
193- vm . runInContext ( `function setImmediate() {}` , context ) ;
194- vm . runInContext ( `function clearTimeout() {}` , context ) ;
195- vm . runInContext ( `function clearInterval() {}` , context ) ;
196- vm . runInContext ( `function clearImmediate() {}` , context ) ;
197- vm . runInContext ( `function queueMicrotask() {}` , context ) ;
198- }
219+ // Only now, after VM is fully initialized, store the context
220+ vmContexts . set ( filePath , {
221+ context,
222+ sharedConsoleHistory,
223+ lastUsed : Date . now ( ) ,
224+ } ) ;
199225
200- // Run bundle code in created context:
201- const bundleContents = await readFileAsync ( filePath , 'utf8' ) ;
202-
203- // If node-specific code is provided then it must be wrapped into a module wrapper. The bundle
204- // may need the `require` function, which is not available when running in vm unless passed in.
205- if ( additionalContextIsObject || supportModules ) {
206- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
207- vm . runInContext ( m . wrap ( bundleContents ) , context ) (
208- exports ,
209- require ,
210- module ,
211- filePath ,
212- path . dirname ( filePath ) ,
213- ) ;
214- } else {
215- vm . runInContext ( bundleContents , context ) ;
216- }
226+ // Manage pool size after adding new VM
227+ manageVMPoolSize ( ) ;
217228
218- // isWorker check is required for JS unit testing:
219- if ( cluster . isWorker && cluster . worker !== undefined ) {
220- log . debug ( `Built VM for worker #${ cluster . worker . id } with bundle ${ filePath } ` ) ;
221- }
229+ // isWorker check is required for JS unit testing:
230+ if ( cluster . isWorker && cluster . worker !== undefined ) {
231+ log . debug ( `Built VM for worker #${ cluster . worker . id } with bundle ${ filePath } ` ) ;
232+ }
222233
223- if ( log . level === 'debug' ) {
224- log . debug (
225- 'Required objects now in VM sandbox context: %s' ,
226- vm . runInContext ( 'global.ReactOnRails' , context ) !== undefined ,
227- ) ;
228- log . debug (
229- 'Required objects should not leak to the global context (true means OK): %s' ,
230- ! ! global . ReactOnRails ,
231- ) ;
234+ if ( log . level === 'debug' ) {
235+ log . debug (
236+ 'Required objects now in VM sandbox context: %s' ,
237+ vm . runInContext ( 'global.ReactOnRails' , context ) !== undefined ,
238+ ) ;
239+ log . debug (
240+ 'Required objects should not leak to the global context (true means OK): %s' ,
241+ ! ! global . ReactOnRails ,
242+ ) ;
243+ }
244+
245+ return true ;
246+ } catch ( error ) {
247+ log . error ( 'Caught Error when creating context in buildVM, %O' , error ) ;
248+ errorReporter . error ( error as Error ) ;
249+ throw error ;
250+ } finally {
251+ // Always remove the promise from the map when done
252+ vmCreationPromises . delete ( filePath ) ;
232253 }
254+ } ) ( ) ;
233255
234- return Promise . resolve ( true ) ;
235- } catch ( error ) {
236- log . error ( 'Caught Error when creating context in buildVM, %O' , error ) ;
237- errorReporter . error ( error as Error ) ;
238- return Promise . reject ( error as Error ) ;
239- }
256+ // Store the promise
257+ vmCreationPromises . set ( filePath , vmCreationPromise ) ;
258+
259+ return vmCreationPromise ;
240260}
241261
242262/**
@@ -253,6 +273,11 @@ export async function runInVM(
253273 const { bundlePath } = getConfig ( ) ;
254274
255275 try {
276+ // Wait for VM creation if it's in progress
277+ if ( vmCreationPromises . has ( filePath ) ) {
278+ await vmCreationPromises . get ( filePath ) ;
279+ }
280+
256281 // Get the correct VM context based on the provided bundle path
257282 const vmContext = getVMContext ( filePath ) ;
258283
0 commit comments