diff --git a/web/src/pages/ShipDashboardPage.tsx b/web/src/pages/ShipDashboardPage.tsx index 8e63598..13e3d55 100644 --- a/web/src/pages/ShipDashboardPage.tsx +++ b/web/src/pages/ShipDashboardPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import TopBar from '../components/layout/TopBar'; import PageContainer from '../components/layout/PageContainer'; @@ -7,29 +7,16 @@ import MobilitySection from '../components/dashboard/MobilitySection'; import WeaponsSection from '../components/dashboard/WeaponsSection'; import NotesSection from '../components/dashboard/NotesSection'; import { useShipStore } from '../store/use-ship-store'; -import type { Ship } from '../types/ship'; export default function ShipDashboardPage() { const { id } = useParams<{ id: string }>(); const { ship, weapons, loading, joinShip, leaveShip, updateShip, createWeapon, updateWeapon, deleteWeapon } = useShipStore(); - const debounceRef = useRef | null>(null); useEffect(() => { if (id) joinShip(id); return () => leaveShip(); }, [id, joinShip, leaveShip]); - const handleUpdate = useCallback( - (patch: Partial) => { - // Debounce rapid changes (e.g. stepper clicks) - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - updateShip(patch); - }, 300); - }, - [updateShip], - ); - if (loading || !ship) { return ( <> @@ -45,15 +32,15 @@ export default function ShipDashboardPage() { <> - - + + - + ); diff --git a/web/src/store/use-ship-store.ts b/web/src/store/use-ship-store.ts index 99fc269..6693bf5 100644 --- a/web/src/store/use-ship-store.ts +++ b/web/src/store/use-ship-store.ts @@ -108,8 +108,24 @@ export const useShipStore = create((set, get) => ({ updateShip: (patch) => { const id = get().currentShipId; - if (!id) return; - socket.emit('ship:update', { shipId: id, patch }); + const ship = get().ship; + if (!id || !ship) return; + + // Optimistic: apply locally immediately + set({ ship: { ...ship, ...patch } }); + + // Accumulate patches and debounce the server send + const pending = (socket as any).__pendingPatch ?? {}; + Object.assign(pending, patch); + (socket as any).__pendingPatch = pending; + + if ((socket as any).__patchTimer) clearTimeout((socket as any).__patchTimer); + (socket as any).__patchTimer = setTimeout(() => { + const merged = (socket as any).__pendingPatch; + delete (socket as any).__pendingPatch; + delete (socket as any).__patchTimer; + if (merged) socket.emit('ship:update', { shipId: id, patch: merged }); + }, 300); }, createWeapon: (weapon) => { @@ -119,7 +135,27 @@ export const useShipStore = create((set, get) => ({ }, updateWeapon: (weaponId, patch) => { - socket.emit('weapon:update', { weaponId, patch }); + // Optimistic: apply locally immediately + set({ + weapons: get().weapons.map((w) => + w.id === weaponId ? { ...w, ...patch } : w, + ), + }); + + // Debounce per-weapon server sends + const timerKey = `__weaponTimer_${weaponId}`; + const patchKey = `__weaponPatch_${weaponId}`; + const pending = (socket as any)[patchKey] ?? {}; + Object.assign(pending, patch); + (socket as any)[patchKey] = pending; + + if ((socket as any)[timerKey]) clearTimeout((socket as any)[timerKey]); + (socket as any)[timerKey] = setTimeout(() => { + const merged = (socket as any)[patchKey]; + delete (socket as any)[patchKey]; + delete (socket as any)[timerKey]; + if (merged) socket.emit('weapon:update', { weaponId, patch: merged }); + }, 300); }, deleteWeapon: (weaponId) => {