fix(studio): serialize per-animationId GSAP meta commits (shadow ease race)#1512
Conversation
bed31a4 to
a43b4e2
Compare
9b414a2 to
1aada07
Compare
a43b4e2 to
e612d9d
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Review at HEAD 1aada07.
Shape of the fix — correct. Broadening from per-animationId to per-sourceFile at the commitMutation chokepoint is the right grain: the server route is /api/projects/{pid}/gsap-mutations/{sourceFile}, so per-file is exactly the unit that races server-side. Cross-animation writes to the same file would have raced under the per-animationId scheme; per-file closes that. useRef(createKeyedSerializer()) survives re-renders, the chain is rejection-safe + self-cleaning, and the 4 serializer unit tests cover the critical contracts (ordering, distinct-key parallelism, rejection recovery, value propagation). Order preservation is FIFO under React's microtask scheduling, which is what callers will assume.
Concerns
1. targetPath resolution is duplicated between the wrapper and runCommit — coupling bug waiting to happen. useGsapScriptCommits.ts:124 (wrapper) and the same selection.sourceFile || activeCompPath || "index.html" expression inside runCommit (resolving targetPath before mutateGsapScript) are two copies of the same fallback chain. If either drifts (e.g. someone adds a 4th fallback in one place, or normalizes paths in one), the serializer key won't match the actual POST target — silent re-introduction of the race for the drifted path. Either extract a resolveTargetPath(selection, activeCompPath) helper used by both, or compute once in the wrapper and pass targetPath to runCommit as the source of truth. Cheap to do now, expensive to debug later.
2. No failure-path observability. Per the fidelity-telemetry origin story of this PR (16 gsap_fidelity:ease mismatches surfaced the bug), a regression here is invisible until shadow telemetry catches it again. Consider a debug-level counter for queue depth per key — or at minimum a metric on queue depth ≥ N or per-key wait time, so a future regression (slow file route → growing queue → user-visible lag) is observable before the next shadow-telemetry round. Failure paths in runCommit already throw through the chain, but no counter on serialized-chain rejections either.
3. No queue timeout / wedge handling. A hung POST blocks every subsequent same-file commit until it settles or rejects. mutateGsapScript uses bare fetch with no AbortController/timeout — if the request takes 30s, the next edit to that file takes 30s+. Interactive studio surface, so this could be perceptible. Probably fine in practice (HTTP times out eventually), but worth a thought-pass: is there a fetch-level timeout upstream you're relying on, or should this serializer (or mutateGsapScript) carry one?
Nits
4. useRef(createKeyedSerializer()) re-allocates a serializer on every render (the init expression runs each render, the result is just ignored after the first). Costless functionally, but the idiomatic lazy form is const r = useRef<X|null>(null); if (!r.current) r.current = createKeyedSerializer();. nit.
5. commitMutation's return type changed from Promise<void> to Promise<T> (generic from the serializer). All current callers void it, so neutral, but means anyone newly awaiting commitMutation gets the inner return value rather than nothing. Type-level only.
Questions
6. PR-body recency claim. Body says fix is "per-file at the chokepoint" but the title still reads "per-animationId GSAP meta commits". Worth updating the title before merge so the stack history matches the actual landed shape (per-file). Small thing — Graphite stack diff readers tend to skim titles.
What I didn't verify
- GSAP runtime / ease-curve semantics — Miga handles HF-domain coherence.
- Whether
commitMutationSafelyswallowing rejection downstream affects observed failure semantics for shadow telemetry callers. - The other 6 PRs in the stack — only #1512 reviewed here.
— Rames D Jusso
miga-heygen
left a comment
There was a problem hiding this comment.
Approve. The createKeyedSerializer is clean, correct, and well-tested.
The promise-chain design is sound: per-key serialization prevents the read-modify-write race without blocking commits to different files. Rejection safety via prior.then(task, task) ensures a failed commit doesn't wedge the chain. The Map cleanup prevents unbounded growth. The useRef placement ensures the serializer survives React re-renders.
The broadening from per-animationId to per-file serialization (default key gsap-file:${file}) is the right call — the server processes same-file writes serially anyway, so this matches the actual constraint.
Nit: the serializeKey option on CommitMutationOptions is currently unused (no caller passes it). Fine as a future extension point, but a brief comment noting it's reserved for finer/coarser granularity would be clearer than the current JSDoc.
4 well-targeted unit tests cover ordering, isolation, rejection safety, and value propagation.
— Miga
e612d9d to
77608ce
Compare
4d9e9a8 to
f33a974
Compare
77608ce to
5c94fcc
Compare
f33a974 to
ebca753
Compare
5c94fcc to
8be6f33
Compare
…ace) Rapid GSAP edits (ease/duration/keyframe/property) fired overlapping read-modify-write POSTs to one script file — coalesceKey only dedupes edit history, not requests. The gsap_fidelity shadow then diffed an op against whichever POST's scriptText resolved, which could predate that op → false "expected null, actual power2.out" mismatches. Server persists correctly; a pure client request-pairing race. Adds createKeyedSerializer (per-key promise chain, rejection-safe, self- cleaning). commitMutation now serializes every GSAP-script commit per target file by default (key `gsap-file:<path>`) — covering all op types and all animations, not just one meta family — so same-file POSTs can't interleave. Distinct files run concurrently; an explicit serializeKey still overrides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8be6f33 to
cc055f3
Compare
ebca753 to
168bb4f
Compare

What
Rapid GSAP ease changes fired overlapping read-modify-write POSTs for one file (
updateGsapMetahas no request batching —coalesceKeyonly dedupes edit history). Thegsap_fidelityshadow then diffed the latest op against whichever POST'sscriptTextresolved, which could predate that op's ease → falseexpected null actual "power2.out"mismatches. Server persists ease correctly; purely a client request-pairing race.Fix:
createKeyedSerializer(per-key promise chain, rejection-safe, self-cleaning) + aserializeKeycommit option.update-metacommits passserializeKey=gsap:${animationId}:metaso same-animation meta commits run in order; keyless / distinct-key commits run concurrently.Why
Found via SDK shadow telemetry: 16
gsap_fidelity:easefalse mismatches.Tests
4 cases on the serializer (same-key ordering, distinct-key concurrency, rejection-safety).
🤖 Generated with Claude Code
Code-review fixes (folded in)
commitMutationchokepoint, covering all op types + all animations on a file (not just one meta family) — closes the cross-animation file race too.