Skip to content

fix(core): install runtime bridge after transport setup#2558

Merged
miguel-heygen merged 1 commit into
mainfrom
codex/pr2387-runtime-bridge
Jul 20, 2026
Merged

fix(core): install runtime bridge after transport setup#2558
miguel-heygen merged 1 commit into
mainfrom
codex/pr2387-runtime-bridge

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Studio preview now installs the runtime bridge only after its transport is ready, preventing initialization-order gaps without changing the bridge contract.

Stack

Part 1 of 20. Parent: main. Next: #2559. The golden reference, #2387, remains open and unchanged.

Test plan

  • Integrated Studio suite: 2,800 tests passed
  • Integrated parser suite: 853 tests passed
  • Studio and parser typechecks passed
  • Studio and parser production builds passed
  • Per-PR CI completes on the submitted stack

Post-Deploy Monitoring & Validation

Validation window: first 24 hours after the stack merges. Owner: Studio maintainers. Watch browser console and support reports for [Timeline], gsap-parser, failed keyframe mutations, or preview/render easing mismatches. Healthy means edits persist and preview/render agree; revert the first failing layer if authored animation data changes unexpectedly.


Compound Engineering
Codex

@miguel-heygen
miguel-heygen force-pushed the codex/pr2387-runtime-bridge branch from 7e38b13 to f38042c Compare July 16, 2026 20:47

miguel-heygen commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Bridge install is now the last step of initSandboxRuntimeModular, gated after every transport helper it dispatches to. Two hot-path helpers (seekTimelineAndAdapters, applyWebAudioRate) are lifted from const arrows to function declarations so hoisting covers their earlier call sites inside the transport object (lines 2161/2215/2237/2254) as well as the bridge closures. The race the fix closes is real and specific: installRuntimeControlBridge in packages/core/src/runtime/bridge.ts:125-131 posts {source: "hf-preview", type: "ready"} immediately after attaching the message listener, and the parent replays queued set-muted / set-volume / set-playback-rate / seek messages on that ready — which under the old ordering could re-enter the runtime before applyWebAudioRate / seekTimelineAndAdapters (both const arrows declared later in the function body) had crossed their TDZ. Fires deterministically in warm-cache reloads and the Claude desktop Electron client (both documented in the bridge comment). New test locks all three axes: value-side (timeline.time() === 2, timeScale called with 2), reachability-side (mock parent replays two control messages synchronously on ready), and idempotency (two inits → exactly two ready outbound messages, exactly two composition_seeked analytics events).

Findings

  • Race-window closure verified. Post-fix, the only synchronous-during-init postState calls that emit type: "state" fire at lines 2925-2926, immediately before the bridge install at line 2933. Parent only replays on hf-preview:ready, which is now emitted exclusively by installRuntimeControlBridge at line 131 of bridge.ts — so no replay path exists until every dispatch target is declared. The other postState(true) sites (1614, 1641, 1987/1988, 2182, 2198, 2223, 2243, 2400, 2808, 2815) all live inside reactive callbacks that fire in later ticks, not during the sync init frame.
  • Const→function conversion is required, not stylistic. transport.play (line 2161), transport.seek (2215), transport.renderSeek (2237) reference seekTimelineAndAdapters before its line-2592 declaration; transport.setPlaybackRate (2254) references applyWebAudioRate before its line-2899 declaration. Under the old const arrow form + the new install ordering, transport callbacks invoked by the player pre-bridge would still hit TDZ. Hoisting closes that too.
  • No consumer regression on state fields. State fields written by bridge handlers (bridgeMuted, bridgeVolume, mediaOutputMuted, nativeMediaSyncDisabled, webAudioMediaDisabled, playbackRate) are read at lines 1870-71 and 1892-1904 by postState, but those reads only happen inside the postState closure, which is invoked at 2925-26 with initial defaults just before bridge install. If the parent had queued a set-muted before the iframe loaded, the initial state message reflects bridgeMuted: false, then the replayed set-muted immediately overwrites — a one-tick UI flicker window that existed pre-fix as well; not a new regression.
  • Test coverage locks are strong. The outbound.filter(type=analytics && event=composition_seeked).toHaveLength(2) assertion is a good regression lock: it catches both silent-replay-drop (would go to 0) and duplicate-listener-stacking (would go to 4) on re-init. The outbound.filter(type=ready).toHaveLength(2) complements it on the announcement side.
  • Diff shape matches the fix thesis. +175/-117 decomposes to: init.test.ts +53 (new test only); init.ts +122/-117 = 117-line block relocated verbatim + 5-line explanatory comment + two arrow→function-declaration site rewrites at zero-sum churn. No embedded redesign hidden inside the churn.
  • Spec anchor. The 5-line comment at lines 2928-2932 is now the invariant's sole in-repo home. Fine as-is; a packages/core/src/runtime/README.md note calling out "bridge install must be the LAST synchronous step of init" would help future contributors. Non-blocking.

Recommendation

APPROVE. Race is real and specific, fix mechanism is the minimum viable one (relocate + hoist), consumer surface unchanged, test locks value + reachability + idempotency, CI clean at f38042c (all required checks SUCCESS on runs 29533676985 and 29539348222; earlier CANCELLED run 29533490877 was superseded).

— Via

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adversarial pass at HEAD f38042c01f03177e987334664abe72a913df5968. Context: HF#2558 is slice 1/20 of a new keyframe-UI series decomposing the unreviewed golden reference #2387 (OPEN, REVIEW_REQUIRED). Not a byte-identical carve-out from an approved parent — first-look at a slice.

Byte-identity vs #2387 (informational)

Both files DIFFER — init.test.ts and init.ts. Not a byte-for-byte carve-out; targeted refactor standing on its own.

Findings

No blockers. Fix is correct; ordering is verified textually; const-to-function-decl changes are nice hardening.

Concerns (1):

  • C1 Outbound-message ordering contract changed for type:"ready" vs state/timeline. OLD order: bridge posts ready early → many things run → postTimeline()postState(true). NEW order: postTimeline()postState(true) → bridge posts ready at init.ts:2925-2933. Any parent-side handler that treated ready as "first signal from iframe" now sees state/timeline first. Verified via grep: no consumer of type:"ready" exists in current codebase — posted-but-never-received. No live regression today, but this slice establishes the ordering as the contract before any consumer lands. Later slices should be aware that ready now arrives AFTER the first state/timeline post.

Nits (4):

  • N1 [informational] The PR describes this as a "race fix", but in real browsers window.parent.postMessage is asynchronous per spec — the parent's response can't dispatch synchronously to the same tick, so applyWebAudioRate/seekTimelineAndAdapters would always be initialized by the time a real control message fires. The test at init.test.ts:1701-1759 simulates the race by making window.parent.postMessage synchronously dispatch MessageEvents — a JSDOM affordance, not production behavior. Reordering is still worthwhile (belt-and-suspenders + robust to future refactors), but the PR body should be honest that this is preventive rather than a live bug users hit.
  • N2 not.toThrow() assertions at init.test.ts:1746-1747 are largely decorative — dispatchEvent catches uncaught listener errors and routes them through the DOM "report the exception" algorithm. The load-bearing assertion is the composition_seeked analytics count = 2 on the second seek. Consider spying on webAudio.setRate or asserting no error event on window to lock the invariant.
  • N3 The test title says "without duplicate listeners" but the mechanism verified is really "second initSandboxRuntimeModular() teardowns the first bridge via window.__hfRuntimeTeardown" — teardown-on-reinit logic untouched by this PR. Consider explicit assertion on window.addEventListener("message", ...) call count.
  • N4 Listener-leak on install failure at bridge.ts:125-131window.addEventListener runs before postRuntimeMessage. If a throw happens between them, listener leaks and state.controlBridgeHandler stays null so teardown skips it. Not introduced by this PR; unreachable in practice (postRuntimeMessage swallows own errors). Noting only.

Green surface (verified)

  • Init-order correctnessinstallRuntimeControlBridge at init.ts:2933 is the last significant statement before teardown definition. All handler dependencies verified declared before it: applyPlaybackRate at :2128, growRootDurationLive at :1970, syncMediaForCurrentState at :1798, scheduleWebAudioForActiveClips at :2850, applyWebAudioRate at :2899 (hoisted function), seekTimelineAndAdapters at :2592 (hoisted function). player/webAudio/clock/picker/colorGrading all created earlier. No forward references at bridge-install time.
  • Belt-and-suspenders via function-decl hoistingapplyWebAudioRate + seekTimelineAndAdapters changed from const arrow to function. Neither uses this; hoisting safe. Runtime resilient to future re-orderings.
  • Reinit pathwindow.__hfRuntimeTeardown at init.ts:142-149 cleans up previous instance before second init. Removes previous message listener via init.ts:3063-3066. New test exercises this path twice.
  • Ordering guarantee is trivial — synchronous sequential code inside single function; no promises/events/async. "AFTER transport setup" enforced by textual order.
  • No public API changesinstallRuntimeControlBridge + initSandboxRuntimeModular signatures unchanged. Diff is code-shuffle + 2 arrow-to-function-decl conversions + test addition. Nothing exported changed.
  • Idempotency confirmed — on second initSandboxRuntimeModular(), teardown unwires prior listener before new one installs. Test asserts via message counts.
  • Cross-frame race window empty in production — asynchronous postMessage means bridge is always installed by the time the parent replays controls.

What I didn't verify

  • Parent side of the type:"ready" handshake in #2387 (unreviewed).
  • Behavior in the Claude desktop Electron client mentioned in bridge.ts:130 ("deterministic race on warm-cache reloads") — not reproducible from static analysis.
  • Interaction with hot-reload/HMR in Studio.
  • Full 2,800/853 test-suite claims — only read the new test.

Merge gate

Clean init-order fix. No blockers. C1's ordering-contract change is the one worth naming for downstream slices — the parent-side consumer of ready (which will presumably arrive in a later slice) needs to know state/timeline arrive first.

LGTM from my side — leaving as COMMENTED.

Review by Rames D Jusso

@miguel-heygen
miguel-heygen merged commit 735128a into main Jul 20, 2026
133 of 168 checks passed
@miguel-heygen
miguel-heygen deleted the codex/pr2387-runtime-bridge branch July 20, 2026 01:49
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.

3 participants