Skip to content

Commit 23a8103

Browse files
committed
fix(studio): publish keyframe cache refresh atomically
1 parent 29d16df commit 23a8103

3 files changed

Lines changed: 99 additions & 14 deletions

File tree

packages/studio/src/hooks/gsapKeyframeCacheHelpers.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
clearKeyframeCacheForElement,
66
clearKeyframeCacheForFile,
77
pruneKeyframeCacheToFiles,
8+
replaceKeyframeCacheForFile,
89
updateKeyframeCacheFromParsed,
910
} from "./gsapKeyframeCacheHelpers";
1011

@@ -154,6 +155,61 @@ describe("pruneKeyframeCacheToFiles", () => {
154155
});
155156
});
156157

158+
describe("replaceKeyframeCacheForFile", () => {
159+
it("publishes complete keyframe and animation maps in one notification", () => {
160+
const staleEntry = entry();
161+
const otherEntry = entry();
162+
const staleAnimation = animWithKeyframes("stale");
163+
const freshAnimation = animWithKeyframes("fresh");
164+
usePlayerStore.setState({
165+
keyframeCache: new Map([
166+
["scene.html#stale", staleEntry],
167+
["index.html#stale", staleEntry],
168+
["stale", staleEntry],
169+
["other.html#other", otherEntry],
170+
["other", otherEntry],
171+
]),
172+
gsapAnimations: new Map([
173+
["scene.html#stale", [staleAnimation]],
174+
["index.html#stale", [staleAnimation]],
175+
["stale", [staleAnimation]],
176+
["other.html#other", [staleAnimation]],
177+
["other", [staleAnimation]],
178+
]),
179+
});
180+
const snapshots: Array<{ cacheKeys: string[]; animationKeys: string[] }> = [];
181+
const unsubscribe = usePlayerStore.subscribe((state) => {
182+
snapshots.push({
183+
cacheKeys: [...state.keyframeCache.keys()].sort(),
184+
animationKeys: [...state.gsapAnimations.keys()].sort(),
185+
});
186+
});
187+
188+
replaceKeyframeCacheForFile(
189+
"scene.html",
190+
new Map([["fresh", entry()]]),
191+
new Map([["fresh", [freshAnimation]]]),
192+
);
193+
unsubscribe();
194+
195+
expect(snapshots).toEqual([
196+
{
197+
cacheKeys: ["fresh", "index.html#fresh", "other", "other.html#other", "scene.html#fresh"],
198+
animationKeys: [
199+
"fresh",
200+
"index.html#fresh",
201+
"other",
202+
"other.html#other",
203+
"scene.html#fresh",
204+
],
205+
},
206+
]);
207+
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#fresh")).toEqual([
208+
freshAnimation,
209+
]);
210+
});
211+
});
212+
157213
describe("updateKeyframeCacheFromParsed", () => {
158214
it("records colliding animation targets with their own tween percentages", () => {
159215
const animation = (

packages/studio/src/hooks/gsapKeyframeCacheHelpers.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,24 @@ export function clearKeyframeCacheForElement(sourceFile: string, elementId: stri
117117
*/
118118
export function clearKeyframeCacheForFile(sourceFile: string): void {
119119
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
120+
const ids = cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations);
121+
for (const id of ids) {
122+
clearKeyframeCacheForElement(sourceFile, id);
123+
}
124+
}
125+
126+
function cachedElementIdsForFile(
127+
sourceFile: string,
128+
keyframeCache: ReadonlyMap<string, KeyframeCacheEntry>,
129+
gsapAnimations: ReadonlyMap<string, GsapAnimation[]>,
130+
): Set<string> {
120131
const sfPrefix = `${sourceFile}#`;
121132
const ids = new Set<string>();
122133
for (const key of [...keyframeCache.keys(), ...gsapAnimations.keys()]) {
123134
if (!key.startsWith(sfPrefix)) continue;
124135
ids.add(key.slice(sfPrefix.length));
125136
}
126-
for (const id of ids) {
127-
clearKeyframeCacheForElement(sourceFile, id);
128-
}
137+
return ids;
129138
}
130139

131140
/**
@@ -177,6 +186,35 @@ export function elementCacheKeys(sourceFile: string, elementId: string): string[
177186
: [`${sourceFile}#${elementId}`, `index.html#${elementId}`, elementId];
178187
}
179188

189+
/** Replace one file's complete cache snapshot with one atomic store publish. */
190+
export function replaceKeyframeCacheForFile(
191+
sourceFile: string,
192+
entries: ReadonlyMap<string, KeyframeCacheEntry>,
193+
animationsByElement: ReadonlyMap<string, GsapAnimation[]>,
194+
): void {
195+
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
196+
const nextKeyframeCache = new Map(keyframeCache);
197+
const nextGsapAnimations = new Map(gsapAnimations);
198+
for (const id of cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations)) {
199+
for (const key of elementCacheKeys(sourceFile, id)) {
200+
nextKeyframeCache.delete(key);
201+
nextGsapAnimations.delete(key);
202+
}
203+
}
204+
for (const [id, entry] of entries) {
205+
const animations = animationsByElement.get(id);
206+
for (const key of elementCacheKeys(sourceFile, id)) {
207+
nextKeyframeCache.set(key, entry);
208+
if (animations) nextGsapAnimations.set(key, animations);
209+
else nextGsapAnimations.delete(key);
210+
}
211+
}
212+
usePlayerStore.setState({
213+
keyframeCache: nextKeyframeCache,
214+
gsapAnimations: nextGsapAnimations,
215+
});
216+
}
217+
180218
export function writeGsapAnimationsForElement(
181219
sourceFile: string,
182220
elementId: string,

packages/studio/src/hooks/keyframeCacheAstLoad.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66
import type { GsapAnimation, GsapKeyframesData, ParsedGsap } from "@hyperframes/core/gsap-parser";
77
import { isStudioHoldSet } from "@hyperframes/core/gsap-parser";
88
import { usePlayerStore } from "../player/store/playerStore";
9-
import {
10-
clearKeyframeCacheForFile,
11-
elementCacheKeys,
12-
writeGsapAnimationsForElement,
13-
} from "./gsapKeyframeCacheHelpers";
9+
import { replaceKeyframeCacheForFile } from "./gsapKeyframeCacheHelpers";
1410
import { resolveClipTimingBasis, resolveSelectorElementIds, toClipKeyframes } from "./gsapShared";
1511
import {
1612
deduplicateKeyframes,
@@ -81,8 +77,6 @@ export async function populateKeyframeCacheFromAst(
8177
): Promise<void> {
8278
const parsed = await fetchParsedAnimations(projectId, sf);
8379
if (!parsed) return;
84-
const { setKeyframeCache } = usePlayerStore.getState();
85-
clearKeyframeCacheForFile(sf);
8680
const { elements, domClipChildren } = usePlayerStore.getState();
8781
const mergedByElement = new Map<string, GsapKeyframesData<MergeableKeyframe>>();
8882
const sourceByElement = new Map<string, GsapAnimation[]>();
@@ -109,8 +103,5 @@ export async function populateKeyframeCacheFromAst(
109103
}
110104
}
111105
}
112-
for (const [id, kfData] of mergedByElement) {
113-
for (const key of elementCacheKeys(sf, id)) setKeyframeCache(key, kfData);
114-
writeGsapAnimationsForElement(sf, id, sourceByElement.get(id));
115-
}
106+
replaceKeyframeCacheForFile(sf, mergedByElement, sourceByElement);
116107
}

0 commit comments

Comments
 (0)