mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics (#1366)
* fix(studio): save retries, mutation queue circuit breaker, save_failure diagnostics Save failures could silently drop user work: code-editor saves fired a single PUT with no retry, DOM-edit failures drained the whole queue against a failing server, and several failure paths only logged to the console. - Retry code-editor saves with exponential backoff instead of dropping the edit on the first failed PUT. - Circuit breaker on the DOM-edit save queue: a failing server pauses the queue with a user-visible error state instead of burning every queued mutation against it. - save_failure events now carry error_message, status_code, and source on every emission path; style/attribute DOM-edit failures that previously only logged to the console now emit telemetry too. - Route unawaited commitMutation call sites (GSAP drag, property scrubbing, undo/redo, text fields) through a safe wrapper that reports failures via telemetry instead of unhandledrejection. Follow-ups (deferred): version/ETag conflict guard on file PUTs, offline save queue. * fix(studio): narrow save retry changes for fallow
This commit is contained in:
@@ -6,6 +6,7 @@ import type { PatchOperation } from "../utils/sourcePatcher";
|
||||
import { trackStudioEvent } from "../utils/studioTelemetry";
|
||||
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
||||
import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
|
||||
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
||||
import {
|
||||
buildDomEditPatchTarget,
|
||||
getDomEditTargetKey,
|
||||
@@ -31,8 +32,10 @@ import {
|
||||
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
||||
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
||||
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
||||
|
||||
// ── Helpers ──
|
||||
type TimelineLike = { getChildren?: (nested: boolean) => Array<{ targets?: () => Element[] }> };
|
||||
|
||||
export const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
||||
@@ -69,6 +72,8 @@ function isElementGsapTargeted(iframe: HTMLIFrameElement | null, element: HTMLEl
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Types ──
|
||||
|
||||
interface RecordEditInput {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
@@ -102,6 +107,7 @@ export interface UseDomEditCommitsParams {
|
||||
projectIdRef: React.MutableRefObject<string | null>;
|
||||
reloadPreview: () => void;
|
||||
|
||||
// From useDomSelection
|
||||
domEditSelection: DomEditSelection | null;
|
||||
applyDomSelection: (
|
||||
selection: DomEditSelection | null,
|
||||
@@ -115,6 +121,8 @@ export interface UseDomEditCommitsParams {
|
||||
) => Promise<DomEditSelection | null>;
|
||||
}
|
||||
|
||||
// ── Hook ──
|
||||
|
||||
export function useDomEditCommits({
|
||||
activeCompPath,
|
||||
previewIframeRef,
|
||||
@@ -172,7 +180,9 @@ export function useDomEditCommits({
|
||||
const readResponse = await fetch(
|
||||
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
||||
);
|
||||
if (!readResponse.ok) throw new Error(`Failed to read ${targetPath}`);
|
||||
if (!readResponse.ok) {
|
||||
throw await createStudioSaveHttpError(readResponse, `Failed to read ${targetPath}`);
|
||||
}
|
||||
const readData = (await readResponse.json()) as { content?: string };
|
||||
const originalContent = readData.content;
|
||||
if (typeof originalContent !== "string") {
|
||||
@@ -196,14 +206,15 @@ export function useDomEditCommits({
|
||||
body: JSON.stringify({ target: patchTarget, operations }),
|
||||
},
|
||||
);
|
||||
if (!patchResponse.ok) throw new Error(`Failed to patch ${targetPath}`);
|
||||
if (!patchResponse.ok) {
|
||||
throw await createStudioSaveHttpError(patchResponse, `Failed to patch ${targetPath}`);
|
||||
}
|
||||
|
||||
const patchData = (await patchResponse.json()) as {
|
||||
ok?: boolean;
|
||||
changed?: boolean;
|
||||
matched?: boolean;
|
||||
content?: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
if (!patchData.changed) {
|
||||
@@ -243,7 +254,6 @@ export function useDomEditCommits({
|
||||
coalesceKey: options?.coalesceKey,
|
||||
files: { [targetPath]: { before: originalContent, after: finalContent } },
|
||||
});
|
||||
showToast(`Updated ${patchData.path ?? targetPath}`, "info");
|
||||
|
||||
if (!options?.skipRefresh) {
|
||||
reloadPreview();
|
||||
@@ -256,7 +266,6 @@ export function useDomEditCommits({
|
||||
projectIdRef,
|
||||
domEditSaveTimestampRef,
|
||||
reloadPreview,
|
||||
showToast,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -282,38 +291,12 @@ export function useDomEditCommits({
|
||||
resolveImportedFontAsset,
|
||||
});
|
||||
|
||||
// ── Position patch helper ──
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const commitPositionPatchToHtml = useCallback(
|
||||
(
|
||||
selection: DomEditSelection,
|
||||
patches: PatchOperation[],
|
||||
options: { label: string; coalesceKey: string; skipRefresh?: boolean },
|
||||
) => {
|
||||
return queueDomEditSave(async () => {
|
||||
await persistDomEditOperations(selection, patches, {
|
||||
label: options.label,
|
||||
coalesceKey: options.coalesceKey,
|
||||
skipRefresh: options.skipRefresh ?? true,
|
||||
});
|
||||
// fallow-ignore-next-line complexity
|
||||
}).catch((error) => {
|
||||
const message = error instanceof Error ? error.message : "Failed to save position";
|
||||
showToast(message);
|
||||
trackStudioEvent("save_failure", {
|
||||
source: "dom_edit",
|
||||
label: options.label,
|
||||
error_message: message,
|
||||
target_id: selection.id ?? undefined,
|
||||
target_selector: selection.selector ?? undefined,
|
||||
target_source_file: selection.sourceFile ?? undefined,
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
[persistDomEditOperations, queueDomEditSave, showToast],
|
||||
);
|
||||
const commitPositionPatchToHtml = useDomEditPositionPatchCommit({
|
||||
activeCompPath,
|
||||
persistDomEditOperations,
|
||||
queueDomEditSave,
|
||||
showToast,
|
||||
});
|
||||
|
||||
// ── Position commits ──
|
||||
|
||||
@@ -426,7 +409,9 @@ export function useDomEditCommits({
|
||||
const response = await fetch(
|
||||
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
||||
);
|
||||
if (!response.ok) throw new Error(`Failed to read ${targetPath}`);
|
||||
if (!response.ok) {
|
||||
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { content?: string };
|
||||
const originalContent = data.content;
|
||||
@@ -447,7 +432,12 @@ export function useDomEditCommits({
|
||||
body: JSON.stringify({ target: patchTarget }),
|
||||
},
|
||||
);
|
||||
if (!removeResponse.ok) throw new Error(`Failed to delete element from ${targetPath}`);
|
||||
if (!removeResponse.ok) {
|
||||
throw await createStudioSaveHttpError(
|
||||
removeResponse,
|
||||
`Failed to delete element from ${targetPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
|
||||
const patchedContent =
|
||||
|
||||
Reference in New Issue
Block a user