import type { MutableRefObject } from "react"; import type { Composition, GsapTweenSpec } from "@hyperframes/sdk"; import type { DomEditSelection } from "../components/editor/domEditing"; import type { EditHistoryKind } from "./editHistory"; import type { PatchOperation } from "./sourcePatcher"; import { STUDIO_SDK_CUTOVER_ENABLED } from "../components/editor/manualEditingAvailability"; import { trackStudioEvent } from "./studioTelemetry"; import { markSelfWrite } from "../hooks/sdkSelfWriteRegistry"; import { patchOpsToSdkEditOps } from "./sdkOpMapping"; import { recordResolverParity, recordAnimationResolverParity } from "./sdkResolverShadow"; import { shouldDeclineTextCutoverForTarget, shouldUseSdkCutover } from "./sdkCutoverEligibility"; export { shouldUseSdkCutover } from "./sdkCutoverEligibility"; export interface CutoverDeps { editHistory: { recordEdit: (entry: { label: string; kind: EditHistoryKind; coalesceKey?: string; coalesceMs?: number; files: Record; }) => Promise; }; writeProjectFile: (path: string, content: string) => Promise; reloadPreview: () => void; domEditSaveTimestampRef: MutableRefObject; /** * Optional post-write refresh. When provided, it REPLACES the default * reloadPreview() — the GSAP path passes one that soft-reloads (preserving * the playhead) and invalidates the keyframe/gsap panel cache. Receives the * serialized document just written. */ refresh?: (after: string) => void; /** * Path of the composition the SDK session was opened for. The session models * ONLY this file (serialize() emits the whole active composition), so any edit * whose targetPath differs (a sub-composition file) must take the server path * — otherwise we'd write the full active-comp serialization into that file. */ compositionPath?: string | null; /** * Optional per-key task serializer (the same `gsap-file:${file}` serializer the * legacy `commitMutation` uses). When provided, every GSAP-op persist routes its * read-serialize → dispatch → serialize → write through it so two concurrent * same-file flushes can't interleave their read-modify-write and lose an edit. * Absent (e.g. in unit tests) → ops run unserialized as before. */ serialize?: (key: string, task: () => Promise) => Promise; /** * Optional reader for the on-disk content of targetPath. Timing/GSAP persists * use it to capture the EXACT prior bytes as the undo-history `before`, so undo * restores the file verbatim instead of a normalized SDK re-emit (which would * reformat the whole file). The style/delete paths already thread originalContent * in explicitly; this gives timing/GSAP parity without touching every call site. * Absent → falls back to the SDK's pre-edit serialize() (the prior behavior). */ readProjectFile?: (path: string) => Promise; } /** * Capture the undo-history `before` baseline for timing/GSAP persists: the exact * on-disk bytes when a reader is available (so undo restores them verbatim), * falling back to the SDK's pre-edit serialization when it isn't. Never throws — * a failed read degrades to the serialized fallback rather than aborting the edit. */ async function captureOnDiskBefore( deps: CutoverDeps, targetPath: string, serializedFallback: string, ): Promise { if (!deps.readProjectFile) return serializedFallback; try { return await deps.readProjectFile(targetPath); } catch { return serializedFallback; } } /** True when targetPath isn't the composition the SDK session models. */ function wrongCompositionFile(deps: CutoverDeps, targetPath: string): boolean { return deps.compositionPath != null && targetPath !== deps.compositionPath; } interface CutoverOptions { label?: string; coalesceKey?: string; /** Coalesce window (ms); Infinity folds across a slow round-trip. */ coalesceMs?: number; /** Skip the preview reload (mirrors the server path's skipRefresh). */ skipRefresh?: boolean; } // ponytail: exported for setSlideshowManifest (third caller — island write bypasses // the SDK dispatch path since