From a206684e83c930ac86d76b2c729e11ffe73cbf03 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Fri, 5 Dec 2025 01:18:16 +0700 Subject: [PATCH] UPDATE: Add Worker, pending map to fix bug missing data --- src/app/page.tsx | 4 +- src/components/actionbar/index.tsx | 4 +- src/components/card/characterCard.tsx | 4 +- src/components/enemybar/index.tsx | 2 +- src/components/header/index.tsx | 10 +- src/components/lineupbar/index.tsx | 8 +- src/global.d.ts | 4 + src/lib/socket.ts | 375 ++++++++++++++------------ 8 files changed, 220 insertions(+), 191 deletions(-) create mode 100644 src/global.d.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 2af7d74..9a7ffce 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -47,7 +47,7 @@ export default function Home() { }, [expandedCharts]); return ( -
+
@@ -76,7 +76,7 @@ export default function Home() {
{enemyDetail && } -
+
-

+

{transI18n("turnDetail").toUpperCase()}

diff --git a/src/components/card/characterCard.tsx b/src/components/card/characterCard.tsx index 018c983..6e08259 100644 --- a/src/components/card/characterCard.tsx +++ b/src/components/card/characterCard.tsx @@ -19,9 +19,9 @@ export default function CharacterCard({ data }: CharacterCardProps) { const { avatarDetail } = useBattleDataStore() return ( -
  • +
  • {enemyDetail && Object.values(enemyDetail).filter((enemy) => (enemy.stats?.AV > 0 && enemy.stats.HP <= enemy.maxHP)).map((enemy, uid) => ( -
    +
    {listEnemy.find((monster) => monster.child.includes(enemy.id))?.icon?.split("/")?.pop()?.replace(".png", "") && ( diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index 3d43936..5c8cefe 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -86,7 +86,7 @@ export default function Header() { return () => { disconnectSocket() }; - }, [setStatus]); + }, []); const handleConnect = () => { if (!host || !port) { @@ -217,7 +217,7 @@ export default function Header() {

    Firefly Analy - + sis

    @@ -227,7 +227,7 @@ export default function Header() { {version && (
    -
    +
    {version} @@ -380,7 +380,7 @@ export default function Header() { {/* GitHub Link */}
    -

    +

    {transI18n("socketConnection").toUpperCase()}

    diff --git a/src/components/lineupbar/index.tsx b/src/components/lineupbar/index.tsx index 4069d20..301ac98 100644 --- a/src/components/lineupbar/index.tsx +++ b/src/components/lineupbar/index.tsx @@ -67,7 +67,7 @@ export default function LineupBar() { return (
    handleShow("character_detail_modal", item)} - className={`cursor-pointer flex-shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),_0_0_20px_5px_rgba(59,130,246,0.3)]" : "" + className={`cursor-pointer shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),0_0_20px_5px_rgba(59,130,246,0.3)]" : "" }`} > @@ -129,7 +129,7 @@ export default function LineupBar() { {/* Character Detail Modal */} -
    +
    diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..444f793 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: { [className: string]: string }; + export default content; +} diff --git a/src/lib/socket.ts b/src/lib/socket.ts index 1f51b74..f308bdb 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -1,59 +1,149 @@ +// File: socket-client.ts + import { io, Socket } from "socket.io-client"; import useSocketStore from "@/stores/socketSettingStore"; -import { toast } from 'react-toastify'; +import { toast } from "react-toastify"; import useBattleDataStore from "@/stores/battleDataStore"; -import { BattleBeginType } from "@/types"; +import type { + AvatarAnalysisJson, + BattleBeginType, + BattleEndType, + DamageType, + EntityDefeatedType, + InitializeEnemyType, + SetBattleLineupType, + StatChangeType, + TurnBeginType, + TurnEndType, + UpdateCycleType, + UpdateTeamFormationType, + UpdateWaveType, + UseSkillType, + VersionType, +} from "@/types"; let socket: Socket | null = null; - -const notify = (msg: string, type: 'info' | 'success' | 'error' = 'info') => { - if (type === 'success') toast.success(msg); - else if (type === 'error') toast.error(msg); +const notify = (msg: string, type: "info" | "success" | "error" = "info") => { + if (type === "success") toast.success(msg); + else if (type === "error") toast.error(msg); else toast.info(msg); }; -function safeParse(json: unknown | string) { - try { - return typeof json === "string" ? JSON.parse(json) : json; - } catch (e) { - console.error("JSON parse error:", e, json); - return null; - } +interface SocketEvents { + Connected: VersionType; + OnBattleBegin: BattleBeginType; + OnSetBattleLineup: SetBattleLineupType; + OnDamage: DamageType; + OnTurnBegin: TurnBeginType; + OnTurnEnd: TurnEndType; + OnEntityDefeated: EntityDefeatedType; + OnUseSkill: UseSkillType; + OnUpdateWave: UpdateWaveType; + OnUpdateCycle: UpdateCycleType; + OnStatChange: StatChangeType; + OnUpdateTeamFormation: UpdateTeamFormationType; + OnInitializeEnemy: InitializeEnemyType; + OnBattleEnd: BattleEndType; + OnCreateBattle: AvatarAnalysisJson[]; + Error: string; } +type ListenerMap = { + [K in keyof SocketEvents]: (payload: SocketEvents[K]) => void; +}; -export const connectSocket = (): Socket => { +let listeners: ListenerMap | null = null; + +let workerPool: Worker[] | null = null; +let nextWorker = 0; +let reqId = 1; +const pendingMap = new Map void>(); + +const createWorkerPool = (size = Math.max(1, (navigator.hardwareConcurrency || 4) - 1)) => { + if (workerPool) return workerPool; + const code = `self.onmessage = function(e) { try { const { id, raw } = e.data; const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; self.postMessage({ id, parsed }); } catch (err) { self.postMessage({ id, err: err && err.message ? err.message : String(err) }); } }`; + const blob = new Blob([code], { type: 'application/javascript' }); + const url = URL.createObjectURL(blob); + workerPool = Array.from({ length: size }).map(() => new Worker(url)); + workerPool.forEach((w) => { + w.onmessage = (ev) => { + const { id, parsed, err } = ev.data as { id: number; parsed: unknown; err?: string }; + const cb = pendingMap.get(id); + if (cb) { + cb({ parsed: parsed ?? null, err }); + pendingMap.delete(id); + } + }; + w.onerror = (ev) => { + console.error('worker error', ev.message); + }; + }); + return workerPool; +}; + +const parseWithWorker = (raw: unknown): Promise<{ parsed: unknown | null; err?: string }> => { + if (!workerPool) createWorkerPool(); + if (!workerPool) return Promise.resolve({ parsed: null, err: 'no worker' }); + const id = reqId++; + return new Promise((resolve) => { + pendingMap.set(id, resolve); + const w = workerPool![nextWorker]; + nextWorker = (nextWorker + 1) % workerPool!.length; + try { + w.postMessage({ id, raw }); + } catch (err) { + pendingMap.delete(id); + resolve({ parsed: null, err: err && (err as Error).message ? (err as Error).message : String(err) }); + } + }); +}; + +let eventQueue: { name: keyof SocketEvents; data: unknown }[] = []; +let rafScheduled = false; +const MAX_QUEUE = 5000; + +const flushQueue = () => { + const items = eventQueue.splice(0, eventQueue.length); + if (items.length === 0) return; + const l = listeners; + if (!l) return; + for (const it of items) { + try { + l[it.name](it.data as never); + } catch (err) { + console.error('listener handler error', err); + } + } +}; + +const scheduleFlush = () => { + if (rafScheduled) return; + rafScheduled = true; + requestAnimationFrame(() => { + rafScheduled = false; + flushQueue(); + }); +}; + +const safeEnqueue = (name: keyof SocketEvents, data: unknown) => { + if (eventQueue.length > MAX_QUEUE) { + eventQueue.shift(); + console.warn('eventQueue overflow, dropping oldest'); + } + eventQueue.push({ name, data }); + scheduleFlush(); +}; + +export const connectSocket = async (): Promise => { const { host, port, connectionType, setStatus } = useSocketStore.getState(); - const { - onConnectedService, - onBattleBeginService, - onSetBattleLineupService, - onDamageService, - onTurnBeginService, - onTurnEndService, - onEntityDefeatedService, - onUseSkillService, - onUpdateWaveService, - onUpdateCycleService, - onStatChange, - onUpdateTeamFormation, - onInitializeEnemyService, - onBattleEndService, - onCreateBattleService, - } = useBattleDataStore.getState(); + const battle = useBattleDataStore.getState(); let url = `${host}:${port}`; - if (connectionType === "Native") { - url = "http://localhost:1305" - } - else if (connectionType === "PS") { - url = "http://localhost:21000" - } + if (connectionType === "Native") url = "http://localhost:1305"; + else if (connectionType === "PS") url = "http://localhost:21000"; - if (socket) { - socket.disconnect(); - } + if (socket) socket.disconnect(); socket = io(url, { reconnectionAttempts: 5, @@ -62,156 +152,91 @@ export const connectSocket = (): Socket => { }); socket.on("connect", () => { - console.log("Socket connected"); setStatus(true); + notify(`Connected: ${socket?.id}`, "success"); }); - socket.on("disconnect", () => { - console.log("Socket disconnected"); - setStatus(false); - }); + socket.on("disconnect", () => setStatus(false)); + socket.on("connect_error", () => setStatus(false)); + socket.on("connect_timeout", () => setStatus(false)); + socket.on("reconnect_failed", () => setStatus(false)); - socket.on("connect_error", (err) => { - console.error("Connection error:", err); - setStatus(false); - }); - - socket.on("connect_timeout", () => { - console.warn("Connection timeout"); - setStatus(false); - }); - - socket.on("reconnect_failed", () => { - console.error("Reconnect failed"); - setStatus(false); - }); - - const onConnect = () => { - setStatus(true); - notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success'); + listeners = { + Connected: (payload) => battle.onConnectedService(payload), + OnBattleBegin: (payload) => { + notify("Battle Started!", "info"); + battle.onBattleBeginService(payload); + }, + OnSetBattleLineup: (payload) => battle.onSetBattleLineupService(payload), + OnDamage: (payload) => battle.onDamageService(payload), + OnTurnBegin: (payload) => battle.onTurnBeginService(payload), + OnTurnEnd: (payload) => battle.onTurnEndService(payload), + OnEntityDefeated: (payload) => battle.onEntityDefeatedService(payload), + OnUseSkill: (payload) => battle.onUseSkillService(payload), + OnUpdateWave: (payload) => battle.onUpdateWaveService(payload), + OnUpdateCycle: (payload) => battle.onUpdateCycleService(payload), + OnStatChange: (payload) => battle.onStatChange(payload), + OnUpdateTeamFormation: (payload) => battle.onUpdateTeamFormation(payload), + OnInitializeEnemy: (payload) => battle.onInitializeEnemyService(payload), + OnBattleEnd: (payload) => battle.onBattleEndService(payload), + OnCreateBattle: (payload) => battle.onCreateBattleService(payload), + Error: (msg) => console.error("Server Error:", msg), }; - const onBattleBegin = (data: BattleBeginType) => { - notify("Battle Started!", "info") - onBattleBeginService(data) - } + const register = (eventName: string) => { + socket?.on(eventName, async (raw: unknown) => { + if (!listeners) return; + if (workerPool) { + const res = await parseWithWorker(raw); + if (res.err) { + safeEnqueue("Error", `Worker parse error for ${eventName}: ${res.err}`); + } else if (res.parsed !== null) { + safeEnqueue(eventName as keyof SocketEvents, res.parsed); + } + } else { + try { + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + safeEnqueue(eventName as keyof SocketEvents, parsed); + } catch (err) { + safeEnqueue("Error", `Parse error for ${eventName}: ${err && (err as Error).message ? (err as Error).message : String(err)}`); + } + } + }); + }; - if (isSocketConnected()) onConnect(); - socket.on("Connected", (json) => { - const data = safeParse(json); - if (data) onConnectedService(data); - }); - socket.on("OnBattleBegin", (json) => { - const data = safeParse(json); - if (data) onBattleBegin(data); - }); - socket.on("OnSetBattleLineup", (json) => { - const data = safeParse(json); - if (data) onSetBattleLineupService(data); - }); - socket.on("OnDamage", (json) => { - const data = safeParse(json); - if (data) onDamageService(data); - }); - socket.on("OnTurnBegin", (json) => { - const data = safeParse(json); - if (data) onTurnBeginService(data); - }); - socket.on("OnTurnEnd", (json) => { - const data = safeParse(json); - if (data) onTurnEndService(data); - }); - socket.on("OnEntityDefeated", (json) => { - const data = safeParse(json); - if (data) onEntityDefeatedService(data); - }); - socket.on("OnUseSkill", (json) => { - const data = safeParse(json); - if (data) onUseSkillService(data); - }); - socket.on("OnUpdateWave", (json) => { - const data = safeParse(json); - if (data) onUpdateWaveService(data); - }); - socket.on("OnUpdateCycle", (json) => { - const data = safeParse(json); - if (data) onUpdateCycleService(data); - }); - socket.on("OnStatChange", (json) => { - const data = safeParse(json); - if (data) onStatChange(data); - }); - socket.on("OnUpdateTeamFormation", (json) => { - const data = safeParse(json); - if (data) onUpdateTeamFormation(data); - }); - socket.on("OnInitializeEnemy", (json) => { - const data = safeParse(json); - if (data) onInitializeEnemyService(data); - }); - socket.on("OnBattleEnd", (json) => { - const data = safeParse(json); - if (data) onBattleEndService(data); - }); - socket.on("OnCreateBattle", (json) => { - const data = safeParse(json); - if (data) onCreateBattleService(data); - }); - socket.on("Error", (msg: string) => { - console.error("Server Error:", msg); - }); + Object.keys(listeners).forEach((k) => register(k)); + + createWorkerPool(); return socket; }; export const disconnectSocket = (): void => { - const { - onConnectedService, - onBattleBeginService, - onSetBattleLineupService, - onDamageService, - onTurnBeginService, - onTurnEndService, - onEntityDefeatedService, - onUseSkillService, - onUpdateWaveService, - onUpdateCycleService, - onStatChange, - onUpdateTeamFormation, - onInitializeEnemyService, - onBattleEndService, - onCreateBattleService, - } = useBattleDataStore.getState(); - const onBattleBegin = (data: BattleBeginType) => { - notify("Battle Started!", "info") - onBattleBeginService(data) + if (!socket) return; + if (listeners) { + Object.keys(listeners).forEach((eventName) => { + socket?.off(eventName); + }); } - if (socket) { - socket.off("Connected", (json) => onConnectedService(JSON.parse(json))); - socket.off("OnBattleBegin", (json) => onBattleBegin(JSON.parse(json))); - socket.off("OnSetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json))); - socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json))); - socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json))); - socket.off("OnEntityDefeated", (json) => onEntityDefeatedService(JSON.parse(json))); - socket.off("OnDamage", (json) => onDamageService(JSON.parse(json))); - socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json))); - socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json))); - socket.off('OnUpdateCycle', (json) => onUpdateCycleService(JSON.parse(json))); - socket.off('OnUpdateWave', (json) => onUpdateWaveService(JSON.parse(json))); - socket.off('OnCreateBattle', (json) => onCreateBattleService(JSON.parse(json))); - socket.off('OnStatChange', (json) => onStatChange(JSON.parse(json))); - socket.off('OnUpdateTeamFormation', (json) => onUpdateTeamFormation(JSON.parse(json))); - socket.off('OnInitializeEnemy', (json) => onInitializeEnemyService(JSON.parse(json))); - socket.offAny(); - socket.disconnect(); - useSocketStore.getState().setStatus(false); + socket.disconnect(); + useSocketStore.getState().setStatus(false); + listeners = null; + if (workerPool) { + workerPool.forEach((w) => w.terminate()); + workerPool = null; } + pendingMap.clear(); + eventQueue = []; }; -export const isSocketConnected = (): boolean => { - return socket?.connected || false; +export const isSocketConnected = (): boolean => socket?.connected || false; +export const getSocket = (): Socket | null => socket; + +export const setWorkerPoolSize = (size: number) => { + if (workerPool) { + workerPool.forEach((w) => w.terminate()); + workerPool = null; + } + createWorkerPool(Math.max(1, size)); }; -export const getSocket = (): Socket | null => { - return socket; -};