mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
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>
34 lines
1.3 KiB
TypeScript
34 lines
1.3 KiB
TypeScript
/**
|
|
* Per-key task serializer. Tasks sharing a key run strictly in order: a new
|
|
* task for a key awaits the prior task for that key before starting, so their
|
|
* effects (e.g. overlapping read-modify-write POSTs to one file) can't
|
|
* interleave. Tasks under different keys are independent and never block each
|
|
* other.
|
|
*
|
|
* Used to serialize GSAP meta-update commits per animationId so the shadow
|
|
* fidelity diff always pairs an op with the server result that includes it —
|
|
* without globally serializing unrelated commits.
|
|
*/
|
|
export function createKeyedSerializer() {
|
|
const inFlight = new Map<string, Promise<unknown>>();
|
|
|
|
return function run<T>(key: string, task: () => Promise<T>): Promise<T> {
|
|
const prior = inFlight.get(key) ?? Promise.resolve();
|
|
// Chain onto the prior task regardless of how it settled; a rejected prior
|
|
// commit must not wedge the key forever.
|
|
const next = prior.then(task, task);
|
|
inFlight.set(key, next);
|
|
// Once this task settles, drop it from the map if nothing newer replaced it,
|
|
// so completed keys don't leak.
|
|
void next.then(
|
|
() => {
|
|
if (inFlight.get(key) === next) inFlight.delete(key);
|
|
},
|
|
() => {
|
|
if (inFlight.get(key) === next) inFlight.delete(key);
|
|
},
|
|
);
|
|
return next;
|
|
};
|
|
}
|