fix: remove TLStore from useState and dead state vars in singlePlayerPage

This commit is contained in:
CC Worker 2026-05-31 20:41:51 +00:00
parent bf592886c6
commit d3bd25d544

View File

@ -15,7 +15,6 @@ import { useUser } from '../../contexts/UserContext';
// Tldraw services // Tldraw services
import { localStoreService } from '../../services/tldraw/localStoreService'; import { localStoreService } from '../../services/tldraw/localStoreService';
import { PresentationService } from '../../services/tldraw/presentationService'; import { PresentationService } from '../../services/tldraw/presentationService';
import { NodeCanvasService } from '../../services/tldraw/nodeCanvasService';
import { NavigationSnapshotService } from '../../services/tldraw/snapshotService'; import { NavigationSnapshotService } from '../../services/tldraw/snapshotService';
// Tldraw utils // Tldraw utils
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides'; import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
@ -34,9 +33,6 @@ import '../../utils/tldraw/tldraw.css';
// App debug // App debug
import { logger } from '../../debugConfig'; import { logger } from '../../debugConfig';
import { CircularProgress, Alert, Snackbar } from '@mui/material'; import { CircularProgress, Alert, Snackbar } from '@mui/material';
import { getThemeFromLabel } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-styles';
import { NodeData } from '../../types/graph-shape';
import { NavigationNode } from '../../types/navigation';
interface LoadingState { interface LoadingState {
status: 'ready' | 'loading' | 'error'; status: 'ready' | 'loading' | 'error';
@ -67,9 +63,8 @@ export default function SinglePlayerPage() {
status: 'ready', status: 'ready',
error: '' error: ''
}); });
const [isInitialLoad, setIsInitialLoad] = useState(true); const storeRef = useRef<TLStore | null>(null);
const [isEditorReady, setIsEditorReady] = useState(false); const [storeReady, setStoreReady] = useState(false);
const [store, setStore] = useState<TLStore | TLStoreWithStatus | undefined>(undefined);
// TLDraw user preferences // TLDraw user preferences
const tldrawUser = useTldrawUser({ const tldrawUser = useTldrawUser({
@ -93,7 +88,6 @@ export default function SinglePlayerPage() {
} }
logger.info('single-player-page', '🔄 Starting store initialization', { logger.info('single-player-page', '🔄 Starting store initialization', {
isEditorReady,
hasUser: !!user, hasUser: !!user,
userType: user.user_type, userType: user.user_type,
username: user.username username: user.username
@ -119,73 +113,51 @@ export default function SinglePlayerPage() {
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService'); logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
// 3. Load initial snapshot if we have a node // 3. Load initial snapshot if we have a node
if (context.node) { const storagePath = context.node?.node_storage_path;
const nodeStoragePath = getNodeStoragePath(context.node); if (storagePath) {
if (nodeStoragePath) { logger.debug('single-player-page', '📥 Loading snapshot from database', {
logger.debug('single-player-page', '📥 Loading snapshot from database', { node_storage_path: storagePath,
dbName: null, user_type: user.user_type,
node: context.node, });
node_storage_path: nodeStoragePath, await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
user_type: user.user_type, storagePath,
username: user.username accessToken || '',
}); newStore,
setLoadingState,
await NavigationSnapshotService.loadNodeSnapshotFromDatabase( undefined,
nodeStoragePath, editorRef.current || undefined
accessToken || '', );
newStore, snapshotService.setCurrentNodePath(storagePath);
setLoadingState, logger.debug('single-player-page', '✅ Snapshot loaded from database');
undefined,
editorRef.current || undefined
);
// Wire auto-save: set the current path on the service instance
snapshotService.setCurrentNodePath(nodeStoragePath);
logger.debug('single-player-page', '✅ Snapshot loaded from database');
} else {
logger.debug('single-player-page', '⚠️ No node_storage_path found in node, skipping snapshot load', {
node: context.node
});
}
} else { } else {
logger.debug('single-player-page', '⚠️ No node in context, skipping snapshot load'); logger.debug('single-player-page', '⚠️ No node in context, skipping snapshot load');
} }
// 4. Set up auto-save with debouncing (only after initial load is complete) // 4. Set up debounced auto-save
let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null; let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
let isAutoSaving = false; let isAutoSaving = false;
newStore.listen(() => { newStore.listen(() => {
if (snapshotServiceRef.current && snapshotServiceRef.current.getCurrentNodePath()) { if (!snapshotServiceRef.current?.getCurrentNodePath()) return;
// Skip if already saving if (isAutoSaving) return;
if (isAutoSaving) { if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
logger.debug('single-player-page', '⚠️ Skipping auto-save - already saving'); autoSaveTimeout = setTimeout(async () => {
return; 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;
} }
}, 2000);
// Clear existing timeout
if (autoSaveTimeout) {
clearTimeout(autoSaveTimeout);
}
// Debounce auto-save to prevent excessive saves
autoSaveTimeout = setTimeout(async () => {
if (isAutoSaving) return; // Double-check
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;
}
}, 2000); // Increased to 2 seconds debounce
}
}); });
// 5. Update store state // 5. Update store state
setStore(newStore); storeRef.current = newStore;
setStoreReady(true);
setLoadingState({ status: 'ready', error: '' }); setLoadingState({ status: 'ready', error: '' });
logger.info('single-player-page', '✅ Store initialization complete'); logger.info('single-player-page', '✅ Store initialization complete');
@ -199,8 +171,9 @@ export default function SinglePlayerPage() {
snapshotServiceRef.current.clearCurrentNode(); snapshotServiceRef.current.clearCurrentNode();
snapshotServiceRef.current = null; snapshotServiceRef.current = null;
} }
newStore.dispose(); storeRef.current?.dispose();
setStore(undefined); storeRef.current = null;
setStoreReady(false);
logger.debug('single-player-page', '🧹 Cleanup complete'); logger.debug('single-player-page', '🧹 Cleanup complete');
}; };
} catch (error) { } catch (error) {
@ -214,110 +187,30 @@ export default function SinglePlayerPage() {
initializeStoreAndSnapshot(); initializeStoreAndSnapshot();
}, [user, context.node]); }, [user, context.node]);
// Handle initial node placement // Handle navigation changes — save previous snapshot, load next one.
useEffect(() => { // No automatic node shape placement: the canvas shows only persisted state.
const placeInitialNode = async () => {
if (!context.node || !editorRef.current || !store || !isInitialLoad) {
logger.debug('single-player-page', '⚠️ Skipping placeInitialNode - missing dependencies', {
hasNode: !!context.node,
hasEditor: !!editorRef.current,
hasStore: !!store,
isInitialLoad
});
return;
}
// Debug: Log the actual node structure
logger.debug('single-player-page', '🔍 Node structure for placeInitialNode', {
node: context.node,
nodeKeys: Object.keys(context.node),
hasId: !!context.node.id,
hasStoragePath: !!context.node.node_storage_path,
hasData: !!context.node.data,
dataKeys: context.node.data ? Object.keys(context.node.data) : null
});
// Validate that the node has required properties
const nodeStoragePath = getNodeStoragePath(context.node);
if (!context.node.id || !nodeStoragePath) {
logger.error('single-player-page', '❌ Node missing required properties', {
nodeId: context.node.id,
hasStoragePath: !!nodeStoragePath,
node: context.node
});
setLoadingState({
status: 'error',
error: 'Node is missing required information'
});
return;
}
try {
setLoadingState({ status: 'loading', error: '' });
if (context.node.type !== 'workspace') {
try {
const nodeData = await loadNodeData(context.node);
await NodeCanvasService.centerCurrentNode(editorRef.current, context.node, nodeData);
} catch (shapeErr) {
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: context.node.type, error: shapeErr });
}
}
setIsInitialLoad(false);
setLoadingState({ status: 'ready', error: '' });
} catch (error) {
logger.error('single-player-page', '❌ Failed to place initial node', error);
setLoadingState({
status: 'error',
error: error instanceof Error ? error.message : 'Failed to place initial node'
});
}
};
placeInitialNode();
}, [context.node, store, isInitialLoad]);
// Handle navigation changes
useEffect(() => { useEffect(() => {
const handleNodeChange = async () => { const handleNodeChange = async () => {
if (!context.node?.id || !editorRef.current || !snapshotServiceRef.current || !store) { if (!context.node?.id || !snapshotServiceRef.current || !storeRef.current) return;
return;
}
// We can safely assert these types because we've checked for null above
const editor = editorRef.current as Editor;
const snapshotService = snapshotServiceRef.current; const snapshotService = snapshotServiceRef.current;
const currentNode = context.node; const currentNode = context.node;
try { try {
setLoadingState({ status: 'loading', error: '' }); setLoadingState({ status: 'loading', error: '' });
logger.debug('single-player-page', '🔄 Loading node data', { logger.debug('single-player-page', '🔄 Handling navigation to node', {
nodeId: currentNode.id, nodeId: currentNode.id,
node_storage_path: currentNode.node_storage_path, node_storage_path: currentNode.node_storage_path,
isInitialLoad
}); });
// Get the previous node from navigation history
const previousNode = context.history.currentIndex > 0 const previousNode = context.history.currentIndex > 0
? context.history.nodes[context.history.currentIndex - 1] ? context.history.nodes[context.history.currentIndex - 1]
: null; : null;
// Handle navigation in snapshot service (load/save snapshot)
await snapshotService.handleNavigationStart(previousNode, currentNode); await snapshotService.handleNavigationStart(previousNode, currentNode);
if (currentNode.type !== 'workspace') {
try {
const nodeData = await loadNodeData(currentNode);
await NodeCanvasService.centerCurrentNode(editor, currentNode, nodeData);
} catch (shapeErr) {
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: currentNode.type, error: shapeErr });
}
}
setLoadingState({ status: 'ready', error: '' }); setLoadingState({ status: 'ready', error: '' });
} catch (error) { } catch (error) {
logger.error('single-player-page', '❌ Failed to load node data', error); logger.error('single-player-page', '❌ Failed to load node snapshot', error);
setLoadingState({ setLoadingState({
status: 'error', status: 'error',
error: error instanceof Error ? error.message : 'Failed to load node data' error: error instanceof Error ? error.message : 'Failed to load node data'
@ -326,7 +219,7 @@ export default function SinglePlayerPage() {
}; };
handleNodeChange(); handleNodeChange();
}, [context.node, context.history, store]); }, [context.node, context.history, storeReady]);
// Inject auth and trigger initial context when token is ready // Inject auth and trigger initial context when token is ready
useEffect(() => { useEffect(() => {
@ -485,7 +378,7 @@ export default function SinglePlayerPage() {
top: `${HEADER_HEIGHT}px`, top: `${HEADER_HEIGHT}px`,
}}> }}>
{/* Loading overlay - show when loading or contexts not initialized */} {/* Loading overlay - show when loading or contexts not initialized */}
{(loadingState.status === 'loading' || !store) && ( {(loadingState.status === 'loading' || !storeReady) && (
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
@ -513,9 +406,9 @@ export default function SinglePlayerPage() {
</Alert> </Alert>
</Snackbar> </Snackbar>
{store && <Tldraw {storeReady && storeRef.current && <Tldraw
user={tldrawUser} user={tldrawUser}
store={store} store={storeRef.current}
tools={singlePlayerTools} tools={singlePlayerTools}
shapeUtils={allShapeUtils} shapeUtils={allShapeUtils}
bindingUtils={allBindingUtils} bindingUtils={allBindingUtils}
@ -548,11 +441,9 @@ export default function SinglePlayerPage() {
if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken); if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken);
} }
setIsEditorReady(true);
logger.info('single-player-page', '✅ Tldraw mounted successfully', { logger.info('single-player-page', '✅ Tldraw mounted successfully', {
editorId: editor.store.id, editorId: editor.store.id,
presentationMode, presentationMode,
isEditorReady: true
}); });
} catch (error) { } catch (error) {
logger.error('single-player-page', '❌ Error in onMount', error); logger.error('single-player-page', '❌ Error in onMount', error);
@ -563,42 +454,3 @@ export default function SinglePlayerPage() {
); );
} }
// Helper function to safely extract node_storage_path from different node structures
const getNodeStoragePath = (node: NavigationNode): string | null => {
// Try direct access first
if (node.node_storage_path) {
return node.node_storage_path;
}
// Try nested under data
if (node.data?.node_storage_path) {
return node.data.node_storage_path;
}
// Try other possible locations
if (node.data?.storage_path && typeof node.data.storage_path === 'string') {
return node.data.storage_path;
}
return null;
};
const loadNodeData = async (node: NavigationNode): Promise<NodeData> => {
if (!node?.id) throw new Error('Node parameter is required');
const nodeStoragePath = getNodeStoragePath(node);
if (!nodeStoragePath) throw new Error(`Node ${node.id} is missing node_storage_path`);
const theme = getThemeFromLabel(node.type);
return {
title: node.label || node.type || '',
w: 500,
h: 350,
state: { parentId: null, isPageChild: true, hasChildren: null, bindings: null },
headerColor: theme.headerColor,
backgroundColor: theme.backgroundColor,
isLocked: false,
__primarylabel__: node.type,
uuid_string: node.id,
node_storage_path: nodeStoragePath,
};
};