@hyperframes/studio 0.7.58 → 0.7.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13514 -8528
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.ts +29 -175
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +17 -5
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +16 -10
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTimelineGroupEditing.ts +16 -2
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
package/src/utils/sdkCutover.ts
CHANGED
|
@@ -1,125 +1,51 @@
|
|
|
1
|
-
import type { MutableRefObject } from "react";
|
|
2
1
|
import type { Composition, GsapTweenSpec } from "@hyperframes/sdk";
|
|
3
2
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
5
3
|
import type { PatchOperation } from "./sourcePatcher";
|
|
6
4
|
import { STUDIO_SDK_CUTOVER_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
7
5
|
import { trackStudioEvent } from "./studioTelemetry";
|
|
8
|
-
import { markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
|
|
9
6
|
import { patchOpsToSdkEditOps } from "./sdkOpMapping";
|
|
10
7
|
import { recordResolverParity, recordAnimationResolverParity } from "./sdkResolverShadow";
|
|
11
8
|
import { shouldDeclineTextCutoverForTarget, shouldUseSdkCutover } from "./sdkCutoverEligibility";
|
|
9
|
+
import {
|
|
10
|
+
asCutoverError,
|
|
11
|
+
declinedCutover,
|
|
12
|
+
persistSdkCandidateMutation,
|
|
13
|
+
type CutoverDeps,
|
|
14
|
+
type CutoverOptions,
|
|
15
|
+
type CutoverResult,
|
|
16
|
+
} from "./sdkEditTransaction";
|
|
12
17
|
|
|
13
18
|
export { shouldUseSdkCutover } from "./sdkCutoverEligibility";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
26
|
-
reloadPreview: () => void;
|
|
27
|
-
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
28
|
-
/**
|
|
29
|
-
* Optional post-write refresh. When provided, it REPLACES the default
|
|
30
|
-
* reloadPreview() — the GSAP path passes one that soft-reloads (preserving
|
|
31
|
-
* the playhead) and invalidates the keyframe/gsap panel cache. Receives the
|
|
32
|
-
* serialized document just written.
|
|
33
|
-
*/
|
|
34
|
-
refresh?: (after: string) => void;
|
|
35
|
-
/**
|
|
36
|
-
* Path of the composition the SDK session was opened for. The session models
|
|
37
|
-
* ONLY this file (serialize() emits the whole active composition), so any edit
|
|
38
|
-
* whose targetPath differs (a sub-composition file) must take the server path
|
|
39
|
-
* — otherwise we'd write the full active-comp serialization into that file.
|
|
40
|
-
*/
|
|
41
|
-
compositionPath?: string | null;
|
|
42
|
-
/**
|
|
43
|
-
* Optional per-key task serializer (the same `gsap-file:${file}` serializer the
|
|
44
|
-
* legacy `commitMutation` uses). When provided, every GSAP-op persist routes its
|
|
45
|
-
* read-serialize → dispatch → serialize → write through it so two concurrent
|
|
46
|
-
* same-file flushes can't interleave their read-modify-write and lose an edit.
|
|
47
|
-
* Absent (e.g. in unit tests) → ops run unserialized as before.
|
|
48
|
-
*/
|
|
49
|
-
serialize?: <T>(key: string, task: () => Promise<T>) => Promise<T>;
|
|
50
|
-
/**
|
|
51
|
-
* Optional reader for the on-disk content of targetPath. Timing/GSAP persists
|
|
52
|
-
* use it to capture the EXACT prior bytes as the undo-history `before`, so undo
|
|
53
|
-
* restores the file verbatim instead of a normalized SDK re-emit (which would
|
|
54
|
-
* reformat the whole file). The style/delete paths already thread originalContent
|
|
55
|
-
* in explicitly; this gives timing/GSAP parity without touching every call site.
|
|
56
|
-
* Absent → falls back to the SDK's pre-edit serialize() (the prior behavior).
|
|
57
|
-
*/
|
|
58
|
-
readProjectFile?: (path: string) => Promise<string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Capture the undo-history `before` baseline for timing/GSAP persists: the exact
|
|
63
|
-
* on-disk bytes when a reader is available (so undo restores them verbatim),
|
|
64
|
-
* falling back to the SDK's pre-edit serialization when it isn't. Never throws —
|
|
65
|
-
* a failed read degrades to the serialized fallback rather than aborting the edit.
|
|
66
|
-
*/
|
|
67
|
-
async function captureOnDiskBefore(
|
|
68
|
-
deps: CutoverDeps,
|
|
69
|
-
targetPath: string,
|
|
70
|
-
serializedFallback: string,
|
|
71
|
-
): Promise<string> {
|
|
72
|
-
if (!deps.readProjectFile) return serializedFallback;
|
|
73
|
-
try {
|
|
74
|
-
return await deps.readProjectFile(targetPath);
|
|
75
|
-
} catch {
|
|
76
|
-
return serializedFallback;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
19
|
+
export {
|
|
20
|
+
cutoverCommittedOrThrow,
|
|
21
|
+
persistSdkCandidateMutation,
|
|
22
|
+
persistSdkSerialize,
|
|
23
|
+
} from "./sdkEditTransaction";
|
|
24
|
+
export type {
|
|
25
|
+
CutoverDeps,
|
|
26
|
+
CutoverOptions,
|
|
27
|
+
CutoverResult,
|
|
28
|
+
PublishSdkSession,
|
|
29
|
+
} from "./sdkEditTransaction";
|
|
79
30
|
|
|
80
31
|
/** True when targetPath isn't the composition the SDK session models. */
|
|
81
32
|
function wrongCompositionFile(deps: CutoverDeps, targetPath: string): boolean {
|
|
82
33
|
return deps.compositionPath != null && targetPath !== deps.compositionPath;
|
|
83
34
|
}
|
|
84
35
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ponytail: exported for setSlideshowManifest (third caller — island write bypasses
|
|
95
|
-
// the SDK dispatch path since <script> nodes are not in the element tree).
|
|
96
|
-
// `after` is serialized once by the caller (which also did the no-op check
|
|
97
|
-
// against its pre-dispatch snapshot), so this never re-serializes.
|
|
98
|
-
export async function persistSdkSerialize(
|
|
99
|
-
after: string,
|
|
100
|
-
targetPath: string,
|
|
101
|
-
originalContent: string,
|
|
36
|
+
/**
|
|
37
|
+
* Reader for the animation-resolver tripwire's disk-truth check: on an
|
|
38
|
+
* animationId miss it re-parses the CURRENT file to distinguish a stale
|
|
39
|
+
* session (panel ids re-derive from disk every render; session ids date from
|
|
40
|
+
* the last reload) from a genuine resolver divergence.
|
|
41
|
+
*/
|
|
42
|
+
function gsapReadSource(
|
|
102
43
|
deps: CutoverDeps,
|
|
103
|
-
|
|
104
|
-
): Promise<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// reload-suppression can recognize its own echo by IDENTITY, not just a 2 s
|
|
108
|
-
// clock — an undo write (different bytes, not registered here) then always
|
|
109
|
-
// reloads instead of being swallowed by the time window.
|
|
110
|
-
markSelfWrite(targetPath, after);
|
|
111
|
-
await deps.writeProjectFile(targetPath, after);
|
|
112
|
-
await deps.editHistory.recordEdit({
|
|
113
|
-
label: options?.label ?? "Edit layer",
|
|
114
|
-
kind: "manual",
|
|
115
|
-
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
|
116
|
-
...(options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}),
|
|
117
|
-
files: { [targetPath]: { before: originalContent, after } },
|
|
118
|
-
});
|
|
119
|
-
if (deps.refresh) deps.refresh(after);
|
|
120
|
-
else if (!options?.skipRefresh) deps.reloadPreview();
|
|
44
|
+
targetPath: string,
|
|
45
|
+
): (() => Promise<string | undefined>) | undefined {
|
|
46
|
+
const read = deps.readProjectFile;
|
|
47
|
+
return read ? () => read(targetPath) : undefined;
|
|
121
48
|
}
|
|
122
|
-
|
|
123
49
|
export async function sdkCutoverPersist(
|
|
124
50
|
selection: DomEditSelection,
|
|
125
51
|
ops: PatchOperation[],
|
|
@@ -128,35 +54,33 @@ export async function sdkCutoverPersist(
|
|
|
128
54
|
sdkSession: Composition | null | undefined,
|
|
129
55
|
deps: CutoverDeps,
|
|
130
56
|
options?: CutoverOptions,
|
|
131
|
-
): Promise<
|
|
57
|
+
): Promise<CutoverResult> {
|
|
132
58
|
if (!shouldUseSdkCutover(STUDIO_SDK_CUTOVER_ENABLED, !!sdkSession, selection.hfId, ops))
|
|
133
|
-
return
|
|
134
|
-
if (!sdkSession) return
|
|
59
|
+
return declinedCutover("ineligible_operation");
|
|
60
|
+
if (!sdkSession) return declinedCutover("session_unavailable");
|
|
135
61
|
const hfId = selection.hfId;
|
|
136
|
-
if (!hfId) return
|
|
62
|
+
if (!hfId) return declinedCutover("target_unaddressable");
|
|
137
63
|
const target = sdkSession.getElement(hfId);
|
|
138
|
-
if (!target) return
|
|
139
|
-
if (shouldDeclineTextCutoverForTarget(target, ops))
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
sdkSession
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
64
|
+
if (!target) return declinedCutover("target_not_found");
|
|
65
|
+
if (shouldDeclineTextCutoverForTarget(target, ops))
|
|
66
|
+
return declinedCutover("unsupported_text_target");
|
|
67
|
+
if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
|
|
68
|
+
const result = await persistSdkCandidateMutation(
|
|
69
|
+
sdkSession,
|
|
70
|
+
targetPath,
|
|
71
|
+
originalContent,
|
|
72
|
+
deps,
|
|
73
|
+
(session) => {
|
|
74
|
+
for (const editOp of patchOpsToSdkEditOps(hfId, ops)) session.dispatch(editOp);
|
|
75
|
+
},
|
|
76
|
+
options,
|
|
77
|
+
);
|
|
78
|
+
if (result.status === "committed") {
|
|
151
79
|
trackStudioEvent("sdk_cutover_success", { hfId, opCount: ops.length });
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
trackStudioEvent("sdk_cutover_fallback", {
|
|
155
|
-
hfId: selection.hfId ?? null,
|
|
156
|
-
error: String(err),
|
|
157
|
-
});
|
|
158
|
-
return false;
|
|
80
|
+
} else if (result.status === "failed") {
|
|
81
|
+
trackStudioEvent("sdk_cutover_failed", { hfId, error: result.error.message });
|
|
159
82
|
}
|
|
83
|
+
return result;
|
|
160
84
|
}
|
|
161
85
|
|
|
162
86
|
export async function sdkTimingPersist(
|
|
@@ -166,7 +90,7 @@ export async function sdkTimingPersist(
|
|
|
166
90
|
sdkSession: Composition | null | undefined,
|
|
167
91
|
deps: CutoverDeps,
|
|
168
92
|
options?: CutoverOptions,
|
|
169
|
-
): Promise<
|
|
93
|
+
): Promise<CutoverResult> {
|
|
170
94
|
// Resolver tripwire — runs BEFORE the cutover gate (decoupled): records when
|
|
171
95
|
// the SDK can't resolve a target the server timing path is addressing.
|
|
172
96
|
const timingSrc = deps.readProjectFile;
|
|
@@ -175,28 +99,36 @@ export async function sdkTimingPersist(
|
|
|
175
99
|
hfId,
|
|
176
100
|
"setTiming",
|
|
177
101
|
timingSrc ? () => timingSrc(targetPath) : undefined,
|
|
102
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
178
103
|
);
|
|
179
104
|
// Dark-launch gate: without this, timing cutover runs whenever an SDK session
|
|
180
105
|
// exists (it always does, for shadow/selection) — flipping the flag OFF would
|
|
181
106
|
// NOT disable it. Gate here so flag-off routes back to the legacy server path.
|
|
182
|
-
if (!STUDIO_SDK_CUTOVER_ENABLED) return
|
|
183
|
-
if (!sdkSession
|
|
184
|
-
if (
|
|
107
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return declinedCutover("feature_disabled");
|
|
108
|
+
if (!sdkSession) return declinedCutover("session_unavailable");
|
|
109
|
+
if (!sdkSession.getElement(hfId)) return declinedCutover("target_not_found");
|
|
110
|
+
if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
|
|
185
111
|
try {
|
|
186
112
|
const serializedBefore = sdkSession.serialize();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
113
|
+
const result = await persistSdkCandidateMutation(
|
|
114
|
+
sdkSession,
|
|
115
|
+
targetPath,
|
|
116
|
+
serializedBefore,
|
|
117
|
+
deps,
|
|
118
|
+
(session) => session.setTiming(hfId, timingUpdate),
|
|
119
|
+
options,
|
|
120
|
+
serializedBefore,
|
|
121
|
+
);
|
|
122
|
+
if (result.status === "committed") {
|
|
123
|
+
trackStudioEvent("sdk_cutover_success", { hfId, opCount: 1 });
|
|
124
|
+
} else if (result.status === "failed") {
|
|
125
|
+
trackStudioEvent("sdk_cutover_failed", { hfId, error: result.error.message });
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const failed = { status: "failed", error: asCutoverError(error) } as const;
|
|
130
|
+
trackStudioEvent("sdk_cutover_failed", { hfId, error: failed.error.message });
|
|
131
|
+
return failed;
|
|
200
132
|
}
|
|
201
133
|
}
|
|
202
134
|
|
|
@@ -209,7 +141,7 @@ export async function sdkTimingBatchPersist(
|
|
|
209
141
|
sdkSession: Composition | null | undefined,
|
|
210
142
|
deps: CutoverDeps,
|
|
211
143
|
options?: CutoverOptions,
|
|
212
|
-
): Promise<
|
|
144
|
+
): Promise<CutoverResult> {
|
|
213
145
|
const timingSrc = deps.readProjectFile;
|
|
214
146
|
for (const change of changes) {
|
|
215
147
|
void recordResolverParity(
|
|
@@ -217,31 +149,46 @@ export async function sdkTimingBatchPersist(
|
|
|
217
149
|
change.hfId,
|
|
218
150
|
"setTiming",
|
|
219
151
|
timingSrc ? () => timingSrc(targetPath) : undefined,
|
|
152
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
220
153
|
);
|
|
221
154
|
}
|
|
222
|
-
if (!STUDIO_SDK_CUTOVER_ENABLED) return
|
|
223
|
-
if (!sdkSession
|
|
224
|
-
if (
|
|
155
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return declinedCutover("feature_disabled");
|
|
156
|
+
if (!sdkSession) return declinedCutover("session_unavailable");
|
|
157
|
+
if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
|
|
158
|
+
if (changes.some((change) => !sdkSession.getElement(change.hfId)))
|
|
159
|
+
return declinedCutover("target_not_found");
|
|
225
160
|
try {
|
|
226
161
|
const serializedBefore = sdkSession.serialize();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
162
|
+
const result = await persistSdkCandidateMutation(
|
|
163
|
+
sdkSession,
|
|
164
|
+
targetPath,
|
|
165
|
+
serializedBefore,
|
|
166
|
+
deps,
|
|
167
|
+
(session) => {
|
|
168
|
+
for (const change of changes) session.setTiming(change.hfId, change.timingUpdate);
|
|
169
|
+
},
|
|
170
|
+
options,
|
|
171
|
+
serializedBefore,
|
|
172
|
+
);
|
|
173
|
+
if (result.status === "failed") {
|
|
174
|
+
trackStudioEvent("sdk_cutover_failed", {
|
|
175
|
+
hfId: changes[0]?.hfId ?? null,
|
|
176
|
+
error: result.error.message,
|
|
177
|
+
});
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
234
180
|
trackStudioEvent("sdk_cutover_success", {
|
|
235
181
|
hfId: changes[0]?.hfId ?? null,
|
|
236
182
|
opCount: changes.length,
|
|
237
183
|
});
|
|
238
|
-
return
|
|
239
|
-
} catch (
|
|
240
|
-
|
|
184
|
+
return result;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const failed = { status: "failed", error: asCutoverError(error) } as const;
|
|
187
|
+
trackStudioEvent("sdk_cutover_failed", {
|
|
241
188
|
hfId: changes[0]?.hfId ?? null,
|
|
242
|
-
error:
|
|
189
|
+
error: failed.error.message,
|
|
243
190
|
});
|
|
244
|
-
return
|
|
191
|
+
return failed;
|
|
245
192
|
}
|
|
246
193
|
}
|
|
247
194
|
|
|
@@ -256,7 +203,7 @@ export function sdkGsapTweenPersist(
|
|
|
256
203
|
sdkSession: Composition | null | undefined,
|
|
257
204
|
deps: CutoverDeps,
|
|
258
205
|
options?: CutoverOptions,
|
|
259
|
-
): Promise<
|
|
206
|
+
): Promise<CutoverResult> {
|
|
260
207
|
// Resolver tripwire — runs BEFORE this function's own cutover gate (decoupled).
|
|
261
208
|
// add targets an element (element-resolution parity); set/remove target an
|
|
262
209
|
// animationId (animation-resolution parity). Done here, not via
|
|
@@ -269,20 +216,23 @@ export function sdkGsapTweenPersist(
|
|
|
269
216
|
op.target,
|
|
270
217
|
"addGsapTween",
|
|
271
218
|
gsapSrc ? () => gsapSrc(targetPath) : undefined,
|
|
219
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
272
220
|
);
|
|
273
221
|
} else {
|
|
274
|
-
recordAnimationResolverParity(
|
|
222
|
+
void recordAnimationResolverParity(
|
|
275
223
|
sdkSession,
|
|
276
224
|
op.animationId,
|
|
277
225
|
op.kind === "set" ? "setGsapTween" : "removeGsapTween",
|
|
226
|
+
gsapReadSource(deps, targetPath),
|
|
227
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
278
228
|
);
|
|
279
229
|
}
|
|
280
230
|
// Leading dark-launch gate so flag-off does no SDK touch (getElement) at all —
|
|
281
231
|
// matches the other three chokepoints' discipline.
|
|
282
|
-
if (!STUDIO_SDK_CUTOVER_ENABLED) return Promise.resolve(
|
|
232
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return Promise.resolve(declinedCutover("feature_disabled"));
|
|
283
233
|
if (op.kind === "add" && sdkSession && !sdkSession.getElement(op.target))
|
|
284
|
-
return Promise.resolve(
|
|
285
|
-
// dispatchGsapOpAndPersist
|
|
234
|
+
return Promise.resolve(declinedCutover("target_not_found"));
|
|
235
|
+
// dispatchGsapOpAndPersist declines on before===after — that catches stale
|
|
286
236
|
// animationIds and unsupported shapes (e.g. from-prop on a plain tween), falling
|
|
287
237
|
// back to the server path. This subsumes explicit existence guards for set/remove.
|
|
288
238
|
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) => {
|
|
@@ -305,41 +255,48 @@ async function dispatchGsapOpAndPersist(
|
|
|
305
255
|
options: CutoverOptions | undefined,
|
|
306
256
|
dispatch: (s: Composition) => void,
|
|
307
257
|
resolverTarget?: { animationId: string; opLabel: string },
|
|
308
|
-
): Promise<
|
|
258
|
+
): Promise<CutoverResult> {
|
|
309
259
|
// Resolver tripwire — runs BEFORE the cutover gate (decoupled): records when
|
|
310
260
|
// the SDK can't resolve the animationId the server GSAP path is addressing.
|
|
311
261
|
if (resolverTarget) {
|
|
312
|
-
recordAnimationResolverParity(
|
|
262
|
+
void recordAnimationResolverParity(
|
|
263
|
+
sdkSession,
|
|
264
|
+
resolverTarget.animationId,
|
|
265
|
+
resolverTarget.opLabel,
|
|
266
|
+
gsapReadSource(deps, targetPath),
|
|
267
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
268
|
+
);
|
|
313
269
|
}
|
|
314
270
|
// Dark-launch gate (shared chokepoint for every GSAP-op cutover persist):
|
|
315
|
-
// flag OFF →
|
|
316
|
-
if (!STUDIO_SDK_CUTOVER_ENABLED) return
|
|
317
|
-
if (!sdkSession) return
|
|
318
|
-
if (wrongCompositionFile(deps, targetPath)) return
|
|
271
|
+
// flag OFF → explicit decline → caller falls back to the legacy server path.
|
|
272
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return declinedCutover("feature_disabled");
|
|
273
|
+
if (!sdkSession) return declinedCutover("session_unavailable");
|
|
274
|
+
if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
|
|
319
275
|
const session = sdkSession;
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
await persistSdkSerialize(after, targetPath, undoBefore, deps, options);
|
|
276
|
+
// persistSdkCandidateMutation owns the shared per-project/file transaction
|
|
277
|
+
// coordinator used by both SDK and legacy GSAP writes.
|
|
278
|
+
try {
|
|
279
|
+
const serializedBefore = session.serialize();
|
|
280
|
+
const result = await persistSdkCandidateMutation(
|
|
281
|
+
session,
|
|
282
|
+
targetPath,
|
|
283
|
+
serializedBefore,
|
|
284
|
+
deps,
|
|
285
|
+
dispatch,
|
|
286
|
+
options,
|
|
287
|
+
serializedBefore,
|
|
288
|
+
);
|
|
289
|
+
if (result.status === "committed") {
|
|
335
290
|
trackStudioEvent("sdk_cutover_success", { opCount: 1 });
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
trackStudioEvent("sdk_cutover_fallback", { error: String(err) });
|
|
339
|
-
return false;
|
|
291
|
+
} else if (result.status === "failed") {
|
|
292
|
+
trackStudioEvent("sdk_cutover_failed", { error: result.error.message });
|
|
340
293
|
}
|
|
341
|
-
|
|
342
|
-
|
|
294
|
+
return result;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const failed = { status: "failed", error: asCutoverError(error) } as const;
|
|
297
|
+
trackStudioEvent("sdk_cutover_failed", { error: failed.error.message });
|
|
298
|
+
return failed;
|
|
299
|
+
}
|
|
343
300
|
}
|
|
344
301
|
|
|
345
302
|
export function sdkGsapKeyframePersist(
|
|
@@ -350,7 +307,7 @@ export function sdkGsapKeyframePersist(
|
|
|
350
307
|
sdkSession: Composition | null | undefined,
|
|
351
308
|
deps: CutoverDeps,
|
|
352
309
|
options?: CutoverOptions,
|
|
353
|
-
): Promise<
|
|
310
|
+
): Promise<CutoverResult> {
|
|
354
311
|
return dispatchGsapOpAndPersist(
|
|
355
312
|
targetPath,
|
|
356
313
|
sdkSession,
|
|
@@ -368,7 +325,7 @@ export function sdkGsapRemoveKeyframePersist(
|
|
|
368
325
|
sdkSession: Composition | null | undefined,
|
|
369
326
|
deps: CutoverDeps,
|
|
370
327
|
options?: CutoverOptions,
|
|
371
|
-
): Promise<
|
|
328
|
+
): Promise<CutoverResult> {
|
|
372
329
|
return dispatchGsapOpAndPersist(
|
|
373
330
|
targetPath,
|
|
374
331
|
sdkSession,
|
|
@@ -387,7 +344,7 @@ export function sdkGsapRemovePropertyPersist(
|
|
|
387
344
|
sdkSession: Composition | null | undefined,
|
|
388
345
|
deps: CutoverDeps,
|
|
389
346
|
options?: CutoverOptions,
|
|
390
|
-
): Promise<
|
|
347
|
+
): Promise<CutoverResult> {
|
|
391
348
|
return dispatchGsapOpAndPersist(
|
|
392
349
|
targetPath,
|
|
393
350
|
sdkSession,
|
|
@@ -404,7 +361,7 @@ export function sdkGsapDeleteAllForSelectorPersist(
|
|
|
404
361
|
sdkSession: Composition | null | undefined,
|
|
405
362
|
deps: CutoverDeps,
|
|
406
363
|
options?: CutoverOptions,
|
|
407
|
-
): Promise<
|
|
364
|
+
): Promise<CutoverResult> {
|
|
408
365
|
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
|
|
409
366
|
s.dispatch({ type: "deleteAllForSelector", selector }),
|
|
410
367
|
);
|
|
@@ -416,7 +373,7 @@ export function sdkGsapRemoveAllKeyframesPersist(
|
|
|
416
373
|
sdkSession: Composition | null | undefined,
|
|
417
374
|
deps: CutoverDeps,
|
|
418
375
|
options?: CutoverOptions,
|
|
419
|
-
): Promise<
|
|
376
|
+
): Promise<CutoverResult> {
|
|
420
377
|
return dispatchGsapOpAndPersist(
|
|
421
378
|
targetPath,
|
|
422
379
|
sdkSession,
|
|
@@ -434,7 +391,7 @@ export function sdkGsapConvertToKeyframesPersist(
|
|
|
434
391
|
sdkSession: Composition | null | undefined,
|
|
435
392
|
deps: CutoverDeps,
|
|
436
393
|
options?: CutoverOptions,
|
|
437
|
-
): Promise<
|
|
394
|
+
): Promise<CutoverResult> {
|
|
438
395
|
return dispatchGsapOpAndPersist(
|
|
439
396
|
targetPath,
|
|
440
397
|
sdkSession,
|
|
@@ -483,7 +440,7 @@ export function sdkAddWithKeyframesPersist(
|
|
|
483
440
|
sdkSession: Composition | null | undefined,
|
|
484
441
|
deps: CutoverDeps,
|
|
485
442
|
options?: CutoverOptions,
|
|
486
|
-
): Promise<
|
|
443
|
+
): Promise<CutoverResult> {
|
|
487
444
|
const payload: KeyframesPayload = {
|
|
488
445
|
targetSelector,
|
|
489
446
|
position,
|
|
@@ -507,7 +464,7 @@ export function sdkReplaceWithKeyframesPersist(
|
|
|
507
464
|
sdkSession: Composition | null | undefined,
|
|
508
465
|
deps: CutoverDeps,
|
|
509
466
|
options?: CutoverOptions,
|
|
510
|
-
): Promise<
|
|
467
|
+
): Promise<CutoverResult> {
|
|
511
468
|
const payload: KeyframesPayload = {
|
|
512
469
|
targetSelector,
|
|
513
470
|
position,
|
|
@@ -531,27 +488,32 @@ export async function sdkDeletePersist(
|
|
|
531
488
|
targetPath: string,
|
|
532
489
|
sdkSession: Composition | null | undefined,
|
|
533
490
|
deps: CutoverDeps,
|
|
534
|
-
): Promise<
|
|
491
|
+
): Promise<CutoverResult> {
|
|
535
492
|
// Resolver tripwire — runs BEFORE the cutover gate (decoupled).
|
|
536
|
-
void recordResolverParity(
|
|
537
|
-
|
|
493
|
+
void recordResolverParity(
|
|
494
|
+
sdkSession,
|
|
495
|
+
hfId,
|
|
496
|
+
"removeElement",
|
|
497
|
+
() => Promise.resolve(originalContent),
|
|
498
|
+
{ targetPath, compositionPath: deps.compositionPath },
|
|
538
499
|
);
|
|
539
500
|
// Dark-launch gate: flag OFF → legacy server delete path.
|
|
540
|
-
if (!STUDIO_SDK_CUTOVER_ENABLED) return
|
|
541
|
-
if (!sdkSession
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
sdkSession
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
501
|
+
if (!STUDIO_SDK_CUTOVER_ENABLED) return declinedCutover("feature_disabled");
|
|
502
|
+
if (!sdkSession) return declinedCutover("session_unavailable");
|
|
503
|
+
if (!sdkSession.getElement(hfId)) return declinedCutover("target_not_found");
|
|
504
|
+
if (wrongCompositionFile(deps, targetPath)) return declinedCutover("wrong_composition_file");
|
|
505
|
+
const result = await persistSdkCandidateMutation(
|
|
506
|
+
sdkSession,
|
|
507
|
+
targetPath,
|
|
508
|
+
originalContent,
|
|
509
|
+
deps,
|
|
510
|
+
(session) => session.removeElement(hfId),
|
|
511
|
+
{ label: "Delete element" },
|
|
512
|
+
);
|
|
513
|
+
if (result.status === "committed") {
|
|
551
514
|
trackStudioEvent("sdk_cutover_success", { hfId, opCount: 1 });
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
trackStudioEvent("sdk_cutover_fallback", { hfId, error: String(err) });
|
|
555
|
-
return false;
|
|
515
|
+
} else if (result.status === "failed") {
|
|
516
|
+
trackStudioEvent("sdk_cutover_failed", { hfId, error: result.error.message });
|
|
556
517
|
}
|
|
518
|
+
return result;
|
|
557
519
|
}
|