feat(studio): preserve external file conflicts (#2990)

* fix(studio): drain pending edits before reload

* fix(studio): address drain review feedback (#2989)

- prioritize conflicts and clear recovered DOM queue errors
- cover delayed blur effects and missing drain branches
- document stacked consumers and extend write-token retention

* test(studio): satisfy drain audit gate (#2989)

- share the editor-save hook harness across drain regressions
- extract settled failure inspection from the drain loop

* feat(studio): preserve external file conflicts

* fix(studio): isolate retry write receipts

* test(studio): cover external conflict recovery safety
This commit is contained in:
Miguel Ángel
2026-08-04 21:19:42 +00:00
committed by GitHub
parent 4713138544
commit b30a23402e
8 changed files with 465 additions and 10 deletions
+32 -3
View File
@@ -10,7 +10,11 @@ import {
StudioFileConflictError,
StudioSaveNetworkError,
} from "../utils/studioSaveDiagnostics";
import { createStudioWriteToken, studioExpectedFileVersion } from "../utils/studioFileVersion";
import {
createStudioWriteToken,
markStudioWriteToken,
studioExpectedFileVersion,
} from "../utils/studioFileVersion";
import { useFileTree } from "./useFileTree";
import { useEditorSave } from "./useEditorSave";
@@ -117,8 +121,11 @@ export function useFileManager({
throw await createStudioSaveHttpError(preflight, `Failed to read ${path} before save`);
}
}
const writeToken = createStudioWriteToken();
await retryStudioSave(async () => {
// Each request gets its own receipt identity. If a committed request loses its response,
// the retry can produce a second filesystem receipt that must be suppressed independently.
const writeToken = createStudioWriteToken();
markStudioWriteToken(writeToken);
let response: Response;
try {
response = await fetch(
@@ -191,7 +198,7 @@ export function useFileManager({
// ── Editor save (debounced content change) ──
const { saveRafRef, handleContentChange } = useEditorSave({
const editorSave = useEditorSave({
editingPathRef,
projectIdRef,
readProjectFile,
@@ -201,6 +208,24 @@ export function useFileManager({
setRefreshKey,
showToast,
});
const { saveRafRef, handleContentChange } = editorSave;
const overwriteExternalConflict = useCallback(
async (conflict: StudioFileConflictError) => {
if (conflict.currentContent != null) {
await writeProjectFile(
conflict.filePath,
conflict.attemptedContent,
conflict.currentContent,
);
} else {
fileVersions.set(conflict.filePath, conflict.currentVersion);
await writeProjectFile(conflict.filePath, conflict.attemptedContent);
}
updateEditingFileContent(conflict.filePath, conflict.attemptedContent);
},
[fileVersions, updateEditingFileContent, writeProjectFile],
);
// ── File select ──
@@ -491,11 +516,15 @@ export function useFileManager({
editingPathRef,
projectIdRef,
saveRafRef,
flushPendingSourceSave: editorSave.flushPendingSave,
discardPendingSourceSave: editorSave.discardPendingSave,
getPendingSourceCandidate: editorSave.getPendingCandidate,
importedFontAssetsRef,
// Core I/O
readProjectFile,
writeProjectFile,
overwriteExternalConflict,
readOptionalProjectFile,
observeProjectFileVersion,
updateEditingFileContent,