Skip to content

Commit 4c66382

Browse files
committed
feat: обновить обработку состояния в Scoreboard и AdminPanel для улучшения управления соединением и обработки команд
1 parent 3009385 commit 4c66382

File tree

3 files changed

+152
-54
lines changed

3 files changed

+152
-54
lines changed

src/components/OBS_Components/Scoreboard/AdminPanel/AdminPanel.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { HubConnectionState } from "@microsoft/signalr";
21
import { useCallback, useEffect, useRef } from "react";
32
import { Col, Container, Row } from "react-bootstrap";
43
import { useNavigate } from "react-router-dom";
54

5+
import { ScoreboardDto } from "@/shared/api";
66
import { useSiteColors } from "@/shared/Utils/useSiteColors";
77

88
import ThemeToggle from "../../../ThemeToggle";
@@ -13,7 +13,6 @@ import LayoutCard from "./LayoutCard";
1313
import MetaPanel from "./MetaPanel/MetaPanel";
1414
import PlayerCard from "./PlayerCard/PlayerCard";
1515
import {
16-
ScoreboardState,
1716
useGeneralActions,
1817
usePlayerActions,
1918
useScoreboardStore,
@@ -30,7 +29,7 @@ const AdminPanelContent = () => {
3029

3130
// Функция для обработки получения состояния с сервера
3231
const handleReceiveStateCallback = useCallback(
33-
(state: ScoreboardState) => {
32+
(state: ScoreboardDto) => {
3433
const now = Date.now();
3534
// Дополнительная защита от слишком частых обновлений
3635
if (now - lastUpdateRef.current < 100) {
@@ -46,10 +45,13 @@ const AdminPanelContent = () => {
4645
const connection = useScoreboardStore(state => state._connection);
4746

4847
useEffect(() => {
49-
if (connection.state === HubConnectionState.Connected) {
50-
connection.on("ReceiveState", handleReceiveStateCallback);
51-
connection.on("StateUpdated", handleReceiveStateCallback);
52-
}
48+
connection.on("ReceiveState", handleReceiveStateCallback);
49+
connection.on("StateUpdated", handleReceiveStateCallback);
50+
51+
return () => {
52+
connection.off("ReceiveState", handleReceiveStateCallback);
53+
connection.off("StateUpdated", handleReceiveStateCallback);
54+
};
5355
}, [connection, handleReceiveStateCallback]);
5456

5557
// Редирект на админку при открытии с телефона

src/components/OBS_Components/Scoreboard/AdminPanel/store/scoreboardStore.ts

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,22 @@ import {
1616
MetaInfo,
1717
MetaInfoWithTimestamp,
1818
PlayerWithTimestamp,
19+
ScoreboardState as ScoreboardHubState,
1920
} from "../types";
2021

22+
type ScoreboardIncomingState = Partial<Omit<ScoreboardHubState, "colors">> & {
23+
colors?: ScoreboardColorsDto | Partial<ColorPreset>;
24+
color?: ScoreboardColorsDto | Partial<ColorPreset>;
25+
};
26+
27+
type PendingServerCommand = {
28+
method: string;
29+
data: ScoreboardDto | boolean;
30+
};
31+
32+
const pendingServerCommands: PendingServerCommand[] = [];
33+
const MAX_PENDING_SERVER_COMMANDS = 40;
34+
2135
// Интерфейс состояния
2236
export interface ScoreboardState {
2337
_connection: HubConnection;
@@ -62,14 +76,13 @@ export interface ScoreboardActions {
6276
reset: () => void;
6377

6478
// Действия для получения состояния с сервера
65-
handleReceiveState: (state: ScoreboardState) => void;
79+
handleReceiveState: (state: ScoreboardIncomingState) => void;
6680

6781
// Внутренние действия (не экспортируются)
6882
_sendToServer: (
6983
method: string,
70-
data: ScoreboardDto | boolean,
71-
updateId?: string
72-
) => boolean;
84+
data: ScoreboardDto | boolean
85+
) => Promise<boolean>;
7386
_createServerState: (
7487
updatedPlayer1?: PlayerWithTimestamp,
7588
updatedPlayer2?: PlayerWithTimestamp,
@@ -124,18 +137,67 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
124137
// Инициализируем соединение
125138
const connection = initialState._connection;
126139

127-
const firstActiveFunction = (state: ScoreboardState) => {
140+
const queueServerCommand = (
141+
method: string,
142+
data: ScoreboardDto | boolean
143+
) => {
144+
const nextCommand: PendingServerCommand = { method, data };
145+
const existingCommandIndex = pendingServerCommands.findIndex(
146+
pendingCommand => pendingCommand.method === method
147+
);
148+
149+
if (existingCommandIndex >= 0) {
150+
pendingServerCommands[existingCommandIndex] = nextCommand;
151+
} else {
152+
pendingServerCommands.push(nextCommand);
153+
if (pendingServerCommands.length > MAX_PENDING_SERVER_COMMANDS) {
154+
pendingServerCommands.shift();
155+
}
156+
}
157+
};
158+
159+
const flushPendingServerCommands = async () => {
160+
if (connection.state !== HubConnectionState.Connected) {
161+
return;
162+
}
163+
164+
while (pendingServerCommands.length > 0) {
165+
const pendingCommand = pendingServerCommands.shift();
166+
if (pendingCommand) {
167+
const sendResult = await get()._sendToServer(
168+
pendingCommand.method,
169+
pendingCommand.data
170+
);
171+
172+
if (!sendResult) {
173+
break;
174+
}
175+
}
176+
}
177+
};
178+
179+
const firstActiveFunction = (state: ScoreboardIncomingState) => {
128180
get().handleReceiveState(state);
129181
connection.off("ReceiveState", firstActiveFunction);
130182
};
131183

132184
connection.on("ReceiveState", firstActiveFunction);
133185

134-
// Запускаем соединение
135-
connection.start().catch(err => {
136-
console.error("Error starting SignalR connection:", err);
186+
connection.onreconnected(() => {
187+
console.log("Scoreboard SignalR reconnected. Flushing queued updates...");
188+
void flushPendingServerCommands();
137189
});
138190

191+
// Запускаем соединение
192+
connection
193+
.start()
194+
.then(() => {
195+
void flushPendingServerCommands();
196+
})
197+
.catch(err => {
198+
console.error("Error starting SignalR connection:", err);
199+
});
200+
139201
return {
140202
...initialState,
141203
_connection: connection,
@@ -152,7 +214,7 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
152214

153215
// Отправляем на сервер
154216
const serverState = get()._createServerState(updatedPlayer);
155-
get()._sendToServer("UpdateState", serverState);
217+
void get()._sendToServer("UpdateState", serverState);
156218
},
157219

158220
setPlayer2: playerUpdate => {
@@ -167,7 +229,7 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
167229

168230
// Отправляем на сервер
169231
const serverState = get()._createServerState(undefined, updatedPlayer);
170-
get()._sendToServer("UpdateState", serverState);
232+
void get()._sendToServer("UpdateState", serverState);
171233
},
172234

173235
swapPlayers: () => {
@@ -184,7 +246,7 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
184246

185247
// Отправляем на сервер
186248
const serverState = get()._createServerState(newPlayer1, newPlayer2);
187-
get()._sendToServer("UpdateState", serverState);
249+
void get()._sendToServer("UpdateState", serverState);
188250
},
189251

190252
// Действия с мета информацией
@@ -204,7 +266,7 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
204266
undefined,
205267
updatedMeta
206268
);
207-
get()._sendToServer("UpdateState", serverState);
269+
void get()._sendToServer("UpdateState", serverState);
208270
},
209271

210272
// Действия с цветами
@@ -225,7 +287,7 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
225287
undefined,
226288
updatedColor
227289
);
228-
get()._sendToServer("UpdateState", serverState);
290+
void get()._sendToServer("UpdateState", serverState);
229291
},
230292

231293
handleColorChange: colorUpdate => {
@@ -250,15 +312,15 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
250312
undefined,
251313
updatedLayout
252314
);
253-
get()._sendToServer("UpdateState", serverState);
315+
void get()._sendToServer("UpdateState", serverState);
254316
},
255317

256318
// Действия с видимостью
257319
setVisibility: isVisible => {
258320
set({ isVisible });
259321

260322
// Отправляем на сервер
261-
get()._sendToServer("SetVisibility", isVisible);
323+
void get()._sendToServer("SetVisibility", isVisible);
262324
},
263325

264326
setAnimationDuration: duration => {
@@ -274,61 +336,84 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
274336
undefined,
275337
duration
276338
);
277-
get()._sendToServer("UpdateState", serverState);
339+
void get()._sendToServer("UpdateState", serverState);
278340
},
279341

280342
// Действия сброса
281343
reset: () => {
282-
set(initialState);
344+
set({ ...initialState, _connection: connection });
283345

284346
// Отправляем на сервер
285347
const serverState = get()._createServerState();
286-
get()._sendToServer("UpdateState", serverState);
348+
void get()._sendToServer("UpdateState", serverState);
287349
},
288350

289351
// Действия для получения состояния с сервера
290352
handleReceiveState: state => {
353+
const receiveTime = Date.now();
354+
291355
if (state.player1) {
292-
set({ player1: { ...state.player1, _receivedAt: Date.now() } });
356+
set({ player1: { ...state.player1, _receivedAt: receiveTime } });
293357
}
294358
if (state.player2) {
295-
set({ player2: { ...state.player2, _receivedAt: Date.now() } });
359+
set({ player2: { ...state.player2, _receivedAt: receiveTime } });
296360
}
297361
if (state.meta) {
298-
set({ meta: { ...state.meta, _receivedAt: Date.now() } });
362+
set({ meta: { ...state.meta, _receivedAt: receiveTime } });
299363
}
300-
if (state.color) {
301-
set({ color: { ...state.color, _lastEdit: Date.now() } });
364+
365+
const incomingColor = state.colors ?? state.color;
366+
if (incomingColor) {
367+
const currentColor = get().color;
368+
set({
369+
color: {
370+
...currentColor,
371+
...incomingColor,
372+
name: incomingColor.name ?? currentColor.name ?? "Custom",
373+
_lastEdit: receiveTime,
374+
_receivedAt: receiveTime,
375+
},
376+
});
302377
}
378+
303379
if (state.layout) {
304380
set({ layout: state.layout });
305381
}
382+
306383
if (typeof state.isVisible === "boolean") {
307384
set({ isVisible: state.isVisible });
308385
}
309-
if (state.animationDuration) {
386+
387+
if (typeof state.animationDuration === "number") {
310388
set({ animationDuration: state.animationDuration });
311389
}
312390
},
313391

314392
// Внутренние действия
315-
_sendToServer: (method, data) => {
393+
_sendToServer: async (method, data) => {
394+
let result = false;
395+
316396
try {
317397
if (!connection || connection.state !== HubConnectionState.Connected) {
398+
queueServerCommand(method, data);
318399
console.log(
319-
"SignalR connection not available, using local state only"
400+
`SignalR connection state is '${connection.state}'. Queued ${method}.`
320401
);
321-
return true;
402+
} else {
403+
console.log(`Sending ${method}:`, data);
404+
await connection.invoke(method, data);
405+
console.log(`Successfully sent ${method}`);
406+
result = true;
322407
}
323-
324-
console.log(`Sending ${method}:`, data);
325-
connection.invoke(method, data);
326-
console.log(`Successfully sent ${method}`);
327-
return true;
328408
} catch (error) {
329-
console.error(`Error sending ${method}:`, error);
330-
return false;
409+
queueServerCommand(method, data);
410+
console.error(
411+
`Error sending ${method}. Command queued for retry.`,
412+
error
413+
);
331414
}
415+
416+
return result;
332417
},
333418

334419
_createServerState: (
@@ -356,14 +441,14 @@ export const useScoreboardStore = create<ScoreboardStore>((set, get) => {
356441
});
357442

358443
return {
359-
player1: updatedPlayer1 || state.player1,
360-
player2: updatedPlayer2 || state.player2,
361-
meta: updatedMeta || state.meta,
362-
colors: convertColorPresetToDto(updatedColor || state.color),
363-
layout: updatedLayout || state.layout,
444+
player1: updatedPlayer1 ?? state.player1,
445+
player2: updatedPlayer2 ?? state.player2,
446+
meta: updatedMeta ?? state.meta,
447+
colors: convertColorPresetToDto(updatedColor ?? state.color),
448+
layout: updatedLayout ?? state.layout,
364449
isVisible:
365450
updatedVisibility !== undefined ? updatedVisibility : state.isVisible,
366-
animationDuration: updatedAnimationDuration || state.animationDuration,
451+
animationDuration: updatedAnimationDuration ?? state.animationDuration,
367452
};
368453
},
369454
};

src/components/OBS_Components/Scoreboard/Scoreboard.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AnimatePresence, motion } from "framer-motion";
2-
import { type CSSProperties, useCallback, useState } from "react";
2+
import { type CSSProperties, useCallback, useEffect, useState } from "react";
33

44
import InjectStyles from "@/shared/components/InjectStyles";
55

@@ -99,7 +99,7 @@ const ScoreboardContent: React.FC = () => {
9999
setColors(state.colors);
100100
}
101101

102-
if (state.animationDuration) {
102+
if (typeof state.animationDuration === "number") {
103103
setAnimationDuration(state.animationDuration);
104104
}
105105

@@ -108,11 +108,22 @@ const ScoreboardContent: React.FC = () => {
108108
}
109109
}, []);
110110

111-
connection.on("ReceiveState", handleReceiveState);
112-
connection.on("StateUpdated", handleReceiveState);
113-
connection.on("VisibilityChanged", (isVisible: boolean) => {
114-
setIsVisible(isVisible);
115-
});
111+
const handleVisibilityChanged = useCallback((nextVisibility: boolean) => {
112+
setIsVisible(nextVisibility);
113+
setHasReceivedInitialState(true);
114+
}, []);
115+
116+
useEffect(() => {
117+
connection.on("ReceiveState", handleReceiveState);
118+
connection.on("StateUpdated", handleReceiveState);
119+
connection.on("VisibilityChanged", handleVisibilityChanged);
120+
121+
return () => {
122+
connection.off("ReceiveState", handleReceiveState);
123+
connection.off("StateUpdated", handleReceiveState);
124+
connection.off("VisibilityChanged", handleVisibilityChanged);
125+
};
126+
}, [connection, handleReceiveState, handleVisibilityChanged]);
116127

117128
// В Production окружении не показываем скорборд до первого получения данных
118129
if (import.meta.env.PROD && !hasReceivedInitialState) {

0 commit comments

Comments
 (0)