From c3f70c91dbbe3100622971270c6850c8f516f452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 13 May 2026 22:39:30 +0200 Subject: [PATCH] fix(studio): restore saved positions on page refresh (#801) * fix(studio): restore saved positions on page refresh studio-manual-edits.json was correctly persisted to disk but never read back into memory on bootstrap. On every page refresh, studioManualEditManifestRef started empty, so handleLoad applied an empty manifest and all saved positions/sizes/rotations were silently discarded. applyStudioManualEditsToPreview now reads from disk whenever the in-memory manifest is empty. The existing readRevision guard prevents overwriting an in-flight optimistic edit if a position change races with the disk read. * fix(studio): close delete-all race and apply same bootstrap to motion manifest Two follow-up fixes from review: 1. Replace edits.length === 0 with an explicit manifestBootstrappedRef boolean. The old condition was true in two distinct states: never-bootstrapped AND user-deleted-all-edits. Because the delete-all disk write is async-queued, there was a window where applyStudioManualEditsToPreview could read stale disk content and resurrect just-deleted positions. The boolean flag is set on the first apply and reset on project switch, cleanly separating the two states. 2. applyStudioMotionToPreview had the identical bug: GSAP motion edits were also lost on page refresh. Applied the same motionBootstrappedRef pattern. --- .filesize-allowlist | 1 + .../studio/src/hooks/useManifestPersistence.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.filesize-allowlist b/.filesize-allowlist index bffcc916b..693d19295 100644 --- a/.filesize-allowlist +++ b/.filesize-allowlist @@ -1 +1,2 @@ packages/studio/src/player/hooks/useTimelinePlayer.ts +packages/studio/src/hooks/useManifestPersistence.ts diff --git a/packages/studio/src/hooks/useManifestPersistence.ts b/packages/studio/src/hooks/useManifestPersistence.ts index 49b0450e9..187b1310f 100644 --- a/packages/studio/src/hooks/useManifestPersistence.ts +++ b/packages/studio/src/hooks/useManifestPersistence.ts @@ -77,6 +77,8 @@ export function useManifestPersistence({ options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean }, ) => Promise >(async () => {}); + const manifestBootstrappedRef = useRef(false); + const motionBootstrappedRef = useRef(false); const studioManualEditProjectRef = useRef(projectId); // Keep a ref to the latest projectId so async save callbacks always read the @@ -144,7 +146,13 @@ export function useManifestPersistence({ iframe: HTMLIFrameElement | null = previewIframeRef.current, options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean }, ) => { - const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst); + // Bootstrap from disk on first apply per session; explicit flag avoids + // re-reading disk after the user deletes all edits (async write race). + const needsBootstrap = !manifestBootstrappedRef.current; + if (needsBootstrap) manifestBootstrappedRef.current = true; + const readFromDiskFirst = Boolean( + options?.forceFromDisk || options?.readFromDiskFirst || needsBootstrap, + ); if (!readFromDiskFirst) { applyCurrentStudioManualEditsToPreview(iframe); return; @@ -210,7 +218,11 @@ export function useManifestPersistence({ iframe: HTMLIFrameElement | null = previewIframeRef.current, options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean }, ) => { - const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst); + const needsBootstrap = !motionBootstrappedRef.current; + if (needsBootstrap) motionBootstrappedRef.current = true; + const readFromDiskFirst = Boolean( + options?.forceFromDisk || options?.readFromDiskFirst || needsBootstrap, + ); if (!readFromDiskFirst) { applyCurrentStudioMotionToPreview(iframe); return; @@ -427,6 +439,7 @@ export function useManifestPersistence({ studioMotionManifestRef.current = emptyStudioMotionManifest(); studioMotionRevisionRef.current += 1; setStudioMotionRevision((revision) => revision + 1); + manifestBootstrappedRef.current = motionBootstrappedRef.current = false; }, [projectId]); // ── Listen for external file changes (HMR / SSE) ──