import { useEffect, useMemo, useRef, useState } from 'react'; import { ipcChannels } from '../../../shared/ipc/contracts'; import type { SceneTokensSessionEvent, SceneTokensSessionState } from '../../../shared/types'; import { getDndApi } from '../dndApi'; function applyEvent( prev: SceneTokensSessionState | null, event: SceneTokensSessionEvent, ): SceneTokensSessionState { const base = prev ?? { revision: 0, byPlacementId: {} }; if (event.kind === 'clear') { return { revision: base.revision + 1, byPlacementId: {} }; } const placementId = String(event.placementId ?? ''); return { revision: base.revision + 1, byPlacementId: { ...base.byPlacementId, [placementId]: { nx: event.nx, ny: event.ny }, }, }; } export function useSceneTokensSession(): [ SceneTokensSessionState | null, { dispatch: (event: SceneTokensSessionEvent) => Promise }, ] { const api = getDndApi(); const [state, setState] = useState(null); const localRevisionRef = useRef(0); useEffect(() => { void api.invoke(ipcChannels.sceneTokensSession.getState, {}).then(({ state: s }) => { localRevisionRef.current = Math.max(localRevisionRef.current, s.revision); setState(s); }); return api.on(ipcChannels.sceneTokensSession.stateChanged, ({ state: s }) => { // Не затираем более свежий optimistic-стейт устаревшим broadcast // (например, emit при смене сцены, пока последний move ещё в полёте). if (localRevisionRef.current > s.revision) return; localRevisionRef.current = s.revision; setState(s); }); }, [api]); const apiWrap = useMemo( () => ({ dispatch: async (event: SceneTokensSessionEvent) => { setState((prev) => { const next = applyEvent(prev, event); localRevisionRef.current = Math.max(localRevisionRef.current, next.revision); return next; }); const res = await api.invoke(ipcChannels.sceneTokensSession.dispatch, { event }); void res; }, }), [api], ); return [state, apiWrap]; }