fix(core): a GSAP call added after the first seek fires once, not twice (#4948)

The seek path skips its silent re-render on timelines with a zero-duration callback, because that
re-render re-arms the callback and the next forward seek fires it again. The answer was cached per
timeline on the first seek, so a tl.call added later was never seen: the re-render ran and the call
fired twice. The check now reads the timeline's current children on every seek.
This commit is contained in:
Miguel Ángel
2026-10-03 11:50:18 +00:00
committed by GitHub
parent b3c5207827
commit afe020ae59
2 changed files with 25 additions and 15 deletions
+23
View File
@@ -2897,6 +2897,29 @@ describe("initSandboxRuntimeModular", () => {
expect(seekCalls).toEqual([{ time: 2, suppressEvents: false }]);
});
it("fires a call added after the first seek exactly once", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "10");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const main = gsap.timeline({ paused: true }).to({ x: 0 }, { x: 1, duration: 10 });
window.__timelines = { main };
initSandboxRuntimeModular();
window.__player?.renderSeek(1);
const fired = vi.fn();
main.call(fired, [], 2);
window.__player?.renderSeek(2);
window.__player?.renderSeek(3);
expect(fired).toHaveBeenCalledTimes(1);
});
it("shows pip video at global start time even when host composition starts late", () => {
// Regression: resolveStartForElement used to add the host composition's start on top of
// the video's own data-start, causing double-offset. A pip video with data-start="45.40"
+2 -15
View File
@@ -4249,7 +4249,6 @@ export function initSandboxRuntimeModular(): void {
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
const gsapCallbackTweenCache = new WeakMap<RuntimeTimelineLike, boolean>();
const GSAP_CALLBACK_NAMES = [
"onStart",
"onUpdate",
@@ -4271,9 +4270,6 @@ export function initSandboxRuntimeModular(): void {
};
const hasZeroDurationCallbackTween = (timeline: RuntimeTimelineLike): boolean => {
const cached = gsapCallbackTweenCache.get(timeline);
if (cached != null) return cached;
if (!("getChildren" in timeline) || typeof timeline.getChildren !== "function") {
return false;
}
@@ -4283,13 +4279,9 @@ export function initSandboxRuntimeModular(): void {
children = timeline.getChildren(true, true, true);
} catch (err) {
swallow("runtime.init.gsapCallbackChildren", err);
gsapCallbackTweenCache.set(timeline, false);
return false;
}
if (!Array.isArray(children)) {
gsapCallbackTweenCache.set(timeline, false);
return false;
}
if (!Array.isArray(children)) return false;
for (const child of children) {
if (!isObjectRecord(child)) continue;
@@ -4300,13 +4292,8 @@ export function initSandboxRuntimeModular(): void {
const totalDuration = readGsapDuration(child, "totalDuration");
const duration = totalDuration ?? readGsapDuration(child, "duration");
if (duration != null && duration <= 0.000001) {
gsapCallbackTweenCache.set(timeline, true);
return true;
}
if (duration != null && duration <= 0.000001) return true;
}
gsapCallbackTweenCache.set(timeline, false);
return false;
};