mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
Validate the parse response before reading `.animations` instead of casting the JSON blind, and narrow the fetch's return type to the slice callers read. Route the AST cache load through the shared clip-keyframe and cache-key helpers so it can't drift from the other writer. Drop the unused numeric track-count branches from `trackHeights`/`getTimelineCanvasHeight`, and pick the widest keyframed clip with a reduce so there's no index assertion.
76 lines
3.5 KiB
TypeScript
76 lines
3.5 KiB
TypeScript
import { useCallback } from "react";
|
|
import type { GsapAnimation, ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCache";
|
|
|
|
// A cold parse is the initial-load race: the endpoint is reachable but its parse
|
|
// isn't warm yet (zero total animations). It's worth waiting out (~600ms).
|
|
const COLD_PARSE_RETRIES = 5;
|
|
const COLD_PARSE_DELAY_MS = 120;
|
|
// A hard fetch error (404/403/network/JSON failure → `fetchParsedAnimations`
|
|
// returns null) is NOT a parse-warming race, so it shouldn't burn the full
|
|
// cold-parse budget. One short retry covers a transient blip; beyond that the
|
|
// endpoint genuinely isn't serving this file, so fall through to "no animation".
|
|
const FETCH_ERROR_RETRIES = 1;
|
|
const FETCH_ERROR_DELAY_MS = 120;
|
|
|
|
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
|
|
/**
|
|
* Outcome of resolving an element's animations from a single parse result.
|
|
* - `resolved`: a definitive answer (matched animations, or `[]` when the parse
|
|
* is warm but this element has no animation — create a new one, don't retry).
|
|
* - `fetch-error`: `fetchParsedAnimations` returned null (HTTP/network/JSON
|
|
* failure) — retry briefly, not for the full cold-parse budget.
|
|
* - `cold`: the parse came back reachable but with zero total animations — the
|
|
* initial-load warming race, worth the full cold-parse retry budget.
|
|
*/
|
|
export type ElementAnimationsOutcome =
|
|
| { kind: "resolved"; animations: GsapAnimation[] }
|
|
| { kind: "fetch-error" }
|
|
| { kind: "cold" };
|
|
|
|
/**
|
|
* Classify a parse result for one element. Differentiates a hard fetch failure
|
|
* (`parsed === null`) from a warm-but-empty cold parse (`animations.length === 0`)
|
|
* so the caller can apply the right retry budget to each.
|
|
*/
|
|
export function selectElementAnimationsOrRetry(
|
|
parsed: Pick<ParsedGsap, "animations"> | null,
|
|
target: { id: string | null; selector: string | null },
|
|
): ElementAnimationsOutcome {
|
|
if (!parsed) return { kind: "fetch-error" };
|
|
if (parsed.animations.length === 0) return { kind: "cold" };
|
|
return { kind: "resolved", animations: getAnimationsForElement(parsed.animations, target) };
|
|
}
|
|
|
|
export function useGsapAnimationFetchFallback(projectId: string | null, gsapSourceFile: string) {
|
|
return useCallback(
|
|
(selection: DomEditSelection) => async (): Promise<GsapAnimation[]> => {
|
|
if (!projectId) return [];
|
|
const target = { id: selection.id ?? null, selector: selection.selector ?? null };
|
|
// A drag can fire before the async parse is warm; a cold parse must retry
|
|
// rather than fall through to the no-animation path (which duplicates the
|
|
// tween). A hard fetch error is a different failure — retry only briefly.
|
|
let coldAttempts = 0;
|
|
let errorAttempts = 0;
|
|
for (;;) {
|
|
const parsed = await fetchParsedAnimations(projectId, gsapSourceFile);
|
|
const outcome = selectElementAnimationsOrRetry(parsed, target);
|
|
if (outcome.kind === "resolved") return outcome.animations;
|
|
if (outcome.kind === "fetch-error") {
|
|
if (errorAttempts >= FETCH_ERROR_RETRIES) return [];
|
|
errorAttempts++;
|
|
await delay(FETCH_ERROR_DELAY_MS);
|
|
continue;
|
|
}
|
|
// cold
|
|
if (coldAttempts >= COLD_PARSE_RETRIES) return [];
|
|
coldAttempts++;
|
|
await delay(COLD_PARSE_DELAY_MS);
|
|
}
|
|
},
|
|
[projectId, gsapSourceFile],
|
|
);
|
|
}
|