replace websocket with sse
This commit is contained in:
parent
2e015c3f5e
commit
de67f784a3
22
package-lock.json
generated
22
package-lock.json
generated
@ -38,7 +38,6 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"vanilla-calendar-pro": "^3.0.4",
|
||||
"ws": "^8.18.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -7503,27 +7502,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"vanilla-calendar-pro": "^3.0.4",
|
||||
"ws": "^8.18.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /api/team/create/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma';
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client';
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -37,14 +37,14 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: leader.steamId,
|
||||
steamId: leader.steamId,
|
||||
title: 'Team erstellt',
|
||||
message: `Du hast erfolgreich das Team "${teamname}" erstellt.`,
|
||||
},
|
||||
});
|
||||
|
||||
// 📢 WebSocket Nachricht senden
|
||||
await sendServerWebSocketMessage({
|
||||
// 📢 SSE Nachricht senden
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-created',
|
||||
title: 'Team erstellt',
|
||||
message: `Das Team "${teamname}" wurde erstellt.`,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [userId],
|
||||
message: notification.message,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@ -52,7 +52,7 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [steamId],
|
||||
message: notification.message,
|
||||
@ -76,7 +76,7 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [userId],
|
||||
message: notification.message,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { removePlayerFromTeam } from '@/app/lib/removePlayerFromTeam'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -66,7 +66,7 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [steamId],
|
||||
message: notification.message,
|
||||
@ -93,7 +93,7 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [userId],
|
||||
message: notification.message,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /app/api/team/rename/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -16,8 +16,8 @@ export async function POST(req: NextRequest) {
|
||||
data: { name: newName },
|
||||
})
|
||||
|
||||
// 🔔 WebSocket Nachricht an alle User (global)
|
||||
await sendServerWebSocketMessage({
|
||||
// 🔔 SSE Nachricht an alle User (global)
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-renamed',
|
||||
title: 'Team umbenannt!',
|
||||
message: `Das Team wurde umbenannt in "${newName}".`,
|
||||
|
||||
@ -3,7 +3,7 @@ import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/app/lib/auth'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
/* ---- Doppelte Anfrage vermeiden ------------------------------ */
|
||||
const existingInvite = await prisma.teamInvite.findFirst({
|
||||
where: { userId: requesterSteamId, teamId },
|
||||
where: { steamId: requesterSteamId, teamId },
|
||||
})
|
||||
if (existingInvite) {
|
||||
return NextResponse.json({ message: 'Anfrage läuft bereits' }, { status: 200 })
|
||||
@ -46,7 +46,7 @@ export async function POST(req: NextRequest) {
|
||||
/* ---- Invitation anlegen -------------------------------------- */
|
||||
await prisma.teamInvite.create({
|
||||
data: {
|
||||
userId: requesterSteamId, // User.steamId
|
||||
steamId: requesterSteamId, // User.steamId
|
||||
teamId,
|
||||
type: 'team-join-request',
|
||||
},
|
||||
@ -55,7 +55,7 @@ export async function POST(req: NextRequest) {
|
||||
/* ---- Leader benachrichtigen ---------------------------------- */
|
||||
const notification = await prisma.notification.create({
|
||||
data: {
|
||||
userId: team.leaderId!,
|
||||
steamId: team.leaderId!,
|
||||
title: 'Beitrittsanfrage',
|
||||
message: `${session.user.name ?? 'Ein Spieler'} möchte deinem Team beitreten.`,
|
||||
actionType: 'team-join-request',
|
||||
@ -63,8 +63,8 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
/* ---- WebSocket Event (optional) ------------------------------ */
|
||||
await sendServerWebSocketMessage({
|
||||
/* ---- SSE Event (optional) ------------------------------ */
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [team.leaderId],
|
||||
message: notification.message,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@ -39,7 +39,7 @@ export async function POST(req: NextRequest) {
|
||||
select: { name: true },
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-leader-changed',
|
||||
title: 'Neuer Teamleader',
|
||||
message: `${newLeader?.name ?? 'Ein Spieler'} ist jetzt Teamleader.`,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// ✅ /api/team/update-players/route.ts
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
@ -18,7 +18,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const allSteamIds = [...activePlayers, ...inactivePlayers]
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-updated',
|
||||
teamId,
|
||||
targetUserIds: allSteamIds,
|
||||
|
||||
@ -3,7 +3,7 @@ import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { writeFile, mkdir, unlink } from 'fs/promises'
|
||||
import { join, dirname } from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const formData = await req.formData()
|
||||
@ -47,7 +47,7 @@ export async function POST(req: NextRequest) {
|
||||
data: { logo: filename },
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: 'team-logo-updated',
|
||||
title: 'Team-Logo hochgeladen!',
|
||||
message: `Das Teamlogo wurde aktualisiert.`,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /api/user/invitations/[action]/route.ts
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { prisma } from '@/app/lib/prisma'
|
||||
import { sendServerWebSocketMessage } from '@/app/lib/websocket-server-client'
|
||||
import { sendServerSSEMessage } from '@/app/lib/sse-server-client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@ -58,7 +58,7 @@ export async function POST(
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [steamId],
|
||||
message: notification.message,
|
||||
@ -86,7 +86,7 @@ export async function POST(
|
||||
},
|
||||
})
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: notification.actionType ?? 'notification',
|
||||
targetUserIds: [otherUserId],
|
||||
message: notification.message,
|
||||
@ -118,7 +118,7 @@ export async function POST(
|
||||
? 'team-join-request-reject'
|
||||
: 'team-invite-reject'
|
||||
|
||||
await sendServerWebSocketMessage({
|
||||
await sendServerSSEMessage({
|
||||
type: eventType,
|
||||
targetUserIds: [steamId],
|
||||
message: `Einladung zu Team "${team?.name}" wurde abgelehnt.`,
|
||||
|
||||
@ -5,7 +5,7 @@ import { ReactNode, forwardRef, useState, useRef, useEffect } from 'react'
|
||||
type ButtonProps = {
|
||||
title?: string
|
||||
children?: ReactNode
|
||||
onClick?: () => void
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
onToggle?: (open: boolean) => void
|
||||
modalId?: string
|
||||
color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent'
|
||||
@ -13,6 +13,7 @@ type ButtonProps = {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
dropDirection?: "up" | "down" | "auto"
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
@ -26,7 +27,8 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
variant = 'solid',
|
||||
size = 'md',
|
||||
className,
|
||||
dropDirection = "down"
|
||||
dropDirection = "down",
|
||||
disabled = false
|
||||
},
|
||||
ref
|
||||
) {
|
||||
@ -130,12 +132,12 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
}
|
||||
}, [open, dropDirection]);
|
||||
|
||||
const toggle = () => {
|
||||
const toggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const next = !open
|
||||
setOpen(next)
|
||||
onToggle?.(next)
|
||||
onClick?.()
|
||||
}
|
||||
onClick?.(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@ -45,12 +45,15 @@ export default function CompRankBadge({ rank }: Props) {
|
||||
|
||||
return (
|
||||
<Tooltip content={altText}>
|
||||
<Image
|
||||
src={`/assets/img/skillgroups/${imageName}`}
|
||||
alt={altText}
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
<div style={{ position: 'relative', width: 70, height: 40 }}>
|
||||
<Image
|
||||
src={`/assets/img/skillgroups/${imageName}`}
|
||||
alt={altText}
|
||||
fill
|
||||
style={{ objectFit: 'contain' }}
|
||||
sizes="(max-width: 768px) 100px, 70px"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import NotificationDropdown from './NotificationDropdown'
|
||||
import { useWS } from '@/app/lib/wsStore'
|
||||
import { useSSE } from '@/app/lib/useSSEStore'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useTeamManager } from '../hooks/useTeamManager'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@ -21,7 +21,7 @@ export default function NotificationCenter() {
|
||||
const { data: session } = useSession()
|
||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const { socket, connect } = useWS()
|
||||
const { source, connect } = useSSE()
|
||||
const { markAllAsRead, markOneAsRead, handleInviteAction } = useTeamManager({}, null)
|
||||
const router = useRouter()
|
||||
const [previewText, setPreviewText] = useState<string | null>(null)
|
||||
@ -70,57 +70,57 @@ export default function NotificationCenter() {
|
||||
}, [session?.user?.steamId, connect])
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return
|
||||
if (!source) return
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'heartbeat') return
|
||||
const handleEvent = (event: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'heartbeat') return
|
||||
|
||||
const isNotificationType = [
|
||||
'notification',
|
||||
'invitation',
|
||||
'team-invite',
|
||||
'team-joined',
|
||||
'team-member-joined',
|
||||
'team-kick',
|
||||
'team-kick-other',
|
||||
'team-left',
|
||||
'team-member-left',
|
||||
'team-leader-changed',
|
||||
'team-join-request',
|
||||
'expired-sharecode'
|
||||
].includes(data.type)
|
||||
const isNotificationType = [
|
||||
'notification',
|
||||
'invitation',
|
||||
'team-invite',
|
||||
'team-joined',
|
||||
'team-member-joined',
|
||||
'team-kick',
|
||||
'team-kick-other',
|
||||
'team-left',
|
||||
'team-member-left',
|
||||
'team-leader-changed',
|
||||
'team-join-request',
|
||||
'expired-sharecode'
|
||||
].includes(data.type)
|
||||
|
||||
if (!isNotificationType) return
|
||||
if (!isNotificationType) return
|
||||
|
||||
const newNotification: Notification = {
|
||||
id: data.id,
|
||||
text: data.message || 'Neue Benachrichtigung',
|
||||
read: false,
|
||||
actionType: data.actionType,
|
||||
actionData: data.actionData,
|
||||
createdAt: data.createdAt,
|
||||
}
|
||||
const newNotification: Notification = {
|
||||
id: data.id,
|
||||
text: data.message || 'Neue Benachrichtigung',
|
||||
read: false,
|
||||
actionType: data.actionType,
|
||||
actionData: data.actionData,
|
||||
createdAt: data.createdAt,
|
||||
}
|
||||
|
||||
setNotifications(prev => [newNotification, ...prev])
|
||||
setPreviewText(newNotification.text)
|
||||
setShowPreview(true)
|
||||
setAnimateBell(true)
|
||||
setNotifications(prev => [newNotification, ...prev])
|
||||
setPreviewText(newNotification.text)
|
||||
setShowPreview(true)
|
||||
setAnimateBell(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setShowPreview(false)
|
||||
setTimeout(() => {
|
||||
setPreviewText(null)
|
||||
}, 300)
|
||||
setAnimateBell(false)
|
||||
}, 3000)
|
||||
|
||||
|
||||
setShowPreview(false)
|
||||
setTimeout(() => setPreviewText(null), 300)
|
||||
setAnimateBell(false)
|
||||
}, 3000)
|
||||
} catch (err) {
|
||||
console.error('[SSE] Ungültige Nachricht:', event)
|
||||
}
|
||||
}
|
||||
|
||||
socket.addEventListener('message', handleMessage)
|
||||
return () => socket.removeEventListener('message', handleMessage)
|
||||
}, [socket])
|
||||
source.addEventListener('notification', handleEvent)
|
||||
return () => source.removeEventListener('notification', handleEvent)
|
||||
}, [source])
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
|
||||
@ -2,26 +2,24 @@
|
||||
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useEffect } from 'react'
|
||||
import { useWS } from '@/app/lib/wsStore'
|
||||
import { useSSE } from '@/app/lib/useSSEStore'
|
||||
|
||||
export default function WebSocketManager() {
|
||||
export default function SSEManager() {
|
||||
const { data: session } = useSession()
|
||||
const connectWS = useWS((s) => s.connect)
|
||||
const disconnectWS = useWS((s) => s.disconnect)
|
||||
const connect = useSSE((s) => s.connect)
|
||||
const disconnect = useSSE((s) => s.disconnect)
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user?.steamId) return
|
||||
const steamId = session?.user?.steamId
|
||||
if (!steamId) return
|
||||
|
||||
connectWS(session.user.steamId)
|
||||
const eventSource = connect(steamId)
|
||||
if (!eventSource) return
|
||||
|
||||
const socket = useWS.getState().socket
|
||||
if (!socket) return
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
// Typbasierter Event-Dispatch
|
||||
switch (data.type) {
|
||||
case 'invitation':
|
||||
window.dispatchEvent(new CustomEvent('ws-invitation'))
|
||||
@ -60,23 +58,22 @@ export default function WebSocketManager() {
|
||||
window.dispatchEvent(new CustomEvent('ws-team-join-request'))
|
||||
break
|
||||
case 'team-renamed':
|
||||
console.log('[WS] team-renamed', data.teamId)
|
||||
window.dispatchEvent(new CustomEvent('ws-team-renamed', {
|
||||
detail: { teamId: data.teamId }
|
||||
}))
|
||||
break
|
||||
case 'team-logo-updated':
|
||||
window.dispatchEvent(new CustomEvent('ws-team-logo-updated', { detail: { teamId: data.teamId } }))
|
||||
window.dispatchEvent(new CustomEvent('ws-team-logo-updated', {
|
||||
detail: { teamId: data.teamId }
|
||||
}))
|
||||
break
|
||||
|
||||
// Weitere Events hier hinzufügen ...
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] Ungültige Nachricht:', event.data)
|
||||
} catch (err) {
|
||||
console.error('[SSE] Ungültige Nachricht:', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
return () => disconnectWS()
|
||||
return () => disconnect()
|
||||
}, [session?.user?.steamId])
|
||||
|
||||
return null
|
||||
@ -3,7 +3,6 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Button from './Button'
|
||||
import { useWebSocketListener } from '@/app/hooks/useWebSocketListener'
|
||||
import { Team, Player } from '../types/team'
|
||||
import { useLiveTeam } from '../hooks/useLiveTeam'
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import InvitePlayersModal from './InvitePlayersModal'
|
||||
import Modal from './Modal'
|
||||
import { Player, Team } from '../types/team'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useWS } from '@/app/lib/wsStore'
|
||||
import { useWS } from '@/app/lib/useSSEStore'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useTeamManager } from '../hooks/useTeamManager'
|
||||
import Button from './Button'
|
||||
|
||||
@ -8,7 +8,7 @@ import ThemeProvider from "@/theme/theme-provider";
|
||||
import Script from "next/script";
|
||||
import NotificationCenter from './components/NotificationCenter'
|
||||
import Navbar from "./components/Navbar";
|
||||
import WebSocketManager from "./components/WebSocketManager";
|
||||
import SSEManager from "./components/SSEManager";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -40,7 +40,7 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Providers>
|
||||
<WebSocketManager />
|
||||
<SSEManager />
|
||||
{/* Sidebar und Content direkt nebeneinander */}
|
||||
<Sidebar>
|
||||
{children}
|
||||
|
||||
15
src/app/lib/sse-server-client.ts
Normal file
15
src/app/lib/sse-server-client.ts
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
const host = 'localhost'
|
||||
|
||||
export async function sendServerSSEMessage(message: any) {
|
||||
try {
|
||||
console.log('[SSE Client] Nachricht senden:', message)
|
||||
await fetch(`http://${host}:3001/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(message),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[SSE Client] Fehler beim Senden:', error)
|
||||
}
|
||||
}
|
||||
72
src/app/lib/useSSEStore.ts
Normal file
72
src/app/lib/useSSEStore.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
type SSEState = {
|
||||
source: EventSource | null
|
||||
isConnected: boolean
|
||||
connect: (steamId: string) => EventSource | undefined
|
||||
disconnect: () => void
|
||||
}
|
||||
|
||||
export const useSSE = create<SSEState>((set, get) => {
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const connect = (steamId: string): EventSource | undefined => {
|
||||
const current = get().source
|
||||
if (current) return current // bereits verbunden
|
||||
|
||||
const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`)
|
||||
|
||||
source.onopen = () => {
|
||||
console.log('[SSE] Verbunden')
|
||||
set({ source, isConnected: true })
|
||||
}
|
||||
|
||||
source.onmessage = (event) => {
|
||||
console.log('[SSE] Nachricht:', event.data)
|
||||
}
|
||||
|
||||
source.addEventListener('notification', (event) => {
|
||||
try {
|
||||
const data = JSON.parse((event as MessageEvent).data)
|
||||
window.dispatchEvent(new CustomEvent(`sse-${data.type}`, { detail: data }))
|
||||
} catch (err) {
|
||||
console.error('[SSE] Ungültige Nachricht:', event)
|
||||
}
|
||||
})
|
||||
|
||||
source.onerror = (err) => {
|
||||
console.warn('[SSE] Verbindung verloren, versuche Reconnect...')
|
||||
source.close()
|
||||
set({ source: null, isConnected: false })
|
||||
|
||||
if (!reconnectTimeout) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null
|
||||
connect(steamId)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
set({ source })
|
||||
return source // ✅ wichtig
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
const source = get().source
|
||||
if (source) {
|
||||
source.close()
|
||||
}
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout)
|
||||
reconnectTimeout = null
|
||||
}
|
||||
set({ source: null, isConnected: false })
|
||||
}
|
||||
|
||||
return {
|
||||
source: null,
|
||||
isConnected: false,
|
||||
connect,
|
||||
disconnect,
|
||||
}
|
||||
})
|
||||
@ -1,54 +0,0 @@
|
||||
export class WebSocketClient {
|
||||
private ws: WebSocket | null = null
|
||||
private baseUrl: string
|
||||
private steamId: string
|
||||
private listeners: ((data: any) => void)[] = []
|
||||
|
||||
constructor(baseUrl: string, steamId: string) {
|
||||
this.baseUrl = baseUrl
|
||||
this.steamId = steamId
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) return
|
||||
|
||||
const fullUrl = `${this.baseUrl}?steamId=${encodeURIComponent(this.steamId)}`
|
||||
this.ws = new WebSocket(fullUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[WebSocket] Verbunden mit Server.')
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
//console.log('[WebSocket] Nachricht erhalten:', data)
|
||||
this.listeners.forEach((listener) => listener(data))
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn('[WebSocket] Verbindung verloren. Reconnect in 3 Sekunden...')
|
||||
setTimeout(() => this.connect(), 3000)
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('[WebSocket] Fehler:', error)
|
||||
this.ws?.close()
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(callback: (data: any) => void) {
|
||||
this.listeners.push(callback)
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message))
|
||||
} else {
|
||||
console.warn('[WebSocket] Nachricht konnte nicht gesendet werden.')
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.ws?.close()
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
const host = 'localhost' // oder deine IP wie '10.0.1.25'
|
||||
|
||||
export async function sendServerWebSocketMessage(message: any) {
|
||||
try {
|
||||
console.log('[WebSocket Client] Message:', message)
|
||||
await fetch(`http://${host}:3001/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(message)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[WebSocket Client] Fehler beim Senden:', error)
|
||||
}
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
type WSState = {
|
||||
socket: WebSocket | null
|
||||
connect: (steamId: string) => void
|
||||
disconnect: () => void
|
||||
isConnected: boolean
|
||||
}
|
||||
|
||||
export const useWS = create<WSState>((set, get) => {
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const connect = (steamId: string) => {
|
||||
const current = get().socket
|
||||
if (current && (current.readyState === WebSocket.OPEN || current.readyState === WebSocket.CONNECTING)) {
|
||||
return
|
||||
}
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:3001?steamId=${steamId}`)
|
||||
|
||||
ws.onopen = () => {
|
||||
set({ socket: ws, isConnected: true })
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data?.type) {
|
||||
window.dispatchEvent(new CustomEvent(`ws-${data.type}`, { detail: data }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WS] Ungültige Nachricht:', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn('[WS] Verbindung geschlossen. Versuche Reconnect in 3s...')
|
||||
set({ socket: null, isConnected: false })
|
||||
if (!reconnectTimeout) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null
|
||||
connect(steamId)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('[WS] Fehler:', err)
|
||||
ws.close()
|
||||
}
|
||||
|
||||
set({ socket: ws })
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
const socket = get().socket
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.close()
|
||||
}
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout)
|
||||
reconnectTimeout = null
|
||||
}
|
||||
set({ socket: null, isConnected: false })
|
||||
}
|
||||
|
||||
return {
|
||||
socket: null,
|
||||
isConnected: false,
|
||||
connect,
|
||||
disconnect,
|
||||
}
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
import cron from 'node-cron';
|
||||
import { prisma } from '../app/lib/prisma.js';
|
||||
import { runDownloaderForUser } from './runDownloaderForUser.js';
|
||||
import { sendServerWebSocketMessage } from '../app/lib/websocket-server-client.js';
|
||||
import { sendServerWebSocketMessage } from '../app/lib/sse-server-client.js';
|
||||
import { decrypt } from '../app/lib/crypto.js';
|
||||
import { encodeMatch, decodeMatchShareCode } from 'csgo-sharecode';
|
||||
import { log } from '../../scripts/cs2-cron-runner.js';
|
||||
|
||||
87
sse-server.js
Normal file
87
sse-server.js
Normal file
@ -0,0 +1,87 @@
|
||||
const http = require('http')
|
||||
const url = require('url')
|
||||
|
||||
const clients = new Map()
|
||||
|
||||
// HTTP-Server starten
|
||||
const server = http.createServer((req, res) => {
|
||||
const parsedUrl = url.parse(req.url, true)
|
||||
|
||||
// CORS & SSE-Header
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// Verbindung zu einem Client (SSE)
|
||||
if (req.method === 'GET' && req.url.startsWith('/events')) {
|
||||
const steamId = parsedUrl.query.steamId
|
||||
if (!steamId) {
|
||||
res.writeHead(400)
|
||||
res.end('steamId fehlt')
|
||||
return
|
||||
}
|
||||
|
||||
// SSE-Header
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
})
|
||||
|
||||
res.write('\n') // Verbindung offen halten
|
||||
clients.set(steamId, res)
|
||||
console.log(`[SSE] Verbunden: steamId=${steamId}`)
|
||||
|
||||
req.on('close', () => {
|
||||
clients.delete(steamId)
|
||||
console.log(`[SSE] Verbindung geschlossen: steamId=${steamId}`)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Nachricht senden (POST)
|
||||
if (req.method === 'POST' && req.url === '/send') {
|
||||
let body = ''
|
||||
req.on('data', chunk => body += chunk)
|
||||
req.on('end', () => {
|
||||
const message = JSON.parse(body)
|
||||
const isBroadcast = !Array.isArray(message.targetUserIds)
|
||||
const type = message.type || 'notification'
|
||||
|
||||
let sentCount = 0
|
||||
|
||||
for (const [steamId, clientRes] of clients.entries()) {
|
||||
const shouldSend =
|
||||
isBroadcast || (
|
||||
Array.isArray(message.targetUserIds) &&
|
||||
message.targetUserIds.includes(steamId)
|
||||
)
|
||||
|
||||
if (shouldSend) {
|
||||
clientRes.write(`event: ${type}\n`)
|
||||
clientRes.write(`data: ${JSON.stringify(message)}\n\n`)
|
||||
sentCount++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SSE] Nachricht vom Typ "${type}" an ${sentCount} Client(s) gesendet.`)
|
||||
res.writeHead(200)
|
||||
res.end('Nachricht gesendet.')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Unbekannte Route
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
})
|
||||
|
||||
server.listen(3001, () => {
|
||||
console.log('✅ SSE-Server läuft auf Port 3001')
|
||||
})
|
||||
@ -34,7 +34,7 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"websocket-server.js"
|
||||
"sse-server.js"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
const { WebSocketServer } = require('ws')
|
||||
const http = require('http')
|
||||
const url = require('url')
|
||||
|
||||
// WebSocket-Server starten
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
|
||||
// HTTP-Server zum Empfangen von POST-Anfragen zum Versenden von Nachrichten
|
||||
const server = http.createServer((req, res) => {
|
||||
// CORS Header setzen
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||
|
||||
// Preflight-Request
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// Nachricht per POST empfangen
|
||||
if (req.method === 'POST' && req.url === '/send') {
|
||||
let body = ''
|
||||
req.on('data', chunk => { body += chunk })
|
||||
req.on('end', () => {
|
||||
const message = JSON.parse(body)
|
||||
const isBroadcast = !Array.isArray(message.targetUserIds)
|
||||
const type = message.type || 'notification'
|
||||
|
||||
let sentCount = 0
|
||||
|
||||
wss.clients.forEach((client) => {
|
||||
const shouldSend =
|
||||
client.readyState === 1 &&
|
||||
(isBroadcast || (
|
||||
Array.isArray(message.targetUserIds) &&
|
||||
client.steamId &&
|
||||
message.targetUserIds.includes(client.steamId)
|
||||
))
|
||||
|
||||
if (shouldSend) {
|
||||
client.send(JSON.stringify({
|
||||
type,
|
||||
...message
|
||||
}))
|
||||
sentCount++
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`[WS] Nachricht vom Typ "${type}" an ${sentCount} Client(s) gesendet.`)
|
||||
res.writeHead(200)
|
||||
res.end('Nachricht gesendet.')
|
||||
})
|
||||
} else {
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
}
|
||||
})
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const parsedUrl = url.parse(req.url, true)
|
||||
const steamId = parsedUrl.query.steamId
|
||||
|
||||
if (!steamId) {
|
||||
console.warn('[WS] Verbindung ohne steamId - wird geschlossen')
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
|
||||
ws.steamId = steamId
|
||||
console.log(`[WS] Verbunden: steamId=${steamId}`)
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log(`[WS] Verbindung geschlossen für ${steamId}`)
|
||||
})
|
||||
})
|
||||
|
||||
// WebSocket Upgrade akzeptieren
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, req)
|
||||
})
|
||||
})
|
||||
|
||||
// Server starten
|
||||
server.listen(3001, () => {
|
||||
console.log('✅ WebSocket Server läuft auf Port 3001')
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user