Files
the-sunken-reach/frontend/src/hooks/useCampaignSocket.ts
T
2026-09-22 16:27:53 +02:00

56 lines
2.0 KiB
TypeScript

import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { io, type Socket } from 'socket.io-client';
import { API_URL } from '../api/client';
import { useAuth } from '../auth/AuthContext';
let sharedSocket: Socket | null = null;
function getSocket(token: string): Socket {
if (!sharedSocket) {
sharedSocket = io(API_URL, { auth: { token }, transports: ['websocket', 'polling'] });
}
return sharedSocket;
}
// Joins the Socket.io room for a campaign and keeps React Query's cache in
// sync with server-pushed events (HP/condition changes, encounter rounds,
// new characters) so every open tab in the campaign updates live.
export function useCampaignSocket(campaignId: string | undefined) {
const { token } = useAuth();
const qc = useQueryClient();
useEffect(() => {
if (!campaignId || !token) return;
const socket = getSocket(token);
socket.emit('campaign:join', campaignId);
const onCharacterUpdated = () => {
qc.invalidateQueries({ queryKey: ['campaign', campaignId, 'characters'] });
qc.invalidateQueries({ queryKey: ['character'] });
};
const onCharacterCreated = () => {
qc.invalidateQueries({ queryKey: ['campaign', campaignId, 'characters'] });
};
const onParticipantUpdated = () => {
qc.invalidateQueries({ queryKey: ['encounter'] });
};
const onRoundAdvanced = () => {
qc.invalidateQueries({ queryKey: ['encounter'] });
};
socket.on('character:updated', onCharacterUpdated);
socket.on('character:created', onCharacterCreated);
socket.on('encounter:participant-updated', onParticipantUpdated);
socket.on('encounter:round-advanced', onRoundAdvanced);
return () => {
socket.emit('campaign:leave', campaignId);
socket.off('character:updated', onCharacterUpdated);
socket.off('character:created', onCharacterCreated);
socket.off('encounter:participant-updated', onParticipantUpdated);
socket.off('encounter:round-advanced', onRoundAdvanced);
};
}, [campaignId, token, qc]);
}