Skip to content

Commit ee5bfbb

Browse files
justin808claude
andcommitted
Address PR review: fix test to exercise actual race condition, improve comments
- Rewrite buildVM recovery test to trigger synchronous throw (via vm.createContext mock) before the first await, which is the actual bug scenario. The old test used a nonexistent file path which fails asynchronously at readFileAsync — already handled by the old code. - Fix test to retry the SAME path after failure, proving the vmCreationPromises map was properly cleaned up. - Fix inaccurate comment example in vm.ts (readFileAsync → vm.createContext) - Replace stale line number reference with semantic reference - Add vmCreationPromises.delete() to removeVM to prevent stale entries Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent fef7fba commit ee5bfbb

2 files changed

Lines changed: 29 additions & 12 deletions

File tree

packages/react-on-rails-pro-node-renderer/src/worker/vm.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,12 +359,13 @@ export async function buildVM(filePath: string) {
359359
// right order.
360360
//
361361
// Technically: an async IIFE runs synchronously until its first `await`. If
362-
// it throws before any `await` (e.g. missing file), a try/finally inside the
363-
// IIFE would run *before* set() on line 349 stores the promise — leaving a
364-
// stale rejected promise in the map that permanently poisons retries.
365-
// Chaining .finally() on the promise *after* set() guarantees cleanup runs
366-
// as a microtask after the current synchronous execution, so set() has
367-
// always run first.
362+
// it throws before any `await` (e.g. `vm.createContext()` or a
363+
// `vm.runInContext()` call throwing synchronously before `readFileAsync`),
364+
// a try/finally inside the IIFE would run *before* `vmCreationPromises.set()`
365+
// (below) has executed — leaving a stale rejected promise in the map that
366+
// permanently poisons retries. Chaining .finally() on the promise *after*
367+
// set() guarantees cleanup runs as a microtask after the current synchronous
368+
// execution, so set() has always run first.
368369
void vmCreationPromise
369370
.catch(() => {})
370371
.finally(() => {
@@ -386,4 +387,5 @@ export function resetVM() {
386387
*/
387388
export function removeVM(bundlePath: string) {
388389
vmContexts.delete(bundlePath);
390+
vmCreationPromises.delete(bundlePath);
389391
}

packages/react-on-rails-pro-node-renderer/tests/vm.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'path';
2+
import vm from 'vm';
23
import {
34
uploadedBundlePath,
45
createUploadedBundle,
@@ -542,18 +543,32 @@ describe('buildVM and runInVM', () => {
542543
expect(vmContext1).toBe(vmContext2);
543544
});
544545

545-
test('buildVM recovers after failure for nonexistent bundle', async () => {
546+
test('buildVM recovers after synchronous throw before first await', async () => {
547+
// Clear any cached VM for serverBundlePath from prior tests
548+
resetVM();
549+
546550
const config = getConfig();
547551
config.supportModules = true;
548552
config.stubTimers = false;
549553

550-
const nonExistentPath = path.resolve(__dirname, './tmp/nonexistent-bundle.js');
554+
// Mock vm.createContext to throw synchronously. This simulates a
555+
// failure BEFORE the first `await` in the buildVM IIFE — the exact
556+
// scenario the .finally() cleanup ordering was designed to handle.
557+
// With the old code (try/finally inside the IIFE), a synchronous
558+
// throw would run cleanup before vmCreationPromises.set(), leaving
559+
// a stale rejected promise that permanently blocks retries.
560+
const createContextSpy = jest.spyOn(vm, 'createContext').mockImplementationOnce(() => {
561+
throw new Error('sync context creation failure');
562+
});
563+
564+
// First call fails synchronously during vm.createContext()
565+
await expect(buildVM(serverBundlePath)).rejects.toThrow('sync context creation failure');
551566

552-
// First call should fail because the bundle file does not exist
553-
await expect(buildVM(nonExistentPath)).rejects.toThrow();
567+
// Restore vm.createContext before retrying
568+
createContextSpy.mockRestore();
554569

555-
// After failure, the vmCreationPromises map should be cleaned up,
556-
// allowing a retry with the correct path to succeed
570+
// Retry the SAME path — if vmCreationPromises wasn't cleaned up,
571+
// this would return the stale rejected promise and fail
557572
await buildVM(serverBundlePath);
558573
expect(hasVMContextForBundle(serverBundlePath)).toBeTruthy();
559574
});

0 commit comments

Comments
 (0)