Skip to content

Commit fa1bf97

Browse files
committed
fix(studio): publish keyframe cache refresh atomically
1 parent 0741937 commit fa1bf97

3 files changed

Lines changed: 99 additions & 12 deletions

File tree

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerS
44
import {
55
clearKeyframeCacheForElement,
66
clearKeyframeCacheForFile,
7+
replaceKeyframeCacheForFile,
78
updateKeyframeCacheFromParsed,
89
} from "./gsapKeyframeCacheHelpers";
910

@@ -97,6 +98,61 @@ describe("clearKeyframeCacheForFile", () => {
9798
});
9899
});
99100

101+
describe("replaceKeyframeCacheForFile", () => {
102+
it("publishes complete keyframe and animation maps in one notification", () => {
103+
const staleEntry = entry();
104+
const otherEntry = entry();
105+
const staleAnimation = animWithKeyframes("stale");
106+
const freshAnimation = animWithKeyframes("fresh");
107+
usePlayerStore.setState({
108+
keyframeCache: new Map([
109+
["scene.html#stale", staleEntry],
110+
["index.html#stale", staleEntry],
111+
["stale", staleEntry],
112+
["other.html#other", otherEntry],
113+
["other", otherEntry],
114+
]),
115+
gsapAnimations: new Map([
116+
["scene.html#stale", [staleAnimation]],
117+
["index.html#stale", [staleAnimation]],
118+
["stale", [staleAnimation]],
119+
["other.html#other", [staleAnimation]],
120+
["other", [staleAnimation]],
121+
]),
122+
});
123+
const snapshots: Array<{ cacheKeys: string[]; animationKeys: string[] }> = [];
124+
const unsubscribe = usePlayerStore.subscribe((state) => {
125+
snapshots.push({
126+
cacheKeys: [...state.keyframeCache.keys()].sort(),
127+
animationKeys: [...state.gsapAnimations.keys()].sort(),
128+
});
129+
});
130+
131+
replaceKeyframeCacheForFile(
132+
"scene.html",
133+
new Map([["fresh", entry()]]),
134+
new Map([["fresh", [freshAnimation]]]),
135+
);
136+
unsubscribe();
137+
138+
expect(snapshots).toEqual([
139+
{
140+
cacheKeys: ["fresh", "index.html#fresh", "other", "other.html#other", "scene.html#fresh"],
141+
animationKeys: [
142+
"fresh",
143+
"index.html#fresh",
144+
"other",
145+
"other.html#other",
146+
"scene.html#fresh",
147+
],
148+
},
149+
]);
150+
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#fresh")).toEqual([
151+
freshAnimation,
152+
]);
153+
});
154+
});
155+
100156
describe("updateKeyframeCacheFromParsed", () => {
101157
it("records colliding animation targets with their own tween percentages", () => {
102158
const animation = (

packages/studio/src/hooks/gsapKeyframeCacheHelpers.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ export function clearKeyframeCacheForElement(sourceFile: string, elementId: stri
119119
*/
120120
export function clearKeyframeCacheForFile(sourceFile: string): void {
121121
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
122+
const ids = cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations);
123+
for (const id of ids) {
124+
clearKeyframeCacheForElement(sourceFile, id);
125+
}
126+
}
127+
128+
function cachedElementIdsForFile(
129+
sourceFile: string,
130+
keyframeCache: ReadonlyMap<string, KeyframeCacheEntry>,
131+
gsapAnimations: ReadonlyMap<string, GsapAnimation[]>,
132+
): Set<string> {
122133
const sfPrefix = `${sourceFile}#`;
123134
const fallbackPrefix = "index.html#";
124135
const ids = new Set<string>();
@@ -129,9 +140,7 @@ export function clearKeyframeCacheForFile(sourceFile: string): void {
129140
const hashIdx = key.indexOf("#");
130141
if (hashIdx !== -1) ids.add(key.slice(hashIdx + 1));
131142
}
132-
for (const id of ids) {
133-
clearKeyframeCacheForElement(sourceFile, id);
134-
}
143+
return ids;
135144
}
136145

137146
function elementCacheKeys(sourceFile: string, elementId: string): string[] {
@@ -140,6 +149,35 @@ function elementCacheKeys(sourceFile: string, elementId: string): string[] {
140149
: [`${sourceFile}#${elementId}`, `index.html#${elementId}`, elementId];
141150
}
142151

152+
/** Replace one file's complete cache snapshot with one atomic store publish. */
153+
export function replaceKeyframeCacheForFile(
154+
sourceFile: string,
155+
entries: ReadonlyMap<string, KeyframeCacheEntry>,
156+
animationsByElement: ReadonlyMap<string, GsapAnimation[]>,
157+
): void {
158+
const { keyframeCache, gsapAnimations } = usePlayerStore.getState();
159+
const nextKeyframeCache = new Map(keyframeCache);
160+
const nextGsapAnimations = new Map(gsapAnimations);
161+
for (const id of cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations)) {
162+
for (const key of elementCacheKeys(sourceFile, id)) {
163+
nextKeyframeCache.delete(key);
164+
nextGsapAnimations.delete(key);
165+
}
166+
}
167+
for (const [id, entry] of entries) {
168+
const animations = animationsByElement.get(id);
169+
for (const key of elementCacheKeys(sourceFile, id)) {
170+
nextKeyframeCache.set(key, entry);
171+
if (animations) nextGsapAnimations.set(key, animations);
172+
else nextGsapAnimations.delete(key);
173+
}
174+
}
175+
usePlayerStore.setState({
176+
keyframeCache: nextKeyframeCache,
177+
gsapAnimations: nextGsapAnimations,
178+
});
179+
}
180+
143181
export function writeGsapAnimationsForElement(
144182
sourceFile: string,
145183
elementId: string,

packages/studio/src/hooks/useGsapTweenCache.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { usePlayerStore } from "../player/store/playerStore";
55
import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeBridge";
66
import {
77
clearKeyframeCacheForElement,
8-
clearKeyframeCacheForFile,
8+
replaceKeyframeCacheForFile,
99
writeGsapAnimationsForElement,
1010
} from "./gsapKeyframeCacheHelpers";
1111
import { toAbsoluteTime } from "./gsapShared";
@@ -466,8 +466,6 @@ export function usePopulateKeyframeCacheForFile(
466466
// fallow-ignore-next-line complexity
467467
fetchParsedAnimations(projectId, sf).then((parsed) => {
468468
if (!parsed) return;
469-
const { setKeyframeCache } = usePlayerStore.getState();
470-
clearKeyframeCacheForFile(sf);
471469
const { elements, domClipChildren } = usePlayerStore.getState();
472470
const doc = iframeRef?.current?.contentDocument;
473471
const mergedByElement = new Map<string, GsapKeyframesData>();
@@ -522,12 +520,7 @@ export function usePopulateKeyframeCacheForFile(
522520
}
523521
}
524522
}
525-
for (const [id, kfData] of mergedByElement) {
526-
setKeyframeCache(`${sf}#${id}`, kfData);
527-
setKeyframeCache(id, kfData);
528-
if (sf !== "index.html") setKeyframeCache(`index.html#${id}`, kfData);
529-
writeGsapAnimationsForElement(sf, id, sourceByElement.get(id));
530-
}
523+
replaceKeyframeCacheForFile(sf, mergedByElement, sourceByElement);
531524
astFetchDoneRef.current = fetchKey;
532525
});
533526
// elementCount is in the deps because new timeline elements (e.g. after a

0 commit comments

Comments
 (0)