Private
Public Access
1
0

chg: dev: massive refactor on fetches - create centralized dataProvider #7

This commit is contained in:
2026-04-19 20:56:51 +02:00
parent 5da8a04c18
commit d9059acb78
6 changed files with 202 additions and 98 deletions

View File

@@ -8,10 +8,10 @@
*/
import React, { useEffect, useRef } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useGame } from '@mine-contexts';
import { DESC, IMAGES } from '@mine-utils';
import useStepTimer from './useStepTimer';
import useGameDataProvider from './useGameDataProvider';
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
@@ -28,6 +28,17 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
cells,
} = useGame();
/** Get all API queries and mutations from data provider */
const {
connectQuery,
startMutation,
joinMutation,
stepMutation,
heartbeatMutation,
challengeRespondMutation,
leaveMutation,
} = useGameDataProvider(gameAssoc);
const eventSourceRef = useRef(null);
const rpcUsersRef = useRef(null);
const stepCacheRef = useRef([]);
@@ -42,40 +53,6 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const HEARTBEAT_INTERVAL_MS = 1500;
/** REST mutations / queries */
const connectQuery = useQuery({
queryKey: ['game-connect', gameAssoc],
queryFn: () => fetch('/api/game/connect/' + gameAssoc)
.then(r => r.text())
.then(b64 => JSON.parse(window.atob(b64))),
enabled: false,
retry: false,
});
const startMutation = useMutation({
mutationFn: () => fetch('/api/game/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gameAssoc }),
}),
});
const joinMutation = useMutation({
mutationFn: () => fetch('/api/game/join/' + gameAssoc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}).catch(e => isEnvDev && console.error('Join error', e)),
});
const stepMutation = useMutation({
mutationFn: dataPack => fetch('/api/game/step/' + gameAssoc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataPack),
}).then(r => r.json()),
});
/** Game-start helpers (triggered by server events) */
const wInit = (revealedCells = [], lastStep = {}, gameState = {}, isGameFinished = false) => {
@@ -176,7 +153,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
overlayTitle = 'Choose an opponent!';
overlaySubtitle = gameAssoc ? (
<WaitingOverlayContent
shareUrl={`${window.location.origin}/battle/${gameAssoc}`}
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
currentGameAssoc={gameAssoc}
/>
) : '';
@@ -188,7 +165,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
Promise.resolve().then(() => setGridReady(true));
};
const makeGameStart = (payload, lastStep = {}) => {
const makeGameStart = payload => {
/** Don't start a finished game */
if (isGameFinishedRef.current) {
return;
@@ -242,11 +219,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const publishHeartbeat = () => {
const me = webPlayerRef.current;
if (!me || endRef.current) return;
fetch('/api/game/heartbeat/' + gameAssoc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ color: me }),
}).catch(e => isEnvDev && console.warn('Heartbeat publish failed', e));
heartbeatMutation.mutate(me);
};
const startHeartbeat = () => {
@@ -264,7 +237,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
/** Mercure / SSE message handlers */
const wSubscribe = (payload, rpcUsers = null, lastStep = null) => {
const wSubscribe = (payload, rpcUsers = null) => {
isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
const firstUser = !rpcUsers;
@@ -279,7 +252,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
&& (!connectionLostRef.current
|| (connectionLostRef.current && false === activePlayerRef.current && !endRef.current))
) {
makeGameStart(payload, lastStep);
makeGameStart(payload);
}
};
@@ -315,31 +288,31 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const handleAccept = () => {
clearTimeout(declineTimeout);
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted: true, targetGameAssoc: gameAssoc }),
}).then(() => {
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
}).catch(() => {
});
challengeRespondMutation.mutate(
{ challengerGameAssoc, accepted: true, targetGameAssoc: gameAssoc },
{
onSuccess: () => {
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
},
}
);
};
const handleDecline = () => {
clearTimeout(declineTimeout);
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted: false, targetGameAssoc: gameAssoc }),
}).then(() => {
showOverlay('We are waiting for your opponent...', gameAssoc ? (
<WaitingOverlayContent
shareUrl={window.location.origin + '/play/' + gameAssoc}
currentGameAssoc={gameAssoc}
/>
) : '');
}).catch(() => {
});
challengeRespondMutation.mutate(
{ challengerGameAssoc, accepted: false, targetGameAssoc: gameAssoc },
{
onSuccess: () => {
showOverlay('We are waiting for your opponent...', gameAssoc ? (
<WaitingOverlayContent
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
currentGameAssoc={gameAssoc}
/>
) : '');
},
}
);
};
declineTimeout = setTimeout(handleDecline, 30000);
@@ -404,7 +377,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
if (undefined !== payload.data) {
wTopic(payload);
} else if (undefined === payload.msg) {
wSubscribe(payload, rpcUsersRef.current, lastStepRef.current);
wSubscribe(payload, rpcUsersRef.current);
} else {
wUnsubscribe(payload);
}
@@ -423,7 +396,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
const url = new URL(hubUrl, window.location.origin);
url.searchParams.append('topic', 'mineseeker/channel/' + gameAssoc);
url.searchParams.append('topic', `mineseeker/channel/${gameAssoc}`);
if (subscriberJwt) url.searchParams.append('authorization', subscriberJwt);
if (eventSourceRef.current) eventSourceRef.current.close();
@@ -452,6 +425,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
openEventSource();
return;
}
try {
if (gameInherited) {
const serverData = await connectQuery.refetch().then(r => {
@@ -497,7 +471,9 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
}
})();
window.addEventListener('pagehide', () => navigator.sendBeacon('/api/game/leave/' + gameAssoc));
window.addEventListener('pagehide', () => {
leaveMutation.mutate();
});
return () => {
stopHeartbeat();
@@ -525,9 +501,11 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
try {
const result = await stepMutation.mutateAsync(dataPack);
applyStep(result);
if (result.uuid && !endRef.current) {
setGameUuid(result.uuid);
}
makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
} catch (e) {
isEnvDev && console.error('Step error', e);
@@ -537,6 +515,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const clickResign = () => {
const color = activePlayerRef.current ? 'blue' : 'red';
const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
stepMutation.mutate(
{ resign: color, stepElapsed },
{
@@ -551,7 +530,9 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
const resign = () => {
const activeColor = activePlayerRef.current ? 'blue' : 'red';
if (webPlayerRef.current !== activeColor) return;
showOverlay('Are u sure u want to resign?!', (
<div className="resign">
<a onClick={clickResign}>Yes</a>