feat(frontend): rebuild as SSR with server-authoritative shared state
Rombak total alur data frontend: dari static-export CSR (tiap browser fetch sendiri + akumulasi state voice per-tab) jadi server-side rendering. Frontend (Next.js): - next.config: output export -> standalone; halaman jadi server components - server data layer baru src/lib/api/server.ts (GMW_BACKEND_URL, no window) - dashboard/media/messages/moderation/recordings/voice page -> RSC yang fetch backend di render-time, seed ke client view (SWR fallbackData) - hook-hook utama terima initialData -> first paint data server, revalidate SWR setelahnya, tanpa spinner-blank-load - messages: guild/channel/tab/selected dibaca dari URL di server, page awal di-fetch server-side Shared realtime state (voice) server-authoritative: - backend src/modules/voice/live-speaker.ts: agregat voice_active_user dari gateway jadi snapshot authoritatif (single source of truth semua browser) - GET /api/voice/status kini include activeSpeakers - WS initial states kirim voice_state snapshot saat connect (late join langsung dapat state yang sama, bukan daftar kosong) - useSpeakers seed dari server snapshot + voice_state full-replace + voice_active_user delta upsert Deploy: - flake.nix: frontend package build SSR standalone (server.js wrapper, GMW_FRONTEND_PORT=4017); proxy nginx template proxy / -> Next server, /api + /ws tetap ke backend :4001
This commit is contained in:
@@ -20,9 +20,10 @@ export function hashUserId(userId: string): number {
|
||||
|
||||
const STATUS_KEY = ["voice-status"] as const;
|
||||
|
||||
export function useVoiceStatus() {
|
||||
export function useVoiceStatus(initialData?: VoiceStatus) {
|
||||
return useSWR<VoiceStatus>(STATUS_KEY, () => voiceApi.getStatus(), {
|
||||
shouldRetryOnError: false,
|
||||
fallbackData: initialData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,10 +33,31 @@ export function useVoiceChannels(guildId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useSpeakers() {
|
||||
const [speakers, setSpeakers] = useState<ActiveSpeaker[]>([]);
|
||||
/**
|
||||
* Live shared speaker state.
|
||||
*
|
||||
* Seeded from the server-authored snapshot (`initial` — the voice status the
|
||||
* server rendered, which includes the authoritative active speakers). From
|
||||
* there the WS keeps it converged across ALL users:
|
||||
* - `voice_state` → authoritative FULL replacement (e.g. a late join seeds
|
||||
* every client with the same list);
|
||||
* - `voice_active_user` → incremental upsert of a single speaker delta.
|
||||
*
|
||||
* This replaces the old per-browser model where each tab accumulated speakers
|
||||
* only from events it happened to receive while mounted.
|
||||
*/
|
||||
export function useSpeakers(initialStatusActive?: ActiveSpeaker[]) {
|
||||
const [speakers, setSpeakers] = useState<ActiveSpeaker[]>(
|
||||
initialStatusActive ?? [],
|
||||
);
|
||||
|
||||
const subscribe = useCallback((ws: WsHook) => {
|
||||
const unsubSnapshot = ws.on("voice_state", (data) => {
|
||||
const state = data as { activeSpeakers?: ActiveSpeaker[] };
|
||||
if (Array.isArray(state?.activeSpeakers)) {
|
||||
setSpeakers(state.activeSpeakers);
|
||||
}
|
||||
});
|
||||
const unsub = ws.on("voice_active_user", (data) => {
|
||||
const speaker = data as ActiveSpeaker;
|
||||
setSpeakers((prev) => {
|
||||
@@ -49,6 +71,7 @@ export function useSpeakers() {
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
unsubSnapshot();
|
||||
unsub();
|
||||
setSpeakers([]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user