mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +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:
@@ -0,0 +1,87 @@
|
||||
import { getStudioSaveErrorMessage, getStudioSaveStatusCode } from "./studioSaveDiagnostics";
|
||||
|
||||
interface DomEditSaveQueueOpenEvent {
|
||||
consecutiveFailures: number;
|
||||
errorMessage: string;
|
||||
statusCode: number | null;
|
||||
}
|
||||
|
||||
interface DomEditSaveQueueOptions {
|
||||
failureThreshold?: number;
|
||||
onOpen?: (event: DomEditSaveQueueOpenEvent) => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export interface DomEditSaveQueue {
|
||||
enqueue: (save: () => Promise<void>) => Promise<void>;
|
||||
waitForIdle: () => Promise<void>;
|
||||
reset: () => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_FAILURE_THRESHOLD = 5;
|
||||
|
||||
export class DomEditSaveQueueOpenError extends Error {
|
||||
constructor() {
|
||||
super("Auto-save is paused. Dismiss the warning to retry DOM edits.");
|
||||
this.name = "DomEditSaveQueueOpenError";
|
||||
}
|
||||
}
|
||||
|
||||
export function createDomEditSaveQueue(options: DomEditSaveQueueOptions = {}): DomEditSaveQueue {
|
||||
const failureThreshold = options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
||||
|
||||
let tail = Promise.resolve();
|
||||
let consecutiveFailures = 0;
|
||||
let breakerOpen = false;
|
||||
|
||||
const reset = (notify = true) => {
|
||||
const wasOpen = breakerOpen;
|
||||
consecutiveFailures = 0;
|
||||
breakerOpen = false;
|
||||
if (notify && wasOpen) options.onReset?.();
|
||||
};
|
||||
|
||||
const open = (error: unknown) => {
|
||||
if (breakerOpen) return;
|
||||
breakerOpen = true;
|
||||
options.onOpen?.({
|
||||
consecutiveFailures,
|
||||
errorMessage: getStudioSaveErrorMessage(error),
|
||||
statusCode: getStudioSaveStatusCode(error) ?? null,
|
||||
});
|
||||
};
|
||||
|
||||
const run = async (save: () => Promise<void>) => {
|
||||
try {
|
||||
await save();
|
||||
if (!breakerOpen) consecutiveFailures = 0;
|
||||
} catch (error) {
|
||||
consecutiveFailures += 1;
|
||||
if (consecutiveFailures >= failureThreshold) open(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
enqueue(save) {
|
||||
if (breakerOpen) return Promise.reject(new DomEditSaveQueueOpenError());
|
||||
const queued = tail.catch(() => undefined).then(() => run(save));
|
||||
tail = queued.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
return queued;
|
||||
},
|
||||
|
||||
async waitForIdle() {
|
||||
await tail.catch(() => undefined);
|
||||
},
|
||||
|
||||
reset,
|
||||
|
||||
destroy() {
|
||||
reset(false);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user