MCPcopy Create free account
hub / github.com/heygen-com/hyperframes / applySoftReload

Function applySoftReload

packages/studio/src/utils/gsapSoftReload.ts:174–385  ·  view source on GitHub ↗
(
  iframe: HTMLIFrameElement | null,
  scriptText: string,
  onAsyncFailure?: () => void,
  currentTimeOverride?: number,
)

Source from the content-addressed store, hash-verified

172 * synchronous paths.
173 */
174export function applySoftReload(
175 iframe: HTMLIFrameElement | null,
176 scriptText: string,
177 onAsyncFailure?: () => void,
178 currentTimeOverride?: number,
179): SoftReloadResult {
180 if (!iframe || !scriptText) return "cannot-soft-reload";
181
182 const win = iframe.contentWindow as IframeWindow | null;
183 const doc = iframe.contentDocument;
184 if (!win || !doc) return "cannot-soft-reload";
185 if (!win.gsap || !win.__hfForceTimelineRebind) return "cannot-soft-reload";
186
187 // Which composition(s) does this script rebuild? A soft reload re-runs ONE
188 // composition's GSAP script, which re-registers its own window.__timelines[key].
189 // In a multi-composition preview (top-level + inlined subcompositions) each
190 // composition owns a separate timeline keyed by its id, and they're all children
191 // of the global timeline — so tearing down ALL of them (or the global timeline's
192 // children) and re-running a single script wipes every OTHER composition,
193 // reverting its edits. Scope the teardown to the keys THIS script re-registers.
194 const targetKeys = [...scriptText.matchAll(/__timelines\s*\[\s*["'`]([^"'`]+)["'`]\s*\]/g)]
195 .map((m) => m[1]!)
196 .filter((key) => key !== "__proxied");
197 if (targetKeys.length === 0) return "cannot-soft-reload"; // can't scope safely → full reload
198 const gsapScripts = findGsapScriptElements(doc);
199 if (gsapScripts.length === 0) return "cannot-soft-reload";
200 // Remove only the stale script element(s) that registered a target key; one we
201 // can't match in the doc is left alone (re-running appends a fresh element).
202 const staleScripts = gsapScripts.filter((script) =>
203 targetKeys.some((key) => {
204 const text = script.textContent || "";
205 return text.includes(`__timelines["${key}"]`) || text.includes(`__timelines['${key}']`);
206 }),
207 );
208 // Multiple GSAP scripts exist but none registers a key this script owns — we
209 // can't identify which element to replace (ambiguous, matching
210 // extractGsapScriptText's single-script requirement). Escalate to a full reload
211 // rather than killing the target timeline and appending an orphan script.
212 if (gsapScripts.length > 1 && staleScripts.length === 0) return "cannot-soft-reload";
213
214 // Prefer the caller-supplied scrub position (the studio's own authoritative
215 // currentTime, e.g. usePlayerStore) over the iframe's raw `__player.getTime()`:
216 // the two can desync (a keyframe-node drag parks the playhead via the store
217 // BEFORE this reload's async commit resolves, and the iframe's own GSAP clock
218 // doesn't reliably reflect that yet), which re-seeks the freshly rebuilt
219 // timeline to the wrong frame and leaves the element (and its overlay)
220 // rendered at a stale/unrelated position.
221 const currentTime = currentTimeOverride ?? win.__player?.getTime?.() ?? 0;
222
223 // Track whether the MotionPath async path was taken. When it is, the script
224 // executes inside pluginScript.onload — after applySoftReload has already
225 // returned. We optimistically return true because the script WILL execute
226 // once the plugin loads; the alternative (returning false) would trigger a
227 // full iframe reload that destroys the very WebGL context we're preserving.
228 let deferredToAsync = false;
229
230 // fallow-ignore-next-line complexity
231 const doReload = () => {

Callers 3

softReloadOrEscalateFunction · 0.90

Calls 4

findGsapScriptElementsFunction · 0.85
doReloadFunction · 0.85
verifyTimelinesPopulatedFunction · 0.85
getTimeMethod · 0.65

Tested by

no test coverage detected