feat: move to Socket.IO

This commit is contained in:
Marco Beretta 2024-12-30 01:47:21 +01:00 committed by Danny Avila
parent 7717d3a514
commit 9c0c341dee
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
9 changed files with 413 additions and 134 deletions

View file

@ -98,13 +98,13 @@
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"socket.io": "^4.8.1",
"tiktoken": "^1.0.15", "tiktoken": "^1.0.15",
"traverse": "^0.6.7", "traverse": "^0.6.7",
"ua-parser-js": "^1.0.36", "ua-parser-js": "^1.0.36",
"winston": "^3.11.0", "winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1", "winston-daily-rotate-file": "^4.7.1",
"wrtc": "^0.4.7", "wrtc": "^0.4.7",
"ws": "^8.18.0",
"youtube-transcript": "^1.2.1", "youtube-transcript": "^1.2.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },

View file

@ -15,7 +15,7 @@ const { connectDb, indexSync } = require('~/lib/db');
const { isEnabled } = require('~/server/utils'); const { isEnabled } = require('~/server/utils');
const { ldapLogin } = require('~/strategies'); const { ldapLogin } = require('~/strategies');
const { logger } = require('~/config'); const { logger } = require('~/config');
const { WebSocketService } = require('./services/WebSocket/WebSocketServer'); const { SocketIOService } = require('./services/WebSocket/WebSocketServer');
const validateImageRequest = require('./middleware/validateImageRequest'); const validateImageRequest = require('./middleware/validateImageRequest');
const errorController = require('./controllers/ErrorController'); const errorController = require('./controllers/ErrorController');
const configureSocialLogins = require('./socialLogins'); const configureSocialLogins = require('./socialLogins');
@ -48,7 +48,7 @@ const startServer = async () => {
}), }),
); );
new WebSocketService(server); new SocketIOService(server);
await AppService(app); await AppService(app);
@ -149,7 +149,7 @@ const startServer = async () => {
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
} }
logger.info(`WebSocket endpoint: ws://${host}:${port}`); logger.info(`Socket.IO endpoint: http://${host}:${port}`);
}); });
}; };

View file

@ -4,15 +4,16 @@ const router = express.Router();
router.get('/', optionalJwtAuth, async (req, res) => { router.get('/', optionalJwtAuth, async (req, res) => {
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const useSSL = isProduction && process.env.SERVER_DOMAIN?.startsWith('https');
const protocol = useSSL ? 'wss' : 'ws'; const protocol = isProduction && req.secure ? 'https' : 'http';
const serverDomain = process.env.SERVER_DOMAIN const serverDomain = process.env.SERVER_DOMAIN
? process.env.SERVER_DOMAIN.replace(/^https?:\/\//, '') ? process.env.SERVER_DOMAIN.replace(/^https?:\/\//, '')
: req.headers.host; : req.headers.host;
const wsUrl = `${protocol}://${serverDomain}/ws`;
res.json({ url: wsUrl }); const socketIoUrl = `${protocol}://${serverDomain}`;
res.json({ url: socketIoUrl });
}); });
module.exports = router; module.exports = router;

View file

@ -1,10 +1,10 @@
const { WebSocketServer } = require('ws'); const { Server } = require('socket.io');
const { RTCPeerConnection } = require('wrtc'); const { RTCPeerConnection } = require('wrtc');
module.exports.WebSocketService = class { module.exports.SocketIOService = class {
constructor(server) { constructor(httpServer) {
this.wss = new WebSocketServer({ server, path: '/ws' }); this.io = new Server(httpServer, { path: '/socket.io' });
this.log('Server initialized'); this.log('Socket.IO Server initialized');
this.activeClients = new Map(); this.activeClients = new Map();
this.iceServers = [ this.iceServers = [
{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' },
@ -14,14 +14,14 @@ module.exports.WebSocketService = class {
} }
log(msg) { log(msg) {
console.log(`[WSS ${new Date().toISOString()}] ${msg}`); console.log(`[Socket.IO ${new Date().toISOString()}] ${msg}`);
} }
setupHandlers() { setupHandlers() {
this.wss.on('connection', (ws) => { this.io.on('connection', (socket) => {
const clientId = Date.now().toString(); const clientId = socket.id;
this.activeClients.set(clientId, { this.activeClients.set(clientId, {
ws, socket,
state: 'idle', state: 'idle',
audioBuffer: [], audioBuffer: [],
currentTranscription: '', currentTranscription: '',
@ -30,44 +30,19 @@ module.exports.WebSocketService = class {
this.log(`Client connected: ${clientId}`); this.log(`Client connected: ${clientId}`);
ws.on('message', async (raw) => { socket.on('call-start', () => this.handleCallStart(clientId));
let message; socket.on('audio-chunk', (data) => this.handleAudioChunk(clientId, data));
try { socket.on('processing-start', () => this.processAudioStream(clientId));
message = JSON.parse(raw); socket.on('audio-received', () => this.confirmAudioReceived(clientId));
} catch { socket.on('call-ended', () => this.handleCallEnd(clientId));
return;
}
switch (message.type) { socket.on('disconnect', () => {
case 'call-start':
this.handleCallStart(clientId);
break;
case 'audio-chunk':
await this.handleAudioChunk(clientId, message.data);
break;
case 'processing-start':
await this.processAudioStream(clientId);
break;
case 'audio-received':
this.confirmAudioReceived(clientId);
break;
case 'call-ended':
this.handleCallEnd(clientId);
break;
}
});
ws.on('close', () => {
this.handleCallEnd(clientId); this.handleCallEnd(clientId);
this.activeClients.delete(clientId); this.activeClients.delete(clientId);
this.log(`Client disconnected: ${clientId}`); this.log(`Client disconnected: ${clientId}`);
}); });
ws.on('error', (error) => { socket.on('error', (error) => {
this.log(`Error for client ${clientId}: ${error.message}`); this.log(`Error for client ${clientId}: ${error.message}`);
this.handleCallEnd(clientId); this.handleCallEnd(clientId);
}); });
@ -104,12 +79,7 @@ module.exports.WebSocketService = class {
peerConnection.onicecandidate = (event) => { peerConnection.onicecandidate = (event) => {
if (event.candidate) { if (event.candidate) {
client.ws.send( client.socket.emit('ice-candidate', { candidate: event.candidate });
JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate,
}),
);
} }
}; };
@ -117,12 +87,7 @@ module.exports.WebSocketService = class {
try { try {
const offer = await peerConnection.createOffer(); const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer); await peerConnection.setLocalDescription(offer);
client.ws.send( client.socket.emit('webrtc-offer', { sdp: peerConnection.localDescription });
JSON.stringify({
type: 'webrtc-offer',
sdp: peerConnection.localDescription,
}),
);
} catch (error) { } catch (error) {
this.log(`Negotiation failed for ${clientId}: ${error}`); this.log(`Negotiation failed for ${clientId}: ${error}`);
} }
@ -142,7 +107,7 @@ module.exports.WebSocketService = class {
} }
client.audioBuffer.push(data); client.audioBuffer.push(data);
client.ws.send(JSON.stringify({ type: 'audio-received' })); client.socket.emit('audio-received');
} }
async processAudioStream(clientId) { async processAudioStream(clientId) {
@ -155,28 +120,13 @@ module.exports.WebSocketService = class {
try { try {
// Process transcription // Process transcription
client.ws.send( client.socket.emit('transcription', { data: 'Processing audio...' });
JSON.stringify({
type: 'transcription',
data: 'Processing audio...',
}),
);
// Stream LLM response // Stream LLM response
client.ws.send( client.socket.emit('llm-response', { data: 'Processing response...' });
JSON.stringify({
type: 'llm-response',
data: 'Processing response...',
}),
);
// Stream TTS chunks // Stream TTS chunks
client.ws.send( client.socket.emit('tts-chunk', { data: 'audio_data_here' });
JSON.stringify({
type: 'tts-chunk',
data: 'audio_data_here',
}),
);
} catch (error) { } catch (error) {
this.log(`Processing error for client ${clientId}: ${error.message}`); this.log(`Processing error for client ${clientId}: ${error.message}`);
} finally { } finally {
@ -191,12 +141,7 @@ module.exports.WebSocketService = class {
return; return;
} }
client.ws.send( client.socket.emit('audio-received', { data: null });
JSON.stringify({
type: 'audio-received',
data: null,
}),
);
} }
handleCallEnd(clientId) { handleCallEnd(clientId) {

View file

@ -97,6 +97,7 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-supersub": "^1.0.0", "remark-supersub": "^1.0.0",
"socket.io-client": "^4.8.1",
"sse.js": "^2.5.0", "sse.js": "^2.5.0",
"tailwind-merge": "^1.9.1", "tailwind-merge": "^1.9.1",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",

View file

@ -1,15 +1,51 @@
import React from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Mic, Phone, PhoneOff } from 'lucide-react'; import { Phone, PhoneOff } from 'lucide-react';
import { OGDialog, OGDialogContent, Button } from '~/components'; import { OGDialog, OGDialogContent, Button } from '~/components';
import { useWebRTC, useWebSocket, useCall } from '~/hooks'; import { useWebSocket, useCall } from '~/hooks';
import store from '~/store'; import store from '~/store';
export const Call: React.FC = () => { export const Call: React.FC = () => {
const { isConnected } = useWebSocket(); const { isConnected, sendMessage } = useWebSocket();
const { isCalling, startCall, hangUp } = useCall(); const { isCalling, isProcessing, startCall, hangUp } = useCall();
const [open, setOpen] = useRecoilState(store.callDialogOpen(0)); const [open, setOpen] = useRecoilState(store.callDialogOpen(0));
const [eventLog, setEventLog] = React.useState<string[]>([]);
const logEvent = (message: string) => {
console.log(message);
setEventLog((prev) => [...prev, message]);
};
React.useEffect(() => {
if (isConnected) {
logEvent('Connected to server.');
} else {
logEvent('Disconnected from server.');
}
}, [isConnected]);
React.useEffect(() => {
if (isCalling) {
logEvent('Call started.');
} else if (isProcessing) {
logEvent('Processing audio...');
} else {
logEvent('Call ended.');
}
}, [isCalling, isProcessing]);
const handleStartCall = () => {
logEvent('Attempting to start call...');
startCall();
};
const handleHangUp = () => {
logEvent('Attempting to hang up call...');
hangUp();
};
return ( return (
<OGDialog open={open} onOpenChange={setOpen}> <OGDialog open={open} onOpenChange={setOpen}>
<OGDialogContent className="w-96 p-8"> <OGDialogContent className="w-96 p-8">
@ -27,24 +63,34 @@ export const Call: React.FC = () => {
</span> </span>
</div> </div>
{!isCalling ? ( {isCalling ? (
<Button <Button
onClick={startCall} onClick={handleHangUp}
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white hover:bg-red-600"
>
<PhoneOff size={20} />
<span>End Call</span>
</Button>
) : (
<Button
onClick={handleStartCall}
disabled={!isConnected} disabled={!isConnected}
className="flex items-center gap-2 rounded-full bg-green-500 px-6 py-3 text-white hover:bg-green-600 disabled:opacity-50" className="flex items-center gap-2 rounded-full bg-green-500 px-6 py-3 text-white hover:bg-green-600 disabled:opacity-50"
> >
<Phone size={20} /> <Phone size={20} />
<span>Start Call</span> <span>Start Call</span>
</Button> </Button>
) : (
<Button
onClick={hangUp}
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white hover:bg-red-600"
>
<PhoneOff size={20} />
<span>End Call</span>
</Button>
)} )}
{/* Debugging Information */}
<div className="mt-4 w-full rounded-md bg-gray-100 p-4 shadow-sm">
<h3 className="mb-2 text-lg font-medium">Event Log</h3>
<ul className="h-32 overflow-y-auto text-xs text-gray-600">
{eventLog.map((log, index) => (
<li key={index}>{log}</li>
))}
</ul>
</div>
</div> </div>
</OGDialogContent> </OGDialogContent>
</OGDialog> </OGDialog>

View file

@ -7,7 +7,7 @@ const SILENCE_THRESHOLD = -50;
const SILENCE_DURATION = 1000; const SILENCE_DURATION = 1000;
const useCall = () => { const useCall = () => {
const { sendMessage: wsMessage } = useWebSocket(); const { sendMessage: wsMessage, isConnected } = useWebSocket();
const [isCalling, setIsCalling] = useState(false); const [isCalling, setIsCalling] = useState(false);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const audioContextRef = useRef<AudioContext | null>(null); const audioContextRef = useRef<AudioContext | null>(null);
@ -23,9 +23,7 @@ const useCall = () => {
} }
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
// Send audio through WebRTC data channel
webrtcServiceRef.current?.sendAudioChunk(audioBlob); webrtcServiceRef.current?.sendAudioChunk(audioBlob);
// Signal processing start via WebSocket
wsMessage({ type: 'processing-start' }); wsMessage({ type: 'processing-start' });
audioChunksRef.current = []; audioChunksRef.current = [];
@ -34,17 +32,18 @@ const useCall = () => {
const handleRTCMessage = useCallback((message: RTCMessage) => { const handleRTCMessage = useCallback((message: RTCMessage) => {
if (message.type === 'audio-received') { if (message.type === 'audio-received') {
// Backend confirmed audio receipt
setIsProcessing(true); setIsProcessing(true);
} }
}, []); }, []);
const startCall = useCallback(async () => { const startCall = useCallback(async () => {
// Initialize WebRTC with message handler if (!isConnected) {
return;
}
webrtcServiceRef.current = new WebRTCService(handleRTCMessage); webrtcServiceRef.current = new WebRTCService(handleRTCMessage);
await webrtcServiceRef.current.initializeCall(); await webrtcServiceRef.current.initializeCall();
// Signal call start via WebSocket
wsMessage({ type: 'call-start' }); wsMessage({ type: 'call-start' });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@ -53,7 +52,6 @@ const useCall = () => {
analyserRef.current = audioContextRef.current.createAnalyser(); analyserRef.current = audioContextRef.current.createAnalyser();
source.connect(analyserRef.current); source.connect(analyserRef.current);
// Start VAD monitoring
intervalRef.current = window.setInterval(() => { intervalRef.current = window.setInterval(() => {
if (!analyserRef.current || !isCalling) { if (!analyserRef.current || !isCalling) {
return; return;
@ -76,7 +74,7 @@ const useCall = () => {
}, 100); }, 100);
setIsCalling(true); setIsCalling(true);
}, [handleRTCMessage, wsMessage, sendAudioChunk]); }, [handleRTCMessage, isConnected, wsMessage, sendAudioChunk, isCalling]);
const hangUp = useCallback(async () => { const hangUp = useCallback(async () => {
if (intervalRef.current) { if (intervalRef.current) {

View file

@ -1,49 +1,58 @@
import { useEffect, useRef, useState, useCallback } from 'react'; import { useEffect, useRef, useState, useCallback } from 'react';
import { useGetWebsocketUrlQuery } from 'librechat-data-provider/react-query'; import { useGetWebsocketUrlQuery } from 'librechat-data-provider/react-query';
import { io, Socket } from 'socket.io-client';
import type { RTCMessage } from '~/common'; import type { RTCMessage } from '~/common';
const useWebSocket = () => { const useWebSocket = () => {
const { data: data } = useGetWebsocketUrlQuery(); const { data } = useGetWebsocketUrlQuery();
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null); const socketRef = useRef<Socket | null>(null);
const connect = useCallback(() => { const connect = useCallback(() => {
if (!data || !data.url) { if (!data || !data.url) {
return; return;
} }
wsRef.current = new WebSocket(data.url); socketRef.current = io(data.url, { transports: ['websocket'] });
wsRef.current.onopen = () => setIsConnected(true);
wsRef.current.onclose = () => setIsConnected(false);
wsRef.current.onerror = (err) => console.error('WebSocket error:', err);
wsRef.current.onmessage = (event) => { socketRef.current.on('connect', () => {
const msg: RTCMessage = JSON.parse(event.data); setIsConnected(true);
switch (msg.type) { });
case 'transcription':
socketRef.current.on('disconnect', () => {
setIsConnected(false);
});
socketRef.current.on('error', (err) => {
console.error('Socket.IO error:', err);
});
socketRef.current.on('transcription', (msg: RTCMessage) => {
// TODO: Handle transcription update // TODO: Handle transcription update
break; });
case 'llm-response':
socketRef.current.on('llm-response', (msg: RTCMessage) => {
// TODO: Handle LLM streaming response // TODO: Handle LLM streaming response
break; });
case 'tts-chunk':
socketRef.current.on('tts-chunk', (msg: RTCMessage) => {
if (typeof msg.data === 'string') { if (typeof msg.data === 'string') {
const audio = new Audio(`data:audio/mp3;base64,${msg.data}`); const audio = new Audio(`data:audio/mp3;base64,${msg.data}`);
audio.play().catch(console.error); audio.play().catch(console.error);
} }
break; });
}
};
}, [data?.url]); }, [data?.url]);
useEffect(() => { useEffect(() => {
connect(); connect();
return () => wsRef.current?.close(); return () => {
socketRef.current?.disconnect();
};
}, [connect]); }, [connect]);
const sendMessage = useCallback((message: Record<string, unknown>) => { const sendMessage = useCallback((message: Record<string, unknown>) => {
if (wsRef.current?.readyState === WebSocket.OPEN) { if (socketRef.current?.connected) {
wsRef.current.send(JSON.stringify(message)); socketRef.current.emit('message', message);
} }
}, []); }, []);

281
package-lock.json generated
View file

@ -114,13 +114,13 @@
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"socket.io": "^4.8.1",
"tiktoken": "^1.0.15", "tiktoken": "^1.0.15",
"traverse": "^0.6.7", "traverse": "^0.6.7",
"ua-parser-js": "^1.0.36", "ua-parser-js": "^1.0.36",
"winston": "^3.11.0", "winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1", "winston-daily-rotate-file": "^4.7.1",
"wrtc": "^0.4.7", "wrtc": "^0.4.7",
"ws": "^8.18.0",
"youtube-transcript": "^1.2.1", "youtube-transcript": "^1.2.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@ -1601,6 +1601,8 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@ -1691,6 +1693,7 @@
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-supersub": "^1.0.0", "remark-supersub": "^1.0.0",
"socket.io-client": "^4.8.1",
"sse.js": "^2.5.0", "sse.js": "^2.5.0",
"tailwind-merge": "^1.9.1", "tailwind-merge": "^1.9.1",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",
@ -15980,6 +15983,11 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@stitches/core": { "node_modules/@stitches/core": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
@ -16364,6 +16372,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -18032,6 +18048,14 @@
} }
] ]
}, },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/base64url": { "node_modules/base64url": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
@ -20260,6 +20284,117 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"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/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"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/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.17.1", "version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@ -34011,6 +34146,142 @@
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
"dev": true "dev": true
}, },
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"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/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socks": { "node_modules/socks": {
"version": "2.8.3", "version": "2.8.3",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
@ -37575,6 +37846,14 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"devOptional": true "devOptional": true
}, },
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",