mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(studio): stage 7 step 3c — sdk cutover for inline-style ops (#1522)
Introduces sdkCutoverPersist(): when STUDIO_SDK_CUTOVER_ENABLED is set, inline-style PatchOps are routed through the SDK session's in-memory document model instead of the server patch-element API. The SDK serialize() result is written back through the same writeProjectFile + editHistory.recordEdit path, so the on-disk output is identical to the legacy route. - packages/studio/src/utils/sdkCutover.ts (new): sdkCutoverPersist() + shouldUseSdkCutover() guard; domEditSaveTimestampRef.current is stamped on each write to suppress the echo file-change reload. - packages/studio/src/components/editor/manualEditingAvailability.ts: adds STUDIO_SDK_CUTOVER_ENABLED flag (default false); changes STUDIO_SDK_SHADOW_ENABLED default to false now that cutover is available. - packages/studio/src/hooks/useSdkSession.ts: adds optional domEditSaveTimestampRef param; self-write suppress window (SELF_WRITE_SUPPRESS_MS) gates file-change reloads so SDK writes don't echo back as external edits. - packages/studio/src/App.tsx: passes domEditSaveTimestampRef to useSdkSession so the suppress window can gate reloads triggered by SDK cutover writes. - Test coverage: sdkCutover.test.ts (new, 141 lines) + useDomEditSession.test.ts (new, 50 lines) — guard function + happy-path assertions. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e662fcdea2
commit
ab7145ad9e
@@ -0,0 +1,91 @@
|
||||
import type { MutableRefObject } from "react";
|
||||
import type { Composition } 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 { patchOpsToSdkEditOps } from "./sdkShadow";
|
||||
import { trackStudioEvent } from "./studioTelemetry";
|
||||
|
||||
const CUTOVER_OP_TYPES = new Set<PatchOperation["type"]>([
|
||||
"inline-style",
|
||||
"text-content",
|
||||
"attribute",
|
||||
"html-attribute",
|
||||
]);
|
||||
|
||||
export function shouldUseSdkCutover(
|
||||
flagEnabled: boolean,
|
||||
hasSession: boolean,
|
||||
hfId: string | null | undefined,
|
||||
ops: PatchOperation[],
|
||||
): boolean {
|
||||
return (
|
||||
flagEnabled &&
|
||||
hasSession &&
|
||||
!!hfId &&
|
||||
ops.length > 0 &&
|
||||
ops.every((o) => CUTOVER_OP_TYPES.has(o.type))
|
||||
);
|
||||
}
|
||||
|
||||
interface CutoverDeps {
|
||||
editHistory: {
|
||||
recordEdit: (entry: {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
coalesceKey?: string;
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
writeProjectFile: (path: string, content: string) => Promise<void>;
|
||||
reloadPreview: () => void;
|
||||
domEditSaveTimestampRef: MutableRefObject<number>;
|
||||
}
|
||||
|
||||
interface CutoverOptions {
|
||||
label?: string;
|
||||
coalesceKey?: string;
|
||||
}
|
||||
|
||||
export async function sdkCutoverPersist(
|
||||
selection: DomEditSelection,
|
||||
ops: PatchOperation[],
|
||||
originalContent: string,
|
||||
targetPath: string,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
if (!shouldUseSdkCutover(STUDIO_SDK_CUTOVER_ENABLED, !!sdkSession, selection.hfId, ops))
|
||||
return false;
|
||||
if (!sdkSession) return false;
|
||||
const hfId = selection.hfId;
|
||||
if (!hfId) return false;
|
||||
if (!sdkSession.getElement(hfId)) return false;
|
||||
try {
|
||||
sdkSession.batch(() => {
|
||||
for (const editOp of patchOpsToSdkEditOps(hfId, ops)) {
|
||||
sdkSession.dispatch(editOp);
|
||||
}
|
||||
});
|
||||
const after = sdkSession.serialize();
|
||||
deps.domEditSaveTimestampRef.current = Date.now();
|
||||
await deps.writeProjectFile(targetPath, after);
|
||||
await deps.editHistory.recordEdit({
|
||||
label: options?.label ?? "Edit layer",
|
||||
kind: "manual",
|
||||
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
||||
files: { [targetPath]: { before: originalContent, after } },
|
||||
});
|
||||
deps.reloadPreview();
|
||||
trackStudioEvent("sdk_cutover_success", { hfId, opCount: ops.length });
|
||||
return true;
|
||||
} catch (err) {
|
||||
trackStudioEvent("sdk_cutover_fallback", {
|
||||
hfId: selection.hfId ?? null,
|
||||
error: String(err),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user