Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/features/editor/components/editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const EditorView = ({ projectId }: { projectId: Id<"projects"> }) => {
const activeFile = useFile(activeTabId);
const updateFile = useUpdateFile();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const pendingContentRef = useRef<string | null>(null);

const isActiveFileBinary = activeFile && activeFile.storageId;
const isActiveFileText = activeFile && !activeFile.storageId;
Expand All @@ -27,8 +28,14 @@ export const EditorView = ({ projectId }: { projectId: Id<"projects"> }) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current });
pendingContentRef.current = null;
}
Comment on lines +31 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unhandled Promise in cleanup — silent data loss on non-retryable failure

updateFile(...) returns a Promise that is never handled. Convex React automatically retries mutations until they are confirmed to have been written to the database, and the backend ensures each mutation call executes only once, so transient network hiccups are covered. However, non-retryable failures (e.g., auth expiry, server-side validation errors thrown by the mutation) will be silently swallowed because:

  1. No .catch() is attached.
  2. pendingContentRef.current = null (line 33) clears the pending content synchronously before the Promise settles — meaning on a permanent failure, the content is gone with no retry path and no error surfaced to the user.

The same fire-and-forget pattern exists in the debounce callback (line 71), though that code predates this PR.

🛡️ Proposed fix — attach a .catch() to surface failures
-      if (pendingContentRef.current !== null && activeTabId) {
-        updateFile({ id: activeTabId, content: pendingContentRef.current });
-        pendingContentRef.current = null;
-      }
+      if (pendingContentRef.current !== null && activeTabId) {
+        updateFile({ id: activeTabId, content: pendingContentRef.current }).catch(
+          (err) => console.error("[EditorView] Failed to flush pending editor content:", err)
+        );
+        pendingContentRef.current = null;
+      }

For the debounce handler (line 71), the same .catch() pattern applies:

       timeoutRef.current = setTimeout(() => {
-        updateFile({ id: activeFile._id, content });
+        updateFile({ id: activeFile._id, content }).catch(
+          (err) => console.error("[EditorView] Debounced update failed:", err)
+        );
         pendingContentRef.current = null;
       }, DEBOUNCE_MS);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current });
pendingContentRef.current = null;
}
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current }).catch(
(err) => console.error("[EditorView] Failed to flush pending editor content:", err)
);
pendingContentRef.current = null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/editor/components/editor-view.tsx` around lines 31 - 34, The
updateFile call is fire-and-forget, which loses pendingContentRef.current on
non-retryable failures; wrap the Promise returned by updateFile(id: activeTabId,
content: pendingContentRef.current) with .then/.catch so you only set
pendingContentRef.current = null after the Promise resolves successfully and
handle errors in .catch (e.g., log or surface via the app's notification/error
handler) to allow retry or inform the user; apply the same pattern to the
debounce save handler (the debounce callback that calls updateFile around line
71) so both spots attach .catch and only clear pendingContentRef on success.

};
}, [activeTabId]);
}, [
activeTabId
]);

return (
<div className="h-full flex flex-col">
Expand All @@ -54,12 +61,15 @@ export const EditorView = ({ projectId }: { projectId: Id<"projects"> }) => {
fileName={activeFile.name}
initialValue={activeFile.content}
onChange={(content: string) => {
pendingContentRef.current = content;

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
updateFile({ id: activeFile._id, content });
pendingContentRef.current = null;
}, DEBOUNCE_MS);
}}
/>
Expand Down