mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
* feat(studio): default SDK shadow dispatch on for parity telemetry Shadow mode keeps the server patch path authoritative (no user-visible change) and emits sdk_shadow_dispatch parity signal. Default it on so we collect addressing/serialize-drift telemetry from all traffic before any cutover. Disable via VITE_STUDIO_SDK_SHADOW_ENABLED=false. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(studio): shadow parity for delete/timing/gsap ops + wire delete Extends shadow visibility past the property-edit path. Adds a can()-first shadow core (pure addressing/validity pre-check, works even for GSAP which has no snapshot value) plus runShadowDelete/runShadowTiming/runShadowGsapTween. Parity coverage: delete = getElement null (full); timing = snapshot start/duration/trackIndex (full); gsap = can()+dispatch+returned-id only (animationIds is a stub, tween values are script-level — full fidelity needs serialize() round-trip diffing, out of scope). Wires the delete runner end-to-end via an onElementDeleted callback (useDomEditSession → useDomEditCommits → useElementLifecycleOps), fired after the server delete succeeds. Server stays authoritative. Timing/GSAP wiring follows (each needs threading sdkSession into useTimelineEditing / useGsapScriptCommits). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(studio): wire timing + GSAP-add shadow dispatch Timing: thread sdkSession into useTimelineEditing; fire runShadowTiming after move/resize persist (server authoritative). Moved the useSdkSession call above useTimelineEditing so both share the single session (no duplicate). GSAP: thread sdkSession through useGsapScriptCommits → useGsapAnimationOps; shadow addGsapAnimation via runShadowGsapTween after the server add. Only the add path is shadowed — delete/update key on the server's animationId, which doesn't resolve in the SDK's independent id-space (would emit false cannot_dispatch). "set" has no SDK method, so it's skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): address #1473 review — no-persist shadow session + fallow gate Blocker (Rames): the shadow runners dispatched on the live persisted SDK session, so each shadow op fired the persist queue → an HTTP write of the SDK's serialize() output, clobbering the studio's authoritative write (default-on shipped this). Fix: open the shadow session WITHOUT persist — it reads from the server but never writes back. Shadow dispatches mutate the in-memory model only and are discarded on the next reload-on-change. Cutover (Step 3c+) must re-add persist together with self-write suppression. No persist consumer exists in this stack (cutover is not in main), so this is safe and keeps default-on. Fallow CI gate (Miguel): - drop unused `export` on RecordEditInput (dead-type) - suppress pre-existing CRAP with reasons: commitMutation, addGsapAnimation; file-level complexity on useTimelineEditing (shadow .then() branches nudge several callbacks over threshold — telemetry-only) - suppress 3 pre-existing clones surfaced by adjacent edits (save-error formatter, prop-drilling passthrough, file-change reload handler) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(studio): scrub user content from shadow property-path telemetry Addresses #1473 review concern (Rames): inline-style and text-content edits put user content into the sdk_shadow_dispatch mismatch expected/actual fields. Redact before emit — text-content values fully redacted (length only), others length-capped at 64. The in-memory parity result keeps raw values, so the parity logic and tests are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
import { useCallback } from "react";
|
|
import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
import { applySoftReload } from "../utils/gsapSoftReload";
|
|
import { updateKeyframeCacheFromParsed } from "./gsapKeyframeCacheHelpers";
|
|
import {
|
|
GsapMutationHttpError,
|
|
formatGsapMutationRejectionToast,
|
|
readJsonResponseBody,
|
|
} from "./gsapScriptCommitHelpers";
|
|
import type {
|
|
CommitMutationOptions,
|
|
GsapScriptCommitsParams,
|
|
MutationResult,
|
|
} from "./gsapScriptCommitTypes";
|
|
import { useGsapAnimationOps } from "./useGsapAnimationOps";
|
|
import { useGsapArcPathOps } from "./useGsapArcPathOps";
|
|
import { useGsapKeyframeOps } from "./useGsapKeyframeOps";
|
|
import { useGsapPropertyDebounce } from "./useGsapPropertyDebounce";
|
|
import {
|
|
useGsapSaveFailureTelemetry,
|
|
useSafeGsapCommitMutation,
|
|
} from "./useSafeGsapCommitMutation";
|
|
|
|
async function mutateGsapScript(
|
|
projectId: string,
|
|
sourceFile: string,
|
|
mutation: Record<string, unknown>,
|
|
): Promise<MutationResult> {
|
|
const res = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(mutation),
|
|
},
|
|
);
|
|
if (!res.ok) throw new GsapMutationHttpError(res.status, await readJsonResponseBody(res));
|
|
const result = (await res.json()) as MutationResult;
|
|
if (!result.ok) throw new Error(`Failed to update GSAP in ${sourceFile}`);
|
|
return result;
|
|
}
|
|
|
|
// oxfmt-ignore
|
|
// fallow-ignore-next-line complexity
|
|
export function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession }: GsapScriptCommitsParams) {
|
|
// Pre-existing complexity (server mutate + history + reload branches); this PR
|
|
// adds only a guarded shadow-fidelity dispatch.
|
|
// fallow-ignore-next-line complexity
|
|
const commitMutation = useCallback(async (selection: DomEditSelection, mutation: Record<string, unknown>, options: CommitMutationOptions) => {
|
|
const pid = projectIdRef.current;
|
|
if (!pid) return;
|
|
const unsafeFields = findUnsafeMutationValues(mutation);
|
|
if (unsafeFields.length > 0) {
|
|
showToast?.("Couldn't read element layout — try again at a different playhead time", "error");
|
|
if (options.skipReload) return;
|
|
throw new Error(`Mutation contains unsafe values: ${unsafeFields.map((field) => field.path).join(", ")}`);
|
|
}
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
let result: MutationResult;
|
|
try {
|
|
result = await mutateGsapScript(pid, targetPath, mutation);
|
|
} catch (error) {
|
|
if (error instanceof GsapMutationHttpError) showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
if (options.skipReload) return;
|
|
throw error;
|
|
}
|
|
if (result.changed === false) return;
|
|
domEditSaveTimestampRef.current = Date.now();
|
|
if (result.before != null && result.after != null) {
|
|
await editHistory.recordEdit({ label: options.label, kind: "manual", coalesceKey: options.coalesceKey, files: { [targetPath]: { before: result.before, after: result.after } } });
|
|
}
|
|
if (result.after != null) onFileContentChanged?.(targetPath, result.after);
|
|
if (options.skipReload) return;
|
|
if (result.parsed?.animations) updateKeyframeCacheFromParsed(result.parsed.animations, targetPath, selection.id ?? undefined, mutation);
|
|
options.beforeReload?.();
|
|
if (options.softReload && result.scriptText) {
|
|
if (!applySoftReload(previewIframeRef.current, result.scriptText)) reloadPreview();
|
|
} else {
|
|
reloadPreview();
|
|
}
|
|
onCacheInvalidate();
|
|
}, [projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast]);
|
|
const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
|
|
const commitMutationSafely = useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToast);
|
|
const propertyOps = useGsapPropertyDebounce(commitMutationSafely);
|
|
const animationOps = useGsapAnimationOps({ projectIdRef, activeCompPath, commitMutation, commitMutationSafely, showToast, sdkSession });
|
|
const keyframeOps = useGsapKeyframeOps({ activeCompPath, commitMutation, commitMutationSafely, trackGsapSaveFailure });
|
|
const arcPathOps = useGsapArcPathOps(commitMutationSafely);
|
|
return { commitMutation, ...propertyOps, ...animationOps, ...keyframeOps, ...arcPathOps };
|
|
}
|