summaryrefslogtreecommitdiff
path: root/apps/web/src/app
diff options
context:
space:
mode:
authornaoNao89 <90588855+naoNao89@users.noreply.github.com>2025-07-31 05:29:42 +0700
committernaoNao89 <90588855+naoNao89@users.noreply.github.com>2025-07-31 05:29:42 +0700
commita1d2fbd96bb6713c3172198d71e94274af409aee (patch)
treeb8bf5c16f221462272512377331204b6290607a9 /apps/web/src/app
parente6e08057f510e70c956af0f0c08c6eef349edf3c (diff)
downloadopencut-a1d2fbd96bb6713c3172198d71e94274af409aee.tar.gz
[Editor] Prevent excessive project creation from invalid routes
Race conditions in useEffect hook caused navigation to invalid project IDs to create multiple 'Untitled Project' entries. Users reported accumulating 885+ projects due to this bug. This change adds global invalid project ID tracking across component remounts and prevents race conditions through: - Global invalidProjectIds Set in project store with tracking functions - isInitializingRef to prevent duplicate initialization attempts - Proper useEffect cleanup with cancellation tokens - Specific error handling for 'not found' vs recoverable errors - Removed problematic activeProject?.id dependency Testing: - Add comprehensive test suite (15 tests) with Vitest + React Testing Library - Include integration tests demonstrating bug and validating fix - Update CI pipeline to run automated tests Fixes #182
Diffstat (limited to 'apps/web/src/app')
-rw-r--r--apps/web/src/app/editor/[project_id]/page.tsx85
1 files changed, 78 insertions, 7 deletions
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx
index 26fdcf8a..3378d11d 100644
--- a/apps/web/src/app/editor/[project_id]/page.tsx
+++ b/apps/web/src/app/editor/[project_id]/page.tsx
@@ -32,39 +32,110 @@ export default function Editor() {
setPropertiesPanel,
} = usePanelStore();
- const { activeProject, loadProject, createNewProject } = useProjectStore();
+ const {
+ activeProject,
+ loadProject,
+ createNewProject,
+ isInvalidProjectId,
+ markProjectIdAsInvalid,
+ } = useProjectStore();
const params = useParams();
const router = useRouter();
const projectId = params.project_id as string;
const handledProjectIds = useRef<Set<string>>(new Set());
+ const isInitializingRef = useRef<boolean>(false);
usePlaybackControls();
useEffect(() => {
+ let isCancelled = false;
+
const initProject = async () => {
- if (!projectId) return;
+ if (!projectId) {
+ return;
+ }
+ // Prevent duplicate initialization
+ if (isInitializingRef.current) {
+ return;
+ }
+
+ // Check if project is already loaded
if (activeProject?.id === projectId) {
return;
}
+ // Check global invalid tracking FIRST (most important for preventing duplicates)
+ if (isInvalidProjectId(projectId)) {
+ return;
+ }
+
+ // Check if we've already handled this project ID locally
if (handledProjectIds.current.has(projectId)) {
return;
}
+ // Mark as initializing to prevent race conditions
+ isInitializingRef.current = true;
+ handledProjectIds.current.add(projectId);
+
try {
await loadProject(projectId);
+
+ // Check if component was unmounted during async operation
+ if (isCancelled) {
+ return;
+ }
+
+ // Project loaded successfully
+ isInitializingRef.current = false;
} catch (error) {
- handledProjectIds.current.add(projectId);
+ // Check if component was unmounted during async operation
+ if (isCancelled) {
+ return;
+ }
- const newProjectId = await createNewProject("Untitled Project");
- router.replace(`/editor/${newProjectId}`);
- return;
+ // More specific error handling - only create new project for actual "not found" errors
+ const isProjectNotFound = error instanceof Error &&
+ (error.message.includes('not found') ||
+ error.message.includes('does not exist') ||
+ error.message.includes('Project not found'));
+
+ if (isProjectNotFound) {
+ // Mark this project ID as invalid globally BEFORE creating project
+ markProjectIdAsInvalid(projectId);
+
+ try {
+ const newProjectId = await createNewProject("Untitled Project");
+
+ // Check again if component was unmounted
+ if (isCancelled) {
+ return;
+ }
+
+ router.replace(`/editor/${newProjectId}`);
+ } catch (createError) {
+ console.error("Failed to create new project:", createError);
+ }
+ } else {
+ // For other errors (storage issues, corruption, etc.), log but don't create new project
+ console.error("Project loading failed with recoverable error:", error);
+ // Remove from handled set so user can retry
+ handledProjectIds.current.delete(projectId);
+ }
+
+ isInitializingRef.current = false;
}
};
initProject();
- }, [projectId, activeProject?.id, loadProject, createNewProject, router]);
+
+ // Cleanup function to cancel async operations
+ return () => {
+ isCancelled = true;
+ isInitializingRef.current = false;
+ };
+ }, [projectId, loadProject, createNewProject, router]); // Removed activeProject?.id dependency
return (
<EditorProvider>