diff --git a/src/pages/tldraw/singlePlayerPage.tsx b/src/pages/tldraw/singlePlayerPage.tsx index 864fdcc..25a88c3 100644 --- a/src/pages/tldraw/singlePlayerPage.tsx +++ b/src/pages/tldraw/singlePlayerPage.tsx @@ -6,8 +6,7 @@ import { useTldrawUser, DEFAULT_SUPPORT_VIDEO_TYPES, DEFAULT_SUPPORTED_IMAGE_TYPES, - TLStore, - TLStoreWithStatus + TLStore } from '@tldraw/tldraw'; import { useTLDraw } from '../../contexts/TLDrawContext'; import { useAuth } from '../../contexts/AuthContext'; @@ -80,146 +79,83 @@ export default function SinglePlayerPage() { setUserPreferences: setTldrawPreferences }); - // Initialize store โ€” runs as soon as user is ready, no editor needed for store creation + // Orchestrated context.node lifecycle โ€” replaces 3 separate effects + const [canvasPhase, setCanvasPhase] = useState< + 'idle' | 'store-init' | 'snapshot-loading' | 'node-placing' | 'ready' | 'navigating' | 'error' + >('idle'); + const phaseError = useRef(''); + useEffect(() => { - if (!user) { - logger.debug('single-player-page', 'โณ Waiting for user data'); - return; - } + if (!user) return; + let cancelled = false; - logger.info('single-player-page', '๐Ÿ”„ Starting store initialization', { - hasUser: !!user, - userType: user.user_type, - username: user.username - }); - - const initializeStoreAndSnapshot = async () => { + const run = async () => { try { - setLoadingState({ status: 'loading', error: '' }); - - // 1. Create store - logger.debug('single-player-page', '๐Ÿ”„ Creating TLStore'); + setCanvasPhase('store-init'); const newStore = localStoreService.getStore({ schema: customSchema, shapeUtils: allShapeUtils, bindingUtils: allBindingUtils }); - logger.debug('single-player-page', 'โœ… TLStore created'); - // 2. Initialize snapshot service - const snapshotService = new NavigationSnapshotService(newStore, editorRef.current || undefined); - if (accessToken) snapshotService.setAccessToken(accessToken); - snapshotServiceRef.current = snapshotService; - logger.debug('single-player-page', 'โœจ Initialized NavigationSnapshotService'); + const snapSvc = new NavigationSnapshotService(newStore, editorRef.current || undefined); + if (accessToken) snapSvc.setAccessToken(accessToken); + snapshotServiceRef.current = snapSvc; - // 3. Load initial snapshot if we have a node - const storagePath = context.node?.node_storage_path; - if (storagePath) { - logger.debug('single-player-page', '๐Ÿ“ฅ Loading snapshot from database', { - node_storage_path: storagePath, - user_type: user.user_type, - }); - await NavigationSnapshotService.loadNodeSnapshotFromDatabase( - storagePath, - accessToken || '', - newStore, - setLoadingState, - undefined, - editorRef.current || undefined - ); - snapshotService.setCurrentNodePath(storagePath); - logger.debug('single-player-page', 'โœ… Snapshot loaded from database'); - } else { - logger.debug('single-player-page', 'โš ๏ธ No node in context, skipping snapshot load'); + if (context.node) { + setCanvasPhase('snapshot-loading'); + const path = context.node.node_storage_path; + if (path) { + await NavigationSnapshotService.loadNodeSnapshotFromDatabase( + path, + accessToken || '', + newStore, + setLoadingState, + undefined, + editorRef.current || undefined + ); + snapSvc.setCurrentNodePath(path); + } } - // 4. Set up debounced auto-save - let autoSaveTimeout: ReturnType | null = null; - let isAutoSaving = false; + if (cancelled) { + newStore.dispose(); + return; + } + let debounce: ReturnType | null = null; newStore.listen(() => { if (!snapshotServiceRef.current?.getCurrentNodePath()) return; - if (isAutoSaving) return; - if (autoSaveTimeout) clearTimeout(autoSaveTimeout); - autoSaveTimeout = setTimeout(async () => { - if (isAutoSaving) return; - isAutoSaving = true; - try { - logger.debug('single-player-page', '๐Ÿ’พ Auto-saving changes (debounced)'); - await snapshotServiceRef.current?.forceSaveCurrentNode(); - } catch (error) { - logger.error('single-player-page', 'โŒ Auto-save failed', error); - } finally { - isAutoSaving = false; - } + if (debounce) clearTimeout(debounce); + debounce = setTimeout(async () => { + await snapshotServiceRef.current?.forceSaveCurrentNode().catch(() => {}); }, 2000); }); - // 5. Update store state storeRef.current = newStore; setStoreReady(true); - setLoadingState({ status: 'ready', error: '' }); - logger.info('single-player-page', 'โœ… Store initialization complete'); - - // 6. Handle cleanup - return () => { - logger.debug('single-player-page', '๐Ÿงน Starting cleanup'); - if (snapshotServiceRef.current) { - snapshotServiceRef.current.forceSaveCurrentNode().catch(error => { - logger.error('single-player-page', 'โŒ Final save failed', error); - }); - snapshotServiceRef.current.clearCurrentNode(); - snapshotServiceRef.current = null; - } - storeRef.current?.dispose(); - storeRef.current = null; - setStoreReady(false); - logger.debug('single-player-page', '๐Ÿงน Cleanup complete'); - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to initialize store'; - logger.error('single-player-page', 'โŒ Store initialization failed', error); - setLoadingState({ status: 'error', error: errorMessage }); - return undefined; + setCanvasPhase('ready'); + } catch (e) { + phaseError.current = e instanceof Error ? e.message : 'Init failed'; + setCanvasPhase('error'); } }; - initializeStoreAndSnapshot(); - }, [user, context.node]); + run(); + return () => { + cancelled = true; + snapshotServiceRef.current?.forceSaveCurrentNode().catch(() => {}); + snapshotServiceRef.current?.clearCurrentNode(); + snapshotServiceRef.current = null; + storeRef.current?.dispose(); + storeRef.current = null; + setStoreReady(false); + setCanvasPhase('idle'); + }; + }, [user?.id, context.node?.id]); // Handle navigation changes โ€” save previous snapshot, load next one. // No automatic node shape placement: the canvas shows only persisted state. - useEffect(() => { - const handleNodeChange = async () => { - if (!context.node?.id || !snapshotServiceRef.current || !storeRef.current) return; - - const snapshotService = snapshotServiceRef.current; - const currentNode = context.node; - - try { - setLoadingState({ status: 'loading', error: '' }); - logger.debug('single-player-page', '๐Ÿ”„ Handling navigation to node', { - nodeId: currentNode.id, - node_storage_path: currentNode.node_storage_path, - }); - - const previousNode = context.history.currentIndex > 0 - ? context.history.nodes[context.history.currentIndex - 1] - : null; - - await snapshotService.handleNavigationStart(previousNode, currentNode); - setLoadingState({ status: 'ready', error: '' }); - } catch (error) { - logger.error('single-player-page', 'โŒ Failed to load node snapshot', error); - setLoadingState({ - status: 'error', - error: error instanceof Error ? error.message : 'Failed to load node data' - }); - } - }; - - handleNodeChange(); - }, [context.node, context.history, storeReady]); // Inject auth and trigger initial context when token is ready useEffect(() => {