feat: replace triple context.node useEffect with single state machine
This commit is contained in:
parent
d3bd25d544
commit
2d15b7cc03
@ -6,8 +6,7 @@ import {
|
|||||||
useTldrawUser,
|
useTldrawUser,
|
||||||
DEFAULT_SUPPORT_VIDEO_TYPES,
|
DEFAULT_SUPPORT_VIDEO_TYPES,
|
||||||
DEFAULT_SUPPORTED_IMAGE_TYPES,
|
DEFAULT_SUPPORTED_IMAGE_TYPES,
|
||||||
TLStore,
|
TLStore
|
||||||
TLStoreWithStatus
|
|
||||||
} from '@tldraw/tldraw';
|
} from '@tldraw/tldraw';
|
||||||
import { useTLDraw } from '../../contexts/TLDrawContext';
|
import { useTLDraw } from '../../contexts/TLDrawContext';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
@ -80,146 +79,83 @@ export default function SinglePlayerPage() {
|
|||||||
setUserPreferences: setTldrawPreferences
|
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<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) return;
|
||||||
logger.debug('single-player-page', '⏳ Waiting for user data');
|
let cancelled = false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('single-player-page', '🔄 Starting store initialization', {
|
const run = async () => {
|
||||||
hasUser: !!user,
|
|
||||||
userType: user.user_type,
|
|
||||||
username: user.username
|
|
||||||
});
|
|
||||||
|
|
||||||
const initializeStoreAndSnapshot = async () => {
|
|
||||||
try {
|
try {
|
||||||
setLoadingState({ status: 'loading', error: '' });
|
setCanvasPhase('store-init');
|
||||||
|
|
||||||
// 1. Create store
|
|
||||||
logger.debug('single-player-page', '🔄 Creating TLStore');
|
|
||||||
const newStore = localStoreService.getStore({
|
const newStore = localStoreService.getStore({
|
||||||
schema: customSchema,
|
schema: customSchema,
|
||||||
shapeUtils: allShapeUtils,
|
shapeUtils: allShapeUtils,
|
||||||
bindingUtils: allBindingUtils
|
bindingUtils: allBindingUtils
|
||||||
});
|
});
|
||||||
logger.debug('single-player-page', '✅ TLStore created');
|
|
||||||
|
|
||||||
// 2. Initialize snapshot service
|
const snapSvc = new NavigationSnapshotService(newStore, editorRef.current || undefined);
|
||||||
const snapshotService = new NavigationSnapshotService(newStore, editorRef.current || undefined);
|
if (accessToken) snapSvc.setAccessToken(accessToken);
|
||||||
if (accessToken) snapshotService.setAccessToken(accessToken);
|
snapshotServiceRef.current = snapSvc;
|
||||||
snapshotServiceRef.current = snapshotService;
|
|
||||||
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
|
|
||||||
|
|
||||||
// 3. Load initial snapshot if we have a node
|
if (context.node) {
|
||||||
const storagePath = context.node?.node_storage_path;
|
setCanvasPhase('snapshot-loading');
|
||||||
if (storagePath) {
|
const path = context.node.node_storage_path;
|
||||||
logger.debug('single-player-page', '📥 Loading snapshot from database', {
|
if (path) {
|
||||||
node_storage_path: storagePath,
|
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||||
user_type: user.user_type,
|
path,
|
||||||
});
|
accessToken || '',
|
||||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
newStore,
|
||||||
storagePath,
|
setLoadingState,
|
||||||
accessToken || '',
|
undefined,
|
||||||
newStore,
|
editorRef.current || undefined
|
||||||
setLoadingState,
|
);
|
||||||
undefined,
|
snapSvc.setCurrentNodePath(path);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Set up debounced auto-save
|
if (cancelled) {
|
||||||
let autoSaveTimeout: ReturnType<typeof setTimeout> | null = null;
|
newStore.dispose();
|
||||||
let isAutoSaving = false;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let debounce: ReturnType<typeof setTimeout> | null = null;
|
||||||
newStore.listen(() => {
|
newStore.listen(() => {
|
||||||
if (!snapshotServiceRef.current?.getCurrentNodePath()) return;
|
if (!snapshotServiceRef.current?.getCurrentNodePath()) return;
|
||||||
if (isAutoSaving) return;
|
if (debounce) clearTimeout(debounce);
|
||||||
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
debounce = setTimeout(async () => {
|
||||||
autoSaveTimeout = setTimeout(async () => {
|
await snapshotServiceRef.current?.forceSaveCurrentNode().catch(() => {});
|
||||||
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);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Update store state
|
|
||||||
storeRef.current = newStore;
|
storeRef.current = newStore;
|
||||||
setStoreReady(true);
|
setStoreReady(true);
|
||||||
setLoadingState({ status: 'ready', error: '' });
|
setCanvasPhase('ready');
|
||||||
logger.info('single-player-page', '✅ Store initialization complete');
|
} catch (e) {
|
||||||
|
phaseError.current = e instanceof Error ? e.message : 'Init failed';
|
||||||
// 6. Handle cleanup
|
setCanvasPhase('error');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeStoreAndSnapshot();
|
run();
|
||||||
}, [user, context.node]);
|
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.
|
// Handle navigation changes — save previous snapshot, load next one.
|
||||||
// No automatic node shape placement: the canvas shows only persisted state.
|
// 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
|
// Inject auth and trigger initial context when token is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user