Skip to content

fix(core): a runtime that fails to start names its real error instead of a half-built page - #5406

Open
miguel-heygen wants to merge 9 commits into
mainfrom
fix/cliptree-partial-timeline
Open

miguel-heygen wants to merge 9 commits into
mainfrom
fix/cliptree-partial-timeline

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

What changes for the user

hyperframes check no longer dies with check_runtime_failure: Cannot access 'nS' before initialization on a project whose sub-composition registers a partial timeline, such as window.__timelines["extra"] = { duration: 4, seek() {} } or one whose duration() throws. The browser audit runs and reports layout, motion and contrast again. Preview and render of the same page stop half-initialising too.

Root cause

During runtime start-up the timeline is announced, which builds the clip tree and the timeline payload. Both read each registered sub-composition timeline's duration:

  • clipTree.ts called registry[id]?.duration?.(). Optional call only skips null/undefined, so a numeric duration was called and threw TypeError: registry[compId]?.duration is not a function.
  • timeline.ts checked that duration was a function but did not guard the call, so a duration() that throws escaped too.

Either throw aborted start-up partway, before the seek helpers further down were initialised. The next seek from check then hit the temporal dead zone of activateSiblingTimelines (minified nS), an error that names an internal variable instead of the real cause.

The same guarded read existed in five places (init.ts, timeline.ts twice, startResolver.ts, clipTree.ts) with two of them missing a guard. They now share one reader, readTimelineDurationSeconds: a finite duration() or null, with the type check inside the guard so a throwing getter or a Proxy entry reads as null too. Each caller keeps its own rule on top (> 0, or clamped at 0).

A start-up that throws now says so

Removing the trigger leaves the class: any exception during runtime start-up left a half-built page that the next seek reported as an internal "before initialization" error. Now:

  • One recorder, recordStartupError (diagnostics.ts), sets window.__hfStartupError ("HyperFrames runtime failed: : . Check the composition's scripts and window.__timelines entries; if they look right, report this as a HyperFrames bug."). A thrown value that cannot be printed is recorded as "unknown error" rather than making the recorder throw. entry.ts calls it when start-up throws, and the render-ready publish calls it when its timeline post throws, including publishes after start-up (GSAP batching finishing, the deferred rebind, adapter readiness). Both still rethrow. Studio's editor refresh posts are a separate path: they keep their own retry and do not record.
  • The page is never left marked render-ready when posting the timeline throws: the flag keeps its place (the first-frame readiness check posted with the timeline waits on it) and is cleared before the error propagates. Once recorded, the error stays set: start-up schedules its own later posts (the deferred rebind, GSAP batching finishing), and clearing on their success would erase a real start-up error and let a half-built page look ready. A render-ready publish whose timeline post throws after a full start is reported as failed too, even if a later publish succeeds, by design (fail loudly rather than render from a page that could not post its timeline).
  • The CLI's readiness wait (check, snapshot, keyframes, compare, layout, validate) and the engine's render wait fail at once with that message instead of timing out or seeking a half-built page.
  • The render probe in the producer now catches a duration() that throws (it runs in the page and cannot import the shared reader); its handling of a non-finite duration is unchanged.
  • position-edits-render-inline.ts is regenerated because the bundle it holds imports diagnostics.ts.
  • Drive-by: a frameCapture.ts comment no longer cites a doc file that was never committed.

Also in this PR (same check audit)

  • check's seek-clock probe (collectSeekClock) read a custom timeline's time() with Number(...), so time: () => null became 0 and a working custom root looked like a stuck clock (exit 1). It now accepts only a finite number.

Verification

  • New init.test.ts cases run real runtime start-up and a seek with a sub-composition timeline whose duration is a number, a method that throws, or a getter that throws. With main's runtime files all three fail (registry[compId]?.duration is not a function, boom, getter); with this change they pass. clipTree.test.ts covers the same shapes at the unit level.
  • entry.startupFailure.test.ts: a throwing start-up records the named error, a start-up whose timeline post throws is not left render-ready, a post that throws after start-up (the deferred rebind) is named and leaves the page not ready, and a start-up error survives a later successful post. Both fail on the previous code. The CLI and engine waits each have a test that fails without the change, and so does the producer probe; a checkBrowser.test.ts case pins the null time() clock (fails on the previous code).
  • Runtime suites (345 + 33 entry tests), the CLI capture and check tests (136), the engine frame-capture tests (55) and the producer compiler tests (141) pass 3 runs in a row; typecheck is clean for core, cli, engine and producer. Real GSAP timelines read the same as before (0, null, NaN, Infinity from duration() give the same results).

Known limits

  • A start-up that throws still does not tear down what it already started (the teardown is installed at the end of start-up). The page reports the error; a reload replaces it.
  • The player and Studio readiness input is unchanged: it only resolves, bounded by its abort signal, so it cannot hang on a failed start-up. Studio's thumbnail capture does not read the new error yet (cosmetic).
  • The CLI and engine waits read the error while they wait. A render-ready publish that throws after the wait has returned is recorded but not read by that run, which continues as it does today.
  • The deprecated validate reports only the start-up error on such a page, not the other console errors it collected.
  • A sub-composition entry that is a Proxy throwing on every property read still fails start-up (in timeline selection, not the duration read); it is now reported by name.

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1664 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@miguel-heygen miguel-heygen changed the title fix(core): a sub-composition timeline with a broken duration no longer aborts the runtime fix(core): a runtime that fails to start names its real error instead of a half-built page Oct 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant