heygen-com/hyperframes · error · Error

hf-seek waitUntil() must be called synchronously

Error message

hf-seek waitUntil() must be called synchronously

What it means

Thrown inside the browser by seekAllAdaptersInBrowser when a composition's hf-seek event handler calls detail.waitUntil(promise) asynchronously — after the dispatchEvent call has returned synchronously. The acceptingGpuWork flag is flipped to false in the finally block the instant dispatchEvent returns, so any waitUntil call outside that synchronous handler window is rejected. This enforces that GPU work registration happens during the event dispatch tick, before the seek routine awaits pending work.

Source

Thrown at packages/cli/src/commands/motionShot.ts:251

  for (const instance of w.__hfAnime ?? []) {
    tryCall(() => {
      instance.pause?.();
      instance.seek?.(timeMs);
    });
  }

  w.__hfThreeTime = tt;
  if (!runtimeSeeked) {
    let acceptingGpuWork = true;
    try {
      window.dispatchEvent(
        new CustomEvent("hf-seek", {
          detail: {
            time: tt,
            waitUntil(promise: PromiseLike<unknown>) {
              if (!acceptingGpuWork) {
                throw new Error("hf-seek waitUntil() must be called synchronously");
              }
              pendingGpuWork.push(promise);
            },
          },
        }),
      );
    } finally {
      acceptingGpuWork = false;
    }
  }
  tryCall(() => w.__hfThreeRender?.());
  tryCall(() => w.gsap?.ticker?.tick?.());

  await Promise.all([Promise.all(pendingGpuWork), w.__hfWaitForSeekCompletion?.()]);
}

// Installs seekAllAdaptersInBrowser as a real `window` global, once per page
// load. Both the ghost-frame capture and the marker sampler then call it via a

View on GitHub (pinned to c2996c8626)

Solutions

  1. Call waitUntil synchronously inside the hf-seek handler, before any await or setTimeout
  2. Register the GPU-work promise at call time even if the work itself resolves later
  3. If using an async handler, collect the promise synchronously and defer only the await
  4. Restructure: do the synchronous registration first, then the async rendering inside the promise

Example fix

// before — waitUntil called after a delay (throws)
window.addEventListener('hf-seek', (e) => {
  requestAnimationFrame(() => e.detail.waitUntil(renderGpu()));
});
// after — register synchronously inside the handler
window.addEventListener('hf-seek', (e) => {
  e.detail.waitUntil(renderGpu());
});
Defensive patterns

Strategy: validation

Validate before calling

// Composition-authoring rule (runs in the browser):
// Register waitUntil synchronously inside the hf-seek handler.
window.addEventListener('hf-seek', (event) => {
  const detail = (event as CustomEvent).detail;
  // GOOD: synchronous registration
  detail.waitUntil(myGpuRenderPromise);
  // BAD (would throw): setTimeout(() => detail.waitUntil(p), 0);
});

Prevention

When it happens

Trigger: A composition registers window.addEventListener('hf-seek', e => { ... }) and inside it calls e.detail.waitUntil() inside a setTimeout, requestAnimationFrame, Promise.then, or any deferred callback. Also triggered if waitUntil is called from an async handler after an await.

Common situations: Custom WebGL/three.js adapters that schedule GPU work via rAF or microtasks. Event handlers written as async functions where waitUntil is called after an await. Adapters ported from an async rendering pipeline that defer submission.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/920384b9f4aa0a59. Report an issue: GitHub.