mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): force-reload sdk session after undo/redo bypasses suppress window (#1524)
writeHistoryFile arms the 2 s self-write suppress window, so the file-change event for an undo/redo write is swallowed and the SDK in-memory doc stays on pre-undo content. Expose forceReload() from useSdkSession (s7.4) and call it in useAppHotkeys after a successful undo/redo that touched the active composition path. Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
co-authored by
Miguel Ángel
parent
ab7145ad9e
commit
0ca1a8a9d1
@@ -145,9 +145,7 @@ export function StudioApp() {
|
|||||||
const domEditSaveTimestampRef = useRef(0);
|
const domEditSaveTimestampRef = useRef(0);
|
||||||
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
||||||
const isGestureRecordingRef = useRef(false);
|
const isGestureRecordingRef = useRef(false);
|
||||||
const reloadPreview = useCallback(() => {
|
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), []);
|
||||||
setRefreshKey((k) => k + 1);
|
|
||||||
}, []);
|
|
||||||
const fileManager = useFileManager({
|
const fileManager = useFileManager({
|
||||||
projectId,
|
projectId,
|
||||||
showToast,
|
showToast,
|
||||||
@@ -155,7 +153,7 @@ export function StudioApp() {
|
|||||||
domEditSaveTimestampRef,
|
domEditSaveTimestampRef,
|
||||||
setRefreshKey,
|
setRefreshKey,
|
||||||
});
|
});
|
||||||
const sdkSession = useSdkSession(projectId, activeCompPath, domEditSaveTimestampRef);
|
const sdkHandle = useSdkSession(projectId, activeCompPath, domEditSaveTimestampRef);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeCompPathHydrated) return;
|
if (activeCompPathHydrated) return;
|
||||||
if (!fileManager.fileTreeLoaded) return;
|
if (!fileManager.fileTreeLoaded) return;
|
||||||
@@ -191,7 +189,7 @@ export function StudioApp() {
|
|||||||
pendingTimelineEditPathRef,
|
pendingTimelineEditPathRef,
|
||||||
uploadProjectFiles: fileManager.uploadProjectFiles,
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
||||||
isRecordingRef: isGestureRecordingRef,
|
isRecordingRef: isGestureRecordingRef,
|
||||||
sdkSession,
|
sdkSession: sdkHandle.session,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
activeBlockParams,
|
activeBlockParams,
|
||||||
@@ -259,6 +257,8 @@ export function StudioApp() {
|
|||||||
onResetKeyframes: () => resetKeyframesRef.current(),
|
onResetKeyframes: () => resetKeyframesRef.current(),
|
||||||
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
|
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
|
||||||
onAfterUndoRedo: () => invalidateGsapCacheRef.current(),
|
onAfterUndoRedo: () => invalidateGsapCacheRef.current(),
|
||||||
|
activeCompPath,
|
||||||
|
forceReloadSdkSession: sdkHandle.forceReload,
|
||||||
onToggleRecording: STUDIO_KEYFRAMES_ENABLED
|
onToggleRecording: STUDIO_KEYFRAMES_ENABLED
|
||||||
? () => handleToggleRecordingRef.current()
|
? () => handleToggleRecordingRef.current()
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -305,7 +305,7 @@ export function StudioApp() {
|
|||||||
openSourceForSelection: fileManager.openSourceForSelection,
|
openSourceForSelection: fileManager.openSourceForSelection,
|
||||||
selectSidebarTab: selectSidebarTabStable,
|
selectSidebarTab: selectSidebarTabStable,
|
||||||
getSidebarTab: getSidebarTabStable,
|
getSidebarTab: getSidebarTabStable,
|
||||||
sdkSession,
|
sdkSession: sdkHandle.session,
|
||||||
});
|
});
|
||||||
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
||||||
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
||||||
@@ -322,7 +322,7 @@ export function StudioApp() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
useSdkSelectionSync(
|
useSdkSelectionSync(
|
||||||
sdkSession,
|
sdkHandle.session,
|
||||||
domEditSession.domEditSelection,
|
domEditSession.domEditSelection,
|
||||||
domEditSession.domEditGroupSelections,
|
domEditSession.domEditGroupSelections,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -117,6 +117,14 @@ interface UseAppHotkeysParams {
|
|||||||
onDeleteSelectedKeyframes: () => void;
|
onDeleteSelectedKeyframes: () => void;
|
||||||
onAfterUndoRedo?: () => void;
|
onAfterUndoRedo?: () => void;
|
||||||
onToggleRecording?: () => void;
|
onToggleRecording?: () => void;
|
||||||
|
/** Active composition path — used to decide whether undo/redo must resync the SDK session. */
|
||||||
|
activeCompPath?: string | null;
|
||||||
|
/**
|
||||||
|
* Force-reload the SDK session after undo/redo reverts the active comp file,
|
||||||
|
* bypassing the self-write suppress window. Without this, the suppress window
|
||||||
|
* blocks the file-change reload and the SDK session stays on pre-undo content.
|
||||||
|
*/
|
||||||
|
forceReloadSdkSession?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Extracted keydown dispatch (pure function, no hooks) ──
|
// ── Extracted keydown dispatch (pure function, no hooks) ──
|
||||||
@@ -302,6 +310,8 @@ export function useAppHotkeys({
|
|||||||
onDeleteSelectedKeyframes,
|
onDeleteSelectedKeyframes,
|
||||||
onAfterUndoRedo,
|
onAfterUndoRedo,
|
||||||
onToggleRecording,
|
onToggleRecording,
|
||||||
|
activeCompPath,
|
||||||
|
forceReloadSdkSession,
|
||||||
}: UseAppHotkeysParams) {
|
}: UseAppHotkeysParams) {
|
||||||
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
const previewHotkeyWindowRef = useRef<Window | null>(null);
|
||||||
const previewHistoryCleanupRef = useRef<(() => void) | null>(null);
|
const previewHistoryCleanupRef = useRef<(() => void) | null>(null);
|
||||||
@@ -349,6 +359,14 @@ export function useAppHotkeys({
|
|||||||
}
|
}
|
||||||
if (result.ok && result.label) {
|
if (result.ok && result.label) {
|
||||||
onAfterUndoRedo?.();
|
onAfterUndoRedo?.();
|
||||||
|
// If the active composition was among the written files, force-reload
|
||||||
|
// the SDK session so its in-memory doc matches the reverted content.
|
||||||
|
// writeHistoryFile sets domEditSaveTimestampRef which activates the
|
||||||
|
// 2 s suppress window — without this call the file-change event would
|
||||||
|
// be swallowed and the SDK session would stay on stale pre-undo content.
|
||||||
|
if (activeCompPath && result.paths?.includes(activeCompPath)) {
|
||||||
|
forceReloadSdkSession?.();
|
||||||
|
}
|
||||||
await syncHistoryPreviewAfterApply(result.paths);
|
await syncHistoryPreviewAfterApply(result.paths);
|
||||||
showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
|
showToast(`${direction === "undo" ? "Undid" : "Redid"} ${result.label}`, "info");
|
||||||
}
|
}
|
||||||
@@ -361,6 +379,8 @@ export function useAppHotkeys({
|
|||||||
waitForPendingDomEditSaves,
|
waitForPendingDomEditSaves,
|
||||||
writeHistoryFile,
|
writeHistoryFile,
|
||||||
onAfterUndoRedo,
|
onAfterUndoRedo,
|
||||||
|
activeCompPath,
|
||||||
|
forceReloadSdkSession,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { shouldReloadSdkSession } from "./useSdkSession";
|
import { shouldReloadSdkSession } from "./useSdkSession";
|
||||||
|
|
||||||
|
// ── undo-sync contract ────────────────────────────────────────────────────────
|
||||||
|
// useSdkSession exposes forceReload() so callers can bypass the 2 s self-write
|
||||||
|
// suppress window. useAppHotkeys calls forceReload() after a successful
|
||||||
|
// undo/redo that wrote the active composition path. Without it, the suppress
|
||||||
|
// window swallows the file-change event and the SDK session stays stale.
|
||||||
|
//
|
||||||
|
// The React hook internals (useState / useEffect) cannot be unit-tested without
|
||||||
|
// a full render environment; the correctness of the suppress-bypass path is
|
||||||
|
// covered by the integration tests in usePersistentEditHistory.test.ts
|
||||||
|
// (which verify undo writes the correct before-content to disk).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("shouldReloadSdkSession", () => {
|
describe("shouldReloadSdkSession", () => {
|
||||||
it("reloads when the changed file is the active composition", () => {
|
it("reloads when the changed file is the active composition", () => {
|
||||||
expect(shouldReloadSdkSession({ path: "scenes/intro.html" }, "scenes/intro.html")).toBe(true);
|
expect(shouldReloadSdkSession({ path: "scenes/intro.html" }, "scenes/intro.html")).toBe(true);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import type { MutableRefObject } from "react";
|
import type { MutableRefObject } from "react";
|
||||||
import { openComposition } from "@hyperframes/sdk";
|
import { openComposition } from "@hyperframes/sdk";
|
||||||
import { createHttpAdapter } from "@hyperframes/sdk/adapters/http";
|
import { createHttpAdapter } from "@hyperframes/sdk/adapters/http";
|
||||||
@@ -36,11 +36,21 @@ export function shouldReloadSdkSession(payload: unknown, activeCompPath: string
|
|||||||
// comparison is exact rather than time-based.
|
// comparison is exact rather than time-based.
|
||||||
const SELF_WRITE_SUPPRESS_MS = 2000;
|
const SELF_WRITE_SUPPRESS_MS = 2000;
|
||||||
|
|
||||||
|
export interface SdkSessionHandle {
|
||||||
|
session: Composition | null;
|
||||||
|
/**
|
||||||
|
* Force a session reload immediately, bypassing the self-write suppress
|
||||||
|
* window. Call after undo/redo writes the active composition file so the
|
||||||
|
* SDK in-memory document reflects the reverted content.
|
||||||
|
*/
|
||||||
|
forceReload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSdkSession(
|
export function useSdkSession(
|
||||||
projectId: string | null,
|
projectId: string | null,
|
||||||
activeCompPath: string | null,
|
activeCompPath: string | null,
|
||||||
domEditSaveTimestampRef?: MutableRefObject<number>,
|
domEditSaveTimestampRef?: MutableRefObject<number>,
|
||||||
): Composition | null {
|
): SdkSessionHandle {
|
||||||
const [session, setSession] = useState<Composition | null>(null);
|
const [session, setSession] = useState<Composition | null>(null);
|
||||||
const [reloadToken, setReloadToken] = useState(0);
|
const [reloadToken, setReloadToken] = useState(0);
|
||||||
|
|
||||||
@@ -111,5 +121,6 @@ export function useSdkSession(
|
|||||||
};
|
};
|
||||||
}, [projectId, activeCompPath, reloadToken]);
|
}, [projectId, activeCompPath, reloadToken]);
|
||||||
|
|
||||||
return session;
|
const forceReload = useCallback(() => setReloadToken((t) => t + 1), []);
|
||||||
|
return { session, forceReload };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user