fix(studio-server): previewAdapter getElementTimings ignores relative data-start refs#2097
Conversation
cea98f9 to
0a036dc
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Good port of the SDK's #2092 resolver pattern to studio-server's previewAdapter. Root cause is correct (raw parseFloat on reference expressions + missing data-duration support), and the recursive resolver with cycle guard is well-structured.
Two non-blocking observations:
-
SSOT documentation — This is now the third copy of the "resolve relative data-start" pattern (runtime
startResolver.ts, SDKgetElementTimings, studio-serverpreviewAdapter). The runtime version is substantially more complex (host offsets, media, timelines), so a shared extraction isn't straightforward. But afallow-ignorecomment at the top of the resolver block (likepositionEdits.tsdoes forsplitTopLevelWhitespace) would help future readers understand why three resolvers coexist and which is the authority. -
Dead
parseFloatfallback (resolveStart, theelsebranch) — IfparseStartExpressionreturnsnull, the input is neither empty, a finite number, nor a valid reference expression. A subsequentparseFloatcan only succeed on strings like"3abc"(leading digits thatNumber()rejects). Harmless, but if it fires it's probably a malformed attribute that should resolve toundefinedanyway. Consider dropping the fallback or adding a brief comment explaining when it could fire.
Test coverage is thorough — duration-based end, reference with/without offset, missing target, and mutual cycle all covered.
LGTM.
— Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 0a036dc.
Solid recursive resolver with the right cycle-safety shape — visiting set entered/exited in try/finally, startCache memoizing undefined (via .has-then-.get) so a broken chain doesn't re-explore on every outer-loop iteration, mutual recursion between resolveStart and resolveEnd → resolveStart that still terminates through the guard. The 5-test set covers the interesting corners (duration→end path that never worked, bare vs +offset reference, missing target undefined-not-NaN, mutual cycle terminates). Nice call-out in the cycle-test comment about the intentional divergence from the SDK's static resolver (no 0-fallback → no fabricated numbers).
Two inline comments on the resolver's edges (clamp consistency w/ #2092, fallthrough silence) — both non-blocking.
One coverage nit for the "questions" bucket: no test exercises "reference target exists but its own timing is unresolvable" (e.g. target has neither data-end nor data-duration). Test 4 ("target doesn't exist") covers the findById → null branch; the current tests don't cover the findById → target && resolveEnd(target) → undefined branch. Would return undefined correctly via the existing code, but a test would pin the behavior. Cheap to add.
CI still starting up at review time — will keep an eye if any of the studio-server suites regress.
…ng resolver Documents the shared-pattern context (3rd copy of "resolve relative data-start", after runtime startResolver.ts and the SDK's own getElementTimings) and explains when the raw parseFloat fallback in resolveStart's else branch can actually fire (a malformed grammar string with a leading number). Adds a test pinning the "reference target exists but its own timing is unresolvable" branch, which existing tests didn't cover (only "target doesn't exist" was tested). Cross-checked the negative-offset clamp concern raised in review: the SDK's own resolveReferenceStart (session.ts) also clamps to Math.max(0, ...), so this stays consistent with its sibling — no code change needed there.
…ng resolver Documents the shared-pattern context (3rd copy of "resolve relative data-start", after runtime startResolver.ts and the SDK's own getElementTimings) and explains when the raw parseFloat fallback in resolveStart's else branch can actually fire (a malformed grammar string with a leading number). Adds a test pinning the "reference target exists but its own timing is unresolvable" branch, which existing tests didn't cover (only "target doesn't exist" was tested). Cross-checked the negative-offset clamp concern raised in review: the SDK's own resolveReferenceStart (session.ts) also clamps to Math.max(0, ...), so this stays consistent with its sibling — no code change needed there.
jrusso1020
left a comment
There was a problem hiding this comment.
Approving — relative data-start resolver port; RDJ/Miga R1 addressed at 8fb7ff7 (added the target-exists-but-unresolvable test + the resolver-copies + parseFloat-fallthrough comments). CI clean (only Graphite mergeability_check pending = stack artifact). Layered on RDJ + Miga. — Rames Jusso
… data-start refs Same bug class as the SDK's getElementTimings fix (#2092): data-start can be a relative-reference expression ("intro", "intro + 2"), not just an absolute number. The old code did a raw parseFloat on it, so any reference silently resolved to undefined instead of an actual time. Also: this function never read data-duration at all (only data-start/data-end literally), so a reference to a duration-authored (not end-authored) clip was unresolvable regardless of the parseFloat bug — resolving a reference needs the target's END, which for a duration-authored clip requires start+duration. Both fixed together via the shared parseStartExpression grammar parser (@hyperframes/core/runtime/start-expression), with the same cycle-guard pattern as the SDK fix. Reference resolution against other elements is scoped to this file's existing findById (bare data-hf-id lookup). 6 new tests: duration-based end resolution, relative reference (with and without offset), missing target, and a mutual-cycle termination check.
…ng resolver Documents the shared-pattern context (3rd copy of "resolve relative data-start", after runtime startResolver.ts and the SDK's own getElementTimings) and explains when the raw parseFloat fallback in resolveStart's else branch can actually fire (a malformed grammar string with a leading number). Adds a test pinning the "reference target exists but its own timing is unresolvable" branch, which existing tests didn't cover (only "target doesn't exist" was tested). Cross-checked the negative-offset clamp concern raised in review: the SDK's own resolveReferenceStart (session.ts) also clamps to Math.max(0, ...), so this stays consistent with its sibling — no code change needed there.
8fb7ff7 to
e4c4d2e
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Re-approving at head e4c4d2e1 — all 8 regression-shards green (0 failures), and the previewAdapter relative-timing fix + the R1 follow-ups (unresolvable-timing test, resolver-copies context comment, parseFloat fallthrough comment) are all present. ci.yml (Build/Lint/Typecheck) green as the stack bottom.

What
createPreviewAdapter'sgetElementTimings()did a rawparseFloatondata-start, which can be a relative-reference expression ("intro","intro + 2") per the docs' Relative Timing grammar, not just an absolute number. Any reference silently resolved toundefined. It also never readdata-durationat all — onlydata-start/data-endliterally — so a reference to a duration-authored (not end-authored) clip was unresolvable either way.Why
Same bug class just fixed in
@hyperframes/sdk'sgetElementTimings()(#2092) — found while auditing the SDK's surface, since this is studio's own (pre-SDK, server-side) copy of the same capability and had the identical unfixed bug.How
Both fixed via the shared
parseStartExpressiongrammar parser (@hyperframes/core/runtime/start-expression), with the same recursive resolver + cycle-guard pattern as the SDK fix (split intoresolveStart/resolveReferenceStart/resolveEndto keep each function's complexity low — fallow's complexity gate caught the unsplit version). Reference resolution reuses this file's existingfindById(baredata-hf-idlookup).Test plan
previewAdapter.test.tssuite passes (28/28)@hyperframes/studio-serversuite passes (22 files / 256 tests)bun run buildclean (studio-server + full workspace, incl. studio)