Skip to content

fix(studio): a failed preview reload says which step it was still waiting for - #5700

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/preview-reload-names-what-it-waited-for
Oct 16, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/preview-reload-names-what-it-waited-for

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

When an edit's preview reload fails, the message now also says which step the new document was still waiting for: "The preview did not reload (it took too long to load). It was still waiting for the page and its runtime to load / its timeline to be ready / its loaders to clear. The previous preview is still showing."

Why

Reload failures report how they ended (the reload budget ran out, or the player's probe gave up) but not what never happened, so a stuck reload cannot be traced to a cause from the report alone. Each failure now names the first readiness step that had not completed.

How

  • failShadow in useShadowPreviewReload.ts reads the shadow's state before it is discarded: no runtime bridge (__player) means the page and its runtime had not loaded; no pending commit means its timeline (a positive length and the restore seek's paint) was not ready; otherwise its loaders had not cleared.
  • The detail is a new sentence after the existing cause, so anything matching the start of the message keeps matching. It is also logged with logReload("shadow-failed", ...).

No visible change

Only the text of an existing error message changes; nothing on screen moves or renders differently.

Test plan

  • Unit tests added/updated: useTimelinePlayer.shadowReload.test.ts asserts the step for each case (no runtime, runtime with no length, timeline ready but loaders up). Breaking the timeline check makes its test fail.
  • packages/studio/src/player: 2303/2303, 3 runs in a row; tsc --noEmit clean.
  • Manual testing performed: Studio dev server, a reload whose runtime never starts: the console shows "... It was still waiting for the page and its runtime to load."
  • Documentation updated (if applicable): not applicable.
  • Comments follow CONTRIBUTING.md "Comments".

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 166 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)

Unstable (1)

  • crop-tween-xpercent-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.03, reload 40.03, render 39.99, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.03, reload 0.03, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.03, reload 0.03, render 0.03, renderKey -, undo true, teleport true

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed heygen-com/hyperframes#5700 at HEAD 0cd1591e6f8bcd288832d65d88987c59d4833401 against effective/API BASE 4e6fcdb3a9f310a9c483622d6a4ca5354a63fb22.

[P2] Keep failure diagnostics from bypassing shadow cleanup

useShadowPreviewReload.ts:107–112: the new shadowWin?.__player read can throw SecurityError on an inaccessible cross-origin iframe; optional chaining only handles a null/undefined window. This runs before every cleanup and failure notification. For example, while the old live preview is showing, an edited composition can navigate its hidden shadow to another origin before promotion. The actual iframe allows scripts/self-navigation, and CompositionProbe already handles inaccessible frames by catching SecurityError and reporting its eight-second timeout. That error reaches this callback through Player → NLEPreview.onPreviewError → onShadowError; the hook's own terminal wait budget also reaches it. BASE discards that failed shadow, clears the reload flags, and reports the failure through EditorShell's error toast. HEAD instead throws before invalidating the generation, discarding the shadow, clearing previewReloading, or invoking the failure callback; the later timeout hits the same failing read. The old preview stays visible, but the reload remains stuck until another reload/reset/unmount.

Make the diagnostic read best-effort with a safe fallback, so cleanup and reporting always run. Add a current-generation inaccessible-window/property regression asserting retained live preview, discarded shadow, cleared reloading state, and exactly one failure callback; retain the stale-generation control.

Validation / scope: source-only review, with full BASE-before-HEAD reads of both changed files and an independent adversarial pass. Traced the actual iframe policy, authored preview route, probe/error forwarding, reload timer, and toast callback. Normal readiness ordering, restore-seek paint, stale-generation rejection, visual-readiness re-raise, superseding reloads, and cleanup otherwise remain unchanged. This finding is a source-derived failure path, not a claimed browser reproduction. No tests, repository/browser/native runtime, or CI queries were run; author/bot test reports are not independent validation.

— tai

Verdict: Changes requested.
Reasoning: The new diagnostic introduces a throwing operation before the existing handled-failure exit, preventing cleanup and user-visible failure reporting for an inaccessible current shadow.

…p and reports

Naming the step the reload waited for read the hidden frame's player, which throws
for a cross-origin frame and skipped the cleanup and the failure report.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Fixed in 4845b65. You were right: the diagnostic read could throw before the cleanup ran.

  • failShadow now reads the frame's player through shadowHasPlayer, which catches the SecurityError. A cross-origin frame reports "the page and its runtime to load", because its runtime is out of reach.
  • New test, "still drops and reports a shadow whose window throws on access". Its shadow is from the current generation, and reading its __player throws SecurityError. It checks that the live preview is kept, the shadow is dropped, the preview no longer counts as reloading, and the failure callback fires exactly once.
  • Removing the try fails the new test, and the stale-generation control is unchanged.
  • Studio player tests: 2304 passed, 3 runs in a row.

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at HEAD d3d77676038e0e4c94170c73400ac6ca11d1ae93 against API/effective BASE 4e6fcdb3a9f310a9c483622d6a4ca5354a63fb22. No introduced blocker found in this bounded source review.

  • Prior P2 resolved: useShadowPreviewReload.ts:38–44 now catches the entire iframe/window/player access. An inaccessible window no longer lets the new diagnostic bypass shadow cleanup and failure reporting. I checked fixing commit 4845b65b and the subsequent helper rename at this head.
  • Meaningful regression source: useTimelinePlayer.shadowReload.test.ts:523–546 preserves a throwing __player getter through the real hook harness and asserts shadow discard, old-live identity, cleared reload state, and exactly one fallback report.
  • useShadowPreviewReload.ts:112–130 snapshots the waiting step before clearing refs, while retaining generation rejection, cleanup, the previous-preview message, and the latest failure callback.

Audited: both changed files end-to-end at BASE then HEAD; actual adapter/readiness/error-to-toast paths; normal, fallback, and subcomposition runtime injection; diagnostic/cleanup, stale-callback, seek-paint/readiness, supersession/reset, and debug-consumer interactions. An independent adversarial source pass also found no blocker.

Trusting / limits: unchanged subsystems outside those paths and unselected player internals; native DOM/origin and runtime behavior were not executed. Regression tests were inspected as source only. No tests, browser/runtime execution, or CI queries performed; author-reported results are not my validation.

— tai

Verdict: APPROVE
Reasoning: The previously reported cleanup bypass is repaired at this exact head, and the diagnostic addition preserves the inspected failure, promotion, and live-preview ownership paths.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 7266646 Oct 16, 2026
63 checks passed
@miguel-heygen
miguel-heygen deleted the fix/preview-reload-names-what-it-waited-for branch October 16, 2026 18:10
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.

2 participants