Skip to content

Commit 17b3bdd

Browse files
committed
fix(studio): make sdk cutover transactional
1 parent a5374c5 commit 17b3bdd

23 files changed

Lines changed: 890 additions & 334 deletions

packages/studio/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export function StudioApp() {
184184
uploadProjectFiles: fileManager.uploadProjectFiles,
185185
isRecordingRef: isGestureRecordingRef,
186186
sdkSession: editFlowSdkSession,
187+
publishSdkSession: sdkHandle.publish,
187188
forceReloadSdkSession: sdkHandle.forceReload,
188189
handleDomZIndexReorderCommitRef,
189190
});
@@ -301,6 +302,7 @@ export function StudioApp() {
301302
selectSidebarTab: sidebarTabRef.current.select,
302303
getSidebarTab: sidebarTabRef.current.get,
303304
sdkSession: editFlowSdkSession,
305+
publishSdkSession: sdkHandle.publish,
304306
forceReloadSdkSession: sdkHandle.forceReload,
305307
});
306308
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
@@ -559,6 +561,7 @@ export function StudioApp() {
559561
recordingDuration={gestureRecording.recordingDuration}
560562
onToggleRecording={recordingToggle}
561563
sdkSession={sdkHandle.session}
564+
publishSdkSession={sdkHandle.publish}
562565
reloadPreview={reloadPreview}
563566
domEditSaveTimestampRef={domEditSaveTimestampRef}
564567
recordEdit={editHistory.recordEdit}

packages/studio/src/components/DesignPanelPromoteProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function DesignPanelPromoteProvider({
3535
const persist = useVariablesPersist({
3636
...persistDeps,
3737
sdkSession: handle.session,
38+
publishSdkSession: handle.publish,
3839
activeCompPath: targetPath,
3940
});
4041
return (

packages/studio/src/components/StudioRightPanel.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface StudioRightPanelProps {
5656
onToggleRecording?: () => void;
5757
/** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
5858
sdkSession: Composition | null;
59+
publishSdkSession: (session: Composition) => void;
5960
reloadPreview: () => void;
6061
domEditSaveTimestampRef: MutableRefObject<number>;
6162
recordEdit: (entry: {
@@ -75,6 +76,7 @@ export function StudioRightPanel({
7576
recordingDuration,
7677
onToggleRecording,
7778
sdkSession,
79+
publishSdkSession,
7880
reloadPreview,
7981
domEditSaveTimestampRef,
8082
recordEdit,
@@ -167,6 +169,7 @@ export function StudioRightPanel({
167169
recordEdit,
168170
reloadPreview,
169171
domEditSaveTimestampRef,
172+
publishSdkSession,
170173
});
171174

172175
// Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures
@@ -179,6 +182,7 @@ export function StudioRightPanel({
179182
recordEdit,
180183
reloadPreview,
181184
domEditSaveTimestampRef,
185+
publishSdkSession,
182186
coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
183187
});
184188

@@ -529,6 +533,7 @@ export function StudioRightPanel({
529533
) : rightPanelTab === "variables" ? (
530534
<VariablesPanel
531535
sdkSession={sdkSession}
536+
publishSdkSession={publishSdkSession}
532537
reloadPreview={reloadPreview}
533538
domEditSaveTimestampRef={domEditSaveTimestampRef}
534539
recordEdit={recordEdit}

packages/studio/src/components/panels/VariablesPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function shellSingleQuote(value: string): string {
3232

3333
interface VariablesPanelProps {
3434
sdkSession: Composition | null;
35+
publishSdkSession: (session: Composition) => void;
3536
reloadPreview: () => void;
3637
domEditSaveTimestampRef: MutableRefObject<number>;
3738
recordEdit: (entry: {
@@ -247,6 +248,7 @@ const EMPTY_STATE = (
247248
// fallow-ignore-next-line complexity
248249
export const VariablesPanel = memo(function VariablesPanel({
249250
sdkSession,
251+
publishSdkSession,
250252
reloadPreview,
251253
domEditSaveTimestampRef,
252254
recordEdit,
@@ -285,6 +287,7 @@ export const VariablesPanel = memo(function VariablesPanel({
285287
recordEdit,
286288
reloadPreview,
287289
domEditSaveTimestampRef,
290+
publishSdkSession,
288291
});
289292

290293
const declarations = useMemo(

packages/studio/src/hooks/gsapScriptCommitTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export interface GsapScriptCommitsParams {
7676
showToast: (message: string, tone?: "error" | "info") => void;
7777
/** Stage 7 §3.5: SDK session for routing GSAP tween ops through addGsapTween/setGsapTween/removeGsapTween. */
7878
sdkSession?: Composition | null;
79+
/** Publish a fully persisted candidate SDK session. */
80+
publishSdkSession?: (session: Composition) => void;
7981
writeProjectFile?: (path: string, content: string) => Promise<void>;
8082
/** Resync the in-memory SDK session after a server-authoritative write. */
8183
forceReloadSdkSession?: () => void;

packages/studio/src/hooks/useDomEditCommits.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useDomEditTextCommits } from "./useDomEditTextCommits";
2020
import { useDomGeometryCommits } from "./useDomGeometryCommits";
2121
import { useElementLifecycleOps } from "./useElementLifecycleOps";
2222
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
23+
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
2324

2425
// ── Helpers ──
2526

@@ -81,16 +82,20 @@ export interface UseDomEditCommitsParams {
8182
* path, whose session is already current) so a later SDK edit doesn't
8283
* serialize the pre-write doc and revert the server's change. */
8384
forceReloadSdkSession?: () => void;
84-
/** Stage 7 Step 3c: called before the server-side patch path; returns true if SDK handled it. */
85+
/** Stage 7 Step 3c: called before the server-side patch path. */
8586
onTrySdkPersist?: (
8687
selection: DomEditSelection,
8788
operations: PatchOperation[],
8889
originalContent: string,
8990
targetPath: string,
9091
options?: { label?: string; coalesceKey?: string; skipRefresh?: boolean },
91-
) => Promise<boolean>;
92-
/** Stage 7 §3.1: called before the server-side delete path; returns true if SDK handled it. */
93-
onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
92+
) => Promise<CutoverResult>;
93+
/** Stage 7 §3.1: called before the server-side delete path. */
94+
onTrySdkDelete?: (
95+
hfId: string,
96+
originalContent: string,
97+
targetPath: string,
98+
) => Promise<CutoverResult>;
9499
/** Resolver-shadow tripwire for z-index reorder targets (telemetry-only, decoupled from cutover). */
95100
onReorderShadow?: (targets: string[]) => void;
96101
}
@@ -185,18 +190,17 @@ export function useDomEditCommits({
185190
// Skip the SDK path when prepareContent is set (e.g. @font-face injection
186191
// for a custom font): sdkCutoverPersist serializes only the patched DOM
187192
// and would drop the injected content. Let the server path run prepareContent.
188-
if (
189-
onTrySdkPersist &&
190-
!options?.prepareContent &&
191-
(await onTrySdkPersist(selection, operations, originalContent, targetPath, {
193+
if (onTrySdkPersist && !options?.prepareContent) {
194+
const cutover = await onTrySdkPersist(selection, operations, originalContent, targetPath, {
192195
label: options?.label,
193196
coalesceKey: options?.coalesceKey,
194197
skipRefresh: options?.skipRefresh,
195-
}))
196-
) {
197-
// SDK handled it — its in-memory doc is already current, so do NOT
198-
// forceReload (that would echo-reload the session we just wrote).
199-
return;
198+
});
199+
if (cutoverCommittedOrThrow(cutover)) {
200+
// SDK handled it — its in-memory doc is already current, so do NOT
201+
// forceReload (that would echo-reload the session we just wrote).
202+
return;
203+
}
200204
}
201205

202206
// Mark the save timestamp before the file write so the SSE file-change

packages/studio/src/hooks/useDomEditSession.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface UseDomEditSessionParams {
6767
selectSidebarTab?: (tab: SidebarTab) => void;
6868
getSidebarTab?: () => SidebarTab;
6969
sdkSession?: Composition | null;
70+
publishSdkSession?: (session: Composition) => void;
7071
forceReloadSdkSession?: () => void;
7172
}
7273

@@ -108,6 +109,7 @@ export function useDomEditSession({
108109
selectSidebarTab,
109110
getSidebarTab,
110111
sdkSession,
112+
publishSdkSession,
111113
forceReloadSdkSession,
112114
}: UseDomEditSessionParams) {
113115
void _setRefreshKey;
@@ -217,6 +219,7 @@ export function useDomEditSession({
217219
onFileContentChanged: updateEditingFileContent,
218220
showToast,
219221
sdkSession,
222+
publishSdkSession,
220223
writeProjectFile,
221224
forceReloadSdkSession,
222225
});
@@ -274,6 +277,8 @@ export function useDomEditSession({
274277
reloadPreview,
275278
domEditSaveTimestampRef,
276279
compositionPath: activeCompPath,
280+
readProjectFile,
281+
publishSession: publishSdkSession,
277282
},
278283
options,
279284
);
@@ -287,6 +292,8 @@ export function useDomEditSession({
287292
reloadPreview,
288293
domEditSaveTimestampRef,
289294
compositionPath: activeCompPath,
295+
readProjectFile,
296+
publishSession: publishSdkSession,
290297
})
291298
: undefined,
292299
// Resolver shadow for the z-index reorder edit: it takes the server path (no

packages/studio/src/hooks/useElementLifecycleOps.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../components/editor/domEditing";
1010
import type { PatchOperation } from "../utils/sourcePatcher";
1111
import type { EditHistoryKind } from "../utils/editHistory";
12+
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
1213

1314
interface RecordEditInput {
1415
label: string;
@@ -26,8 +27,12 @@ interface UseElementLifecycleOpsParams {
2627
projectIdRef: React.MutableRefObject<string | null>;
2728
reloadPreview: () => void;
2829
clearDomSelection: () => void;
29-
/** Route delete through SDK when session resolves the hf-id; returns true if handled. */
30-
onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
30+
/** Route delete through SDK when session resolves the hf-id. */
31+
onTrySdkDelete?: (
32+
hfId: string,
33+
originalContent: string,
34+
targetPath: string,
35+
) => Promise<CutoverResult>;
3136
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
3237
onReorderShadow?: (targets: string[]) => void;
3338
/** Resync the SDK session after a server-fallback delete. */
@@ -85,7 +90,7 @@ export function useElementLifecycleOps({
8590

8691
if (onTrySdkDelete && selection.hfId) {
8792
const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
88-
if (handled) {
93+
if (cutoverCommittedOrThrow(handled)) {
8994
clearDomSelection();
9095
usePlayerStore.getState().setSelectedElementId(null);
9196
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");

packages/studio/src/hooks/useGsapAnimationOps.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
sdkGsapDeleteAllForSelectorPersist,
88
sdkAddWithKeyframesPersist,
99
sdkReplaceWithKeyframesPersist,
10+
cutoverCommittedOrThrow,
1011
type CutoverDeps,
1112
} from "../utils/sdkCutover";
1213
import {
@@ -52,7 +53,7 @@ export function useGsapAnimationOps({
5253
sdkDeps,
5354
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
5455
);
55-
if (handled) return;
56+
if (cutoverCommittedOrThrow(handled)) return;
5657
}
5758
commitMutationSafely(
5859
selection,
@@ -74,7 +75,7 @@ export function useGsapAnimationOps({
7475
sdkDeps,
7576
{ label: "Delete GSAP animation" },
7677
);
77-
if (handled) return;
78+
if (cutoverCommittedOrThrow(handled)) return;
7879
}
7980
commitMutationSafely(
8081
selection,
@@ -96,7 +97,7 @@ export function useGsapAnimationOps({
9697
sdkDeps,
9798
{ label: "Delete all animations for element" },
9899
);
99-
if (handled) return;
100+
if (cutoverCommittedOrThrow(handled)) return;
100101
}
101102
void commitMutation(
102103
selection,
@@ -162,7 +163,7 @@ export function useGsapAnimationOps({
162163
sdkDeps,
163164
{ label: `Add GSAP ${method} animation` },
164165
);
165-
if (handled) return;
166+
if (cutoverCommittedOrThrow(handled)) return;
166167
}
167168

168169
await commitMutation(
@@ -213,7 +214,7 @@ export function useGsapAnimationOps({
213214
sdkDeps,
214215
{ label },
215216
);
216-
if (handled) return;
217+
if (cutoverCommittedOrThrow(handled)) return;
217218
}
218219
void commitMutation(
219220
selection,
@@ -256,7 +257,7 @@ export function useGsapAnimationOps({
256257
sdkDeps,
257258
{ label },
258259
);
259-
if (handled) return;
260+
if (cutoverCommittedOrThrow(handled)) return;
260261
}
261262
void commitMutation(
262263
selection,

packages/studio/src/hooks/useGsapKeyframeOps.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
sdkGsapRemoveKeyframePersist,
99
sdkGsapRemoveAllKeyframesPersist,
1010
sdkGsapConvertToKeyframesPersist,
11+
cutoverCommittedOrThrow,
1112
type CutoverDeps,
1213
} from "../utils/sdkCutover";
1314
import type { KeyframeCacheEntry } from "../player/store/playerStore";
@@ -120,7 +121,7 @@ export function useGsapKeyframeOps({
120121
coalesceKey: `gsap:${animationId}:kf:${percentage}`,
121122
},
122123
);
123-
if (handled) return;
124+
if (cutoverCommittedOrThrow(handled)) return;
124125
}
125126
await commitMutation(selection, mutation, {
126127
label: `Add keyframe at ${percentage}%`,
@@ -152,7 +153,7 @@ export function useGsapKeyframeOps({
152153
sdkDeps,
153154
{ label: `Add keyframe at ${percentage}%` },
154155
);
155-
if (handled) return;
156+
if (cutoverCommittedOrThrow(handled)) return;
156157
}
157158
return commitMutation(
158159
selection,
@@ -191,7 +192,7 @@ export function useGsapKeyframeOps({
191192
sdkDeps,
192193
{ label: `Remove keyframe at ${percentage}%` },
193194
);
194-
if (handled) return;
195+
if (cutoverCommittedOrThrow(handled)) return;
195196
}
196197
await commitMutation(selection, mutation, {
197198
label: `Remove keyframe at ${percentage}%`,
@@ -272,7 +273,7 @@ export function useGsapKeyframeOps({
272273
sdkDeps,
273274
{ label: "Convert to keyframes" },
274275
);
275-
if (handled) return;
276+
if (cutoverCommittedOrThrow(handled)) return;
276277
}
277278
return commitMutation(
278279
selection,
@@ -301,7 +302,7 @@ export function useGsapKeyframeOps({
301302
sdkDeps,
302303
{ label: "Remove all keyframes" },
303304
);
304-
if (handled) return;
305+
if (cutoverCommittedOrThrow(handled)) return;
305306
}
306307
commitMutationSafely(
307308
selection,

0 commit comments

Comments
 (0)