Skip to content

Commit 6489963

Browse files
committed
fix(flip): replace requestAnimationFrame with setTimeout in createFlipBurnHandler
RAF fires at ~16ms frame intervals in happy-dom, causing a race condition with the test's 10ms await when the flip-burn calculation is fast (e.g. 10 ly input). Heavy calculations accidentally masked the bug by blocking the JS thread. Collapsing RAF + setTimeout into a single setTimeout(0) provides identical event-loop yielding behaviour with deterministic timing in tests.
1 parent b1b1d9b commit 6489963

File tree

1 file changed

+67
-75
lines changed

1 file changed

+67
-75
lines changed

Javascript/src/ui/eventHandlers.ts

Lines changed: 67 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ export function createFlipBurnHandler(
358358
getResults: () => (HTMLElement | null)[],
359359
chartRegistry: { current: ChartRegistry }
360360
): () => void {
361-
let pendingRAF: number | null = null;
362361
let pendingCalculation: number | null = null;
363362

364363
return () => {
@@ -381,10 +380,6 @@ export function createFlipBurnHandler(
381380
if (!accelInput || !distanceInput || !dryMassInput || !efficiencyInput) return;
382381

383382
// Cancel pending calculation to prevent race condition
384-
if (pendingRAF !== null) {
385-
cancelAnimationFrame(pendingRAF);
386-
pendingRAF = null;
387-
}
388383
if (pendingCalculation !== null) {
389384
clearTimeout(pendingCalculation);
390385
pendingCalculation = null;
@@ -463,77 +458,74 @@ export function createFlipBurnHandler(
463458
if (resultFlipStars) resultFlipStars.textContent = "";
464459
if (resultFlipGalaxyFraction) resultFlipGalaxyFraction.textContent = "";
465460

466-
// Allow UI to update before heavy calculation
467-
pendingRAF = requestAnimationFrame(() => {
468-
pendingRAF = null;
469-
pendingCalculation = window.setTimeout(() => {
470-
// Use validated values from above
471-
const accel = rl.g.mul(accelGStr);
472-
const m = rl.ensure(distanceLightYearsStr).mul(rl.lightYear);
473-
const res = rl.flipAndBurn(accel, m);
474-
const peak = res.peakVelocity.div(rl.c);
475-
const lorentz = res.lorentzFactor;
476-
const metre = rl.formatSignificant(rl.one.div(lorentz), "0", 2);
477-
const sec = rl.formatSignificant(rl.one.mul(lorentz), "0", 2);
478-
479-
// Calculate fuel mass
480-
const dryMass = rl.ensure(dryMassStr);
481-
const efficiency = rl.ensure(efficiencyStr);
482-
const fuelFraction = rl.pionRocketFuelFraction(res.properTime, accel, efficiency);
483-
const fuelMass = fuelFraction.mul(dryMass).div(rl.one.minus(fuelFraction));
484-
const fuelPercent = fuelFraction.mul(100);
485-
486-
if (resultFlip1) {
487-
const f = rl.formatDurationAutoUnit(res.properTime);
488-
setElement(resultFlip1, f.value, f.units);
489-
}
490-
if (resultFlip2) setElement(resultFlip2, rl.formatSignificant(peak, "9", 2), "c");
491-
if (resultFlip4) {
492-
const coordFormatted = rl.formatDurationAutoUnit(res.coordTime);
493-
const diffFormatted = rl.formatDurationAutoUnit(res.coordTime.minus(res.properTime));
494-
setElement(
495-
resultFlip4,
496-
`${coordFormatted.value} ${coordFormatted.units} (+${diffFormatted.value} ${diffFormatted.units})`,
497-
""
498-
);
499-
}
500-
if (resultFlip3) setElement(resultFlip3, rl.formatSignificant(lorentz, "0", 2), "");
501-
if (resultFlip5) setElement(resultFlip5, `1m becomes ${metre}m`, "");
502-
if (resultFlip6) setElement(resultFlip6, `1s becomes ${sec}s`, "");
503-
if (resultFlipFuel) setElement(resultFlipFuel, rl.formatMassWithUnit(fuelMass), "");
504-
if (resultFlipFuelFraction)
505-
setElement(resultFlipFuelFraction, rl.formatSignificant(fuelPercent, "9", 2), "%");
506-
507-
// Update charts - parseFloat is OK here as Chart.js only needs limited precision for display
508-
const accelG = parseFloat(accelGStr);
509-
const distanceLightYears = parseFloat(distanceLightYearsStr);
461+
// Yield to the event loop before heavy calculation so "Working..." renders
462+
pendingCalculation = window.setTimeout(() => {
463+
// Use validated values from above
464+
const accel = rl.g.mul(accelGStr);
465+
const m = rl.ensure(distanceLightYearsStr).mul(rl.lightYear);
466+
const res = rl.flipAndBurn(accel, m);
467+
const peak = res.peakVelocity.div(rl.c);
468+
const lorentz = res.lorentzFactor;
469+
const metre = rl.formatSignificant(rl.one.div(lorentz), "0", 2);
470+
const sec = rl.formatSignificant(rl.one.mul(lorentz), "0", 2);
471+
472+
// Calculate fuel mass
473+
const dryMass = rl.ensure(dryMassStr);
474+
const efficiency = rl.ensure(efficiencyStr);
475+
const fuelFraction = rl.pionRocketFuelFraction(res.properTime, accel, efficiency);
476+
const fuelMass = fuelFraction.mul(dryMass).div(rl.one.minus(fuelFraction));
477+
const fuelPercent = fuelFraction.mul(100);
478+
479+
if (resultFlip1) {
480+
const f = rl.formatDurationAutoUnit(res.properTime);
481+
setElement(resultFlip1, f.value, f.units);
482+
}
483+
if (resultFlip2) setElement(resultFlip2, rl.formatSignificant(peak, "9", 2), "c");
484+
if (resultFlip4) {
485+
const coordFormatted = rl.formatDurationAutoUnit(res.coordTime);
486+
const diffFormatted = rl.formatDurationAutoUnit(res.coordTime.minus(res.properTime));
487+
setElement(
488+
resultFlip4,
489+
`${coordFormatted.value} ${coordFormatted.units} (+${diffFormatted.value} ${diffFormatted.units})`,
490+
""
491+
);
492+
}
493+
if (resultFlip3) setElement(resultFlip3, rl.formatSignificant(lorentz, "0", 2), "");
494+
if (resultFlip5) setElement(resultFlip5, `1m becomes ${metre}m`, "");
495+
if (resultFlip6) setElement(resultFlip6, `1s becomes ${sec}s`, "");
496+
if (resultFlipFuel) setElement(resultFlipFuel, rl.formatMassWithUnit(fuelMass), "");
497+
if (resultFlipFuelFraction)
498+
setElement(resultFlipFuelFraction, rl.formatSignificant(fuelPercent, "9", 2), "%");
510499

511-
// Estimate stars in range
512-
if (distanceLightYears >= 100000) {
513-
// At or above 100k ly, show "Entire galaxy"
514-
if (resultFlipStars) setElement(resultFlipStars, "Entire galaxy", "");
515-
if (resultFlipGalaxyFraction) setElement(resultFlipGalaxyFraction, "100", "%");
516-
} else {
517-
const starEstimate = extra.estimateStarsInSphere(distanceLightYears);
518-
const starsFormatted = extra.formatStarCount(starEstimate.stars);
519-
const fractionPercent = rl.formatSignificant(
520-
new Decimal(starEstimate.fraction * 100),
521-
"0",
522-
1
523-
);
524-
if (resultFlipStars) setElement(resultFlipStars, starsFormatted, "");
525-
if (resultFlipGalaxyFraction) setElement(resultFlipGalaxyFraction, fractionPercent, "%");
526-
}
527-
const efficiencyNum = parseFloat(efficiencyStr);
528-
const data = generateFlipBurnChartData(accelG, distanceLightYears, efficiencyNum);
529-
chartRegistry.current = updateFlipBurnCharts(chartRegistry.current, data, efficiencyNum, {
530-
velocity: chartTimeModes.flipVelocity,
531-
lorentz: chartTimeModes.flipLorentz,
532-
rapidity: chartTimeModes.flipRapidity,
533-
});
534-
pendingCalculation = null;
535-
}, 0);
536-
});
500+
// Update charts - parseFloat is OK here as Chart.js only needs limited precision for display
501+
const accelG = parseFloat(accelGStr);
502+
const distanceLightYears = parseFloat(distanceLightYearsStr);
503+
504+
// Estimate stars in range
505+
if (distanceLightYears >= 100000) {
506+
// At or above 100k ly, show "Entire galaxy"
507+
if (resultFlipStars) setElement(resultFlipStars, "Entire galaxy", "");
508+
if (resultFlipGalaxyFraction) setElement(resultFlipGalaxyFraction, "100", "%");
509+
} else {
510+
const starEstimate = extra.estimateStarsInSphere(distanceLightYears);
511+
const starsFormatted = extra.formatStarCount(starEstimate.stars);
512+
const fractionPercent = rl.formatSignificant(
513+
new Decimal(starEstimate.fraction * 100),
514+
"0",
515+
1
516+
);
517+
if (resultFlipStars) setElement(resultFlipStars, starsFormatted, "");
518+
if (resultFlipGalaxyFraction) setElement(resultFlipGalaxyFraction, fractionPercent, "%");
519+
}
520+
const efficiencyNum = parseFloat(efficiencyStr);
521+
const data = generateFlipBurnChartData(accelG, distanceLightYears, efficiencyNum);
522+
chartRegistry.current = updateFlipBurnCharts(chartRegistry.current, data, efficiencyNum, {
523+
velocity: chartTimeModes.flipVelocity,
524+
lorentz: chartTimeModes.flipLorentz,
525+
rapidity: chartTimeModes.flipRapidity,
526+
});
527+
pendingCalculation = null;
528+
}, 0);
537529
};
538530
}
539531

0 commit comments

Comments
 (0)