feat: v0.9.0 — single-screen 3-pane shell + left Assets pane (wallet/reputation/fleet/time controls) from a single /api/state poll; right pane placeholder
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { StateSnapshot } from './game/state';
|
||||
|
||||
export interface GameStateResult {
|
||||
state: StateSnapshot | null;
|
||||
// True until the first successful fetch (stale-while-error afterwards).
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
// Trigger an immediate refetch (used after POST /api/sim so the UI updates without waiting for the next tick).
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
// Single fetch of the full game-state snapshot. Exported separately from the hook so it can be unit-tested with a mocked fetch.
|
||||
export async function fetchState(): Promise<StateSnapshot> {
|
||||
const res = await fetch('/api/state', { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error(`GET /api/state → HTTP ${res.status}`);
|
||||
return (await res.json()) as StateSnapshot;
|
||||
}
|
||||
|
||||
// The shell's single polling source (v0.9.0): one GET /api/state loop for the whole page,
|
||||
// so every pane renders from the same snapshot instead of running its own poll loops.
|
||||
export function useGameState(intervalMs = 1000): GameStateResult {
|
||||
const [state, setState] = useState<StateSnapshot | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Overlap guard: if the previous request is still in flight (slow server), skip this tick.
|
||||
const inFlightRef = useRef(false);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (inFlightRef.current) return;
|
||||
inFlightRef.current = true;
|
||||
fetchState()
|
||||
.then((snapshot) => {
|
||||
setState(snapshot);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
})
|
||||
.finally(() => {
|
||||
inFlightRef.current = false;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh(); // fetch immediately on mount, don't wait for the first interval tick
|
||||
const id = setInterval(refresh, intervalMs);
|
||||
return () => clearInterval(id);
|
||||
}, [intervalMs, refresh]);
|
||||
|
||||
return { state, loading, error, refresh };
|
||||
}
|
||||
Reference in New Issue
Block a user