replace websocket with sse

This commit is contained in:
Linrador 2025-06-18 17:27:45 +02:00
parent 2e015c3f5e
commit de67f784a3
28 changed files with 285 additions and 363 deletions

22
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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.`,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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}".`,

View File

@ -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,

View File

@ -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.`,

View File

@ -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,

View File

@ -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.`,

View File

@ -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.`,

View File

@ -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

View File

@ -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>
);
}

View File

@ -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">

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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}

View 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)
}
}

View 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,
}
})

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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,
}
})

View File

@ -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
View 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')
})

View File

@ -34,7 +34,7 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"websocket-server.js"
"sse-server.js"
],
"exclude": ["node_modules"]
}

View File

@ -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')
})