From 2c84c9a55d7342e542ca67f5830edd2fcce19515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 18 May 2026 00:39:10 -0400 Subject: [PATCH] fix(engine,core): wait for async timelines and force rebind before capture Two fixes for compositions that register timelines after async data loading (e.g. fetch for TopoJSON map data): 1. engine/frameCapture: remove the hosts.length <= 1 early return so the timeline readiness poll runs for ALL compositions, not just multi-composition galleries. Single-composition pages with async setup were silently skipped. 2. core/runtime/init: expose window.__hfForceTimelineRebind() which resets childrenBound and re-runs bindRootTimelineIfAvailable(). The renderer calls this after all timelines are confirmed present, ensuring the root player discovers late-registered timelines from fetch callbacks. Without these fixes, compositions using fetch() to load data at runtime would render blank frames because the root player bound timelines before the async setup completed, and seek() never reached the unbound composition timeline. --- packages/core/src/runtime/init.ts | 5 +++++ packages/engine/src/services/frameCapture.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 5c677959e..a57de988a 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -964,6 +964,11 @@ export function initSandboxRuntimeModular(): void { return true; }; + (window as any).__hfForceTimelineRebind = () => { + childrenBound = false; + bindRootTimelineIfAvailable(); + }; + const emitRootStageLayoutDiagnostics = () => { const rootNode = resolveRootCompositionElement(); if (!(rootNode instanceof HTMLElement)) { diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 313e097a9..ea1b01ec6 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -356,7 +356,7 @@ async function pollSubCompositionTimelines( ): Promise { const expression = `(function() { var hosts = document.querySelectorAll("[data-composition-id]"); - if (hosts.length <= 1) return true; + if (hosts.length === 0) return true; var timelines = window.__timelines || {}; for (var i = 0; i < hosts.length; i++) { var id = hosts[i].getAttribute("data-composition-id"); @@ -366,6 +366,13 @@ async function pollSubCompositionTimelines( return true; })()`; const ready = await pollPageExpression(page, expression, timeoutMs, intervalMs); + if (ready) { + await page.evaluate(`(function() { + if (typeof window.__hfForceTimelineRebind === "function") { + window.__hfForceTimelineRebind(); + } + })()`); + } if (!ready) { const missing = await page.evaluate(`(function() { var hosts = document.querySelectorAll("[data-composition-id]");