Skip to content

Commit 37392fe

Browse files
committed
fix(studio): publish keyframe cache refresh atomically
1 parent 4802f0e commit 37392fe

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
@@ -102,15 +102,24 @@ export function clearKeyframeCacheForElement(sourceFile: string, elementId: stri
102102
*/
103103
export function clearKeyframeCacheForFile(sourceFile: string): void {
104104
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
105+
const ids = cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations);
106+
for (const id of ids) {
107+
clearKeyframeCacheForElement(sourceFile, id);
108+
}
109+
}
110+
111+
function cachedElementIdsForFile(
112+
sourceFile: string,
113+
keyframeCache: ReadonlyMap<string, KeyframeCacheEntry>,
114+
gsapAnimations: ReadonlyMap<string, GsapAnimation[]>,
115+
): Set<string> {
105116
const sfPrefix = `${sourceFile}#`;
106117
const ids = new Set<string>();
107118
for (const key of [...keyframeCache.keys(), ...gsapAnimations.keys()]) {
108119
if (!key.startsWith(sfPrefix)) continue;
109120
ids.add(key.slice(sfPrefix.length));
110121
}
111-
for (const id of ids) {
112-
clearKeyframeCacheForElement(sourceFile, id);
113-
}
122+
return ids;
114123
}
115124

116125
/**
@@ -148,6 +157,35 @@ export function elementCacheKeys(sourceFile: string, elementId: string): string[
148157
: [`${sourceFile}#${elementId}`, `index.html#${elementId}`, elementId];
149158
}
150159

160+
/** Replace one file's complete cache snapshot with one atomic store publish. */
161+
export function replaceKeyframeCacheForFile(
162+
sourceFile: string,
163+
entries: ReadonlyMap<string, KeyframeCacheEntry>,
164+
animationsByElement: ReadonlyMap<string, GsapAnimation[]>,
165+
): void {
166+
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
167+
const nextKeyframeCache = new Map(keyframeCache);
168+
const nextGsapAnimations = new Map(gsapAnimations);
169+
for (const id of cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations)) {
170+
for (const key of elementCacheKeys(sourceFile, id)) {
171+
nextKeyframeCache.delete(key);
172+
nextGsapAnimations.delete(key);
173+
}
174+
}
175+
for (const [id, entry] of entries) {
176+
const animations = animationsByElement.get(id);
177+
for (const key of elementCacheKeys(sourceFile, id)) {
178+
nextKeyframeCache.set(key, entry);
179+
if (animations) nextGsapAnimations.set(key, animations);
180+
else nextGsapAnimations.delete(key);
181+
}
182+
}
183+
usePlayerStore.setState({
184+
keyframeCache: nextKeyframeCache,
185+
gsapAnimations: nextGsapAnimations,
186+
});
187+
}
188+
151189
export function writeGsapAnimationsForElement(
152190
sourceFile: string,
153191
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 { idFromSelector, toClipKeyframes } from "./gsapShared";
1511
import {
1612
deduplicateKeyframes,
@@ -146,8 +142,6 @@ export async function populateKeyframeCacheFromAst(
146142
): Promise<void> {
147143
const parsed = await fetchParsedAnimations(projectId, sf);
148144
if (!parsed) return;
149-
const { setKeyframeCache } = usePlayerStore.getState();
150-
clearKeyframeCacheForFile(sf);
151145
const { elements, domClipChildren } = usePlayerStore.getState();
152146
const mergedByElement = new Map<string, GsapKeyframesData>();
153147
const sourceByElement = new Map<string, GsapAnimation[]>();
@@ -174,8 +168,5 @@ export async function populateKeyframeCacheFromAst(
174168
}
175169
}
176170
}
177-
for (const [id, kfData] of mergedByElement) {
178-
for (const key of elementCacheKeys(sf, id)) setKeyframeCache(key, kfData);
179-
writeGsapAnimationsForElement(sf, id, sourceByElement.get(id));
180-
}
171+
replaceKeyframeCacheForFile(sf, mergedByElement, sourceByElement);
181172
}

0 commit comments

Comments
 (0)