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
import { localStoreService } from '../../services/tldraw/localStoreService';
import { PresentationService } from '../../services/tldraw/presentationService';
import { NodeCanvasService } from '../../services/tldraw/nodeCanvasService';
import { NavigationSnapshotService } from '../../services/tldraw/snapshotService';
// Tldraw utils
import { getUiOverrides, getUiComponents } from '../../utils/tldraw/ui-overrides';
@ -34,9 +33,6 @@ import '../../utils/tldraw/tldraw.css';
// App debug
import { logger } from '../../debugConfig';
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 {
status: 'ready' | 'loading' | 'error';
@ -67,9 +63,8 @@ export default function SinglePlayerPage() {
status: 'ready',
error: ''
});
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [isEditorReady, setIsEditorReady] = useState(false);
const [store, setStore] = useState<TLStore | TLStoreWithStatus | undefined>(undefined);
const storeRef = useRef<TLStore | null>(null);
const [storeReady, setStoreReady] = useState(false);
// TLDraw user preferences
const tldrawUser = useTldrawUser({
@ -93,7 +88,6 @@ export default function SinglePlayerPage() {
}
logger.info('single-player-page', '🔄 Starting store initialization', {
isEditorReady,
hasUser: !!user,
userType: user.user_type,
username: user.username
@ -119,58 +113,36 @@ export default function SinglePlayerPage() {
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
// 3. Load initial snapshot if we have a node
if (context.node) {
const nodeStoragePath = getNodeStoragePath(context.node);
if (nodeStoragePath) {
const storagePath = context.node?.node_storage_path;
if (storagePath) {
logger.debug('single-player-page', '📥 Loading snapshot from database', {
dbName: null,
node: context.node,
node_storage_path: nodeStoragePath,
node_storage_path: storagePath,
user_type: user.user_type,
username: user.username
});
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
nodeStoragePath,
storagePath,
accessToken || '',
newStore,
setLoadingState,
undefined,
editorRef.current || undefined
);
// Wire auto-save: set the current path on the service instance
snapshotService.setCurrentNodePath(nodeStoragePath);
snapshotService.setCurrentNodePath(storagePath);
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 {
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 isAutoSaving = false;
newStore.listen(() => {
if (snapshotServiceRef.current && snapshotServiceRef.current.getCurrentNodePath()) {
// Skip if already saving
if (isAutoSaving) {
logger.debug('single-player-page', '⚠️ Skipping auto-save - already saving');
return;
}
// Clear existing timeout
if (autoSaveTimeout) {
clearTimeout(autoSaveTimeout);
}
// Debounce auto-save to prevent excessive saves
if (!snapshotServiceRef.current?.getCurrentNodePath()) return;
if (isAutoSaving) return;
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(async () => {
if (isAutoSaving) return; // Double-check
if (isAutoSaving) return;
isAutoSaving = true;
try {
logger.debug('single-player-page', '💾 Auto-saving changes (debounced)');
@ -180,12 +152,12 @@ export default function SinglePlayerPage() {
} finally {
isAutoSaving = false;
}
}, 2000); // Increased to 2 seconds debounce
}
}, 2000);
});
// 5. Update store state
setStore(newStore);
storeRef.current = newStore;
setStoreReady(true);
setLoadingState({ status: 'ready', error: '' });
logger.info('single-player-page', '✅ Store initialization complete');
@ -199,8 +171,9 @@ export default function SinglePlayerPage() {
snapshotServiceRef.current.clearCurrentNode();
snapshotServiceRef.current = null;
}
newStore.dispose();
setStore(undefined);
storeRef.current?.dispose();
storeRef.current = null;
setStoreReady(false);
logger.debug('single-player-page', '🧹 Cleanup complete');
};
} catch (error) {
@ -214,110 +187,30 @@ export default function SinglePlayerPage() {
initializeStoreAndSnapshot();
}, [user, context.node]);
// Handle initial node placement
useEffect(() => {
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
// 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 || !editorRef.current || !snapshotServiceRef.current || !store) {
return;
}
if (!context.node?.id || !snapshotServiceRef.current || !storeRef.current) 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 currentNode = context.node;
try {
setLoadingState({ status: 'loading', error: '' });
logger.debug('single-player-page', '🔄 Loading node data', {
logger.debug('single-player-page', '🔄 Handling navigation to node', {
nodeId: currentNode.id,
node_storage_path: currentNode.node_storage_path,
isInitialLoad
});
// Get the previous node from navigation history
const previousNode = context.history.currentIndex > 0
? context.history.nodes[context.history.currentIndex - 1]
: null;
// Handle navigation in snapshot service (load/save snapshot)
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: '' });
} 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({
status: 'error',
error: error instanceof Error ? error.message : 'Failed to load node data'
@ -326,7 +219,7 @@ export default function SinglePlayerPage() {
};
handleNodeChange();
}, [context.node, context.history, store]);
}, [context.node, context.history, storeReady]);
// Inject auth and trigger initial context when token is ready
useEffect(() => {
@ -485,7 +378,7 @@ export default function SinglePlayerPage() {
top: `${HEADER_HEIGHT}px`,
}}>
{/* Loading overlay - show when loading or contexts not initialized */}
{(loadingState.status === 'loading' || !store) && (
{(loadingState.status === 'loading' || !storeReady) && (
<div style={{
position: 'absolute',
top: 0,
@ -513,9 +406,9 @@ export default function SinglePlayerPage() {
</Alert>
</Snackbar>
{store && <Tldraw
{storeReady && storeRef.current && <Tldraw
user={tldrawUser}
store={store}
store={storeRef.current}
tools={singlePlayerTools}
shapeUtils={allShapeUtils}
bindingUtils={allBindingUtils}
@ -548,11 +441,9 @@ export default function SinglePlayerPage() {
if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken);
}
setIsEditorReady(true);
logger.info('single-player-page', '✅ Tldraw mounted successfully', {
editorId: editor.store.id,
presentationMode,
isEditorReady: true
});
} catch (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,
};
};