main commit

This commit is contained in:
2025-10-04 11:55:55 +09:00
parent c8c3274527
commit 4ceccae6ce
678 changed files with 95975 additions and 185 deletions

View File

@@ -32,6 +32,7 @@ class SocketManager(
sock.on(Socket.EVENT_CONNECT, onConnect)
sock.on(Socket.EVENT_DISCONNECT, onDisconnect)
sock.on(Socket.EVENT_CONNECT_ERROR, onConnectError)
sock.on("server:hello", createServerHelloHandler(availableCameras))
sock.on("register:success", onRegisterSuccess)
sock.on("register:error", onRegisterError)
sock.on("camera:request", onCameraRequestReceived)
@@ -42,22 +43,6 @@ class SocketManager(
sock.on("webrtc:ice-candidate", onWebRTCIceCandidate)
sock.connect()
// Регистрация устройства
val deviceInfo = JSONObject().apply {
put("model", android.os.Build.MODEL)
put("manufacturer", android.os.Build.MANUFACTURER)
put("androidVersion", android.os.Build.VERSION.RELEASE)
put("appVersion", "1.0.0")
put("availableCameras", JSONArray(availableCameras))
}
val registerData = JSONObject().apply {
put("deviceId", deviceId)
put("deviceInfo", deviceInfo)
}
sock.emit("register:android", registerData)
}
} catch (e: Exception) {
Log.e(TAG, "Connection error", e)

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,40 @@ function initializeSocket() {
socket.on('device:disconnected', (data) => {
logMessage('warn', `Устройство отключено: ${data.deviceId}`);
});
// Обработчики событий сессий
socket.on('session:created', (data) => {
logMessage('info', `Сессия создана: ${data.sessionId} для устройства ${data.deviceId}`);
updateOperatorSessions();
});
socket.on('session:accepted', (data) => {
logMessage('info', `Сессия принята: ${data.sessionId}`);
currentSessionId = data.sessionId;
document.getElementById('current-session-id').value = currentSessionId;
// Показываем кнопку веб-камеры для оператора
showOperatorWebcamButton();
updateOperatorSessions();
showAlert('Сессия принята! Можно начинать WebRTC соединение.', 'success');
});
socket.on('session:rejected', (data) => {
logMessage('warn', `Сессия отклонена: ${data.sessionId} - ${data.error}`);
updateOperatorSessions();
showAlert(`Сессия отклонена: ${data.error}`, 'warning');
});
socket.on('session:ended', (data) => {
logMessage('info', `Сессия завершена: ${data.sessionId}`);
updateOperatorSessions();
// Скрываем элементы веб-камеры
hideOperatorWebcamButton();
const webcamVideo = document.getElementById('operatorWebcam');
if (webcamVideo) webcamVideo.style.display = 'none';
});
}
// Обновление статуса подключения
@@ -300,6 +334,14 @@ function connectOperator() {
document.getElementById('operator-disconnect').disabled = false;
showAvailableDevices(data.availableDevices || []);
updateOperatorSessions(); // Обновляем список сессий после подключения
// Автоматическое обновление сессий каждые 3 секунды
setInterval(() => {
if (operatorSocket && operatorSocket.connected) {
updateOperatorSessions();
}
}, 3000);
});
operatorSocket.on('device:connected', (data) => {
@@ -310,6 +352,12 @@ function connectOperator() {
operatorSocket.on('camera:stream-ready', (data) => {
logMessage('info', `Камера готова: ${data.sessionId}`);
showOperatorSession(data.sessionId, 'active');
updateOperatorSessions();
});
operatorSocket.on('camera:request-sent', (data) => {
logMessage('info', `Запрос отправлен: ${data.sessionId}`);
updateOperatorSessions();
});
operatorSocket.on('camera:denied', (data) => {
@@ -497,26 +545,7 @@ async function endOperatorSession(sessionId) {
}
async function refreshSessions() {
const operatorId = document.getElementById('operator-id').value;
if (!operatorId) return;
try {
const response = await fetch('/api/operators/sessions', {
headers: { 'X-Operator-Id': operatorId }
});
if (response.ok) {
const data = await response.json();
const container = document.getElementById('operator-sessions');
container.innerHTML = '';
data.sessions.forEach(session => {
showOperatorSession(session.sessionId, session.status);
});
}
} catch (error) {
logMessage('error', 'Ошибка загрузки сессий: ' + error.message);
}
updateOperatorSessions();
}
// === ADMIN FUNCTIONS ===
@@ -780,9 +809,95 @@ function stopLocalVideo() {
}
}
// Вспомогательная функция для показа уведомлений
function showAlert(type, message) {
// Создаем временное уведомление
// Обновление списка сессий оператора
function updateOperatorSessions() {
console.log('updateOperatorSessions called, operatorSocket:', operatorSocket);
if (!operatorSocket) {
console.log('No operatorSocket, returning');
return;
}
if (!operatorSocket.connected) {
console.log('operatorSocket not connected, returning');
return;
}
console.log('Requesting sessions list...');
operatorSocket.emit('sessions:list', {}, (sessions) => {
console.log('Received sessions:', sessions);
const container = document.getElementById('operator-sessions');
if (!container) {
console.log('No operator-sessions container found');
return;
}
container.innerHTML = '';
if (!sessions || sessions.length === 0) {
container.innerHTML = '<p>Нет активных сессий</p>';
console.log('No sessions to display');
return;
}
console.log('Displaying', sessions.length, 'sessions');
sessions.forEach(session => {
const sessionCard = document.createElement('div');
sessionCard.className = `session-card session-${session.status}`;
const statusText = {
'pending': 'Ожидание',
'active': 'Активна',
'rejected': 'Отклонена',
'ended': 'Завершена'
};
sessionCard.innerHTML = `
<h4>Сессия: ${session.sessionId ? session.sessionId.substr(0, 8) : session.id ? session.id.substr(0, 8) : 'N/A'}...</h4>
<p><strong>Устройство:</strong> ${session.deviceId}</p>
<p><strong>Камера:</strong> ${session.cameraType}</p>
<p><strong>Статус:</strong> ${statusText[session.status] || session.status}</p>
<p><strong>Создана:</strong> ${new Date(session.createdAt).toLocaleString()}</p>
${session.status === 'active' ? `
<button class="btn" onclick="switchCamera('${session.sessionId || session.id}', 'front')">Фронтальная</button>
<button class="btn" onclick="switchCamera('${session.sessionId || session.id}', 'back')">Основная</button>
<button class="btn btn-danger" onclick="endSession('${session.sessionId || session.id}')">Завершить</button>
` : ''}
`;
container.appendChild(sessionCard);
});
});
}
// Завершение сессии
function endSession(sessionId) {
if (!operatorSocket) {
showAlert('Не подключен как оператор', 'danger');
return;
}
operatorSocket.emit('session:end', { sessionId });
logMessage('info', `Завершаем сессию: ${sessionId}`);
}
// Переключение камеры
function switchCamera(sessionId, cameraType) {
if (!operatorSocket) {
showAlert('Не подключен как оператор', 'danger');
return;
}
operatorSocket.emit('camera:switch', {
sessionId,
cameraType
});
logMessage('info', `Переключаем камеру: ${sessionId} на ${cameraType}`);
}
// Показ уведомления
function showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
@@ -794,7 +909,6 @@ function showAlert(type, message) {
document.body.appendChild(alert);
// Удаляем через 5 секунд
setTimeout(() => {
if (document.body.contains(alert)) {
document.body.removeChild(alert);

815
backend/public/mobile.html Normal file
View File

@@ -0,0 +1,815 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>GodEye Mobile Camera</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #000;
color: #fff;
overflow: hidden;
touch-action: manipulation;
}
.mobile-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: rgba(0, 0, 0, 0.8);
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
backdrop-filter: blur(10px);
}
.header h1 {
font-size: 18px;
color: #4CAF50;
}
.status {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
background: #333;
}
.status.connected {
background: #4CAF50;
color: #000;
}
.status.disconnected {
background: #f44336;
}
.video-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#localVideo {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1); /* Зеркало для фронтальной камеры */
}
.no-camera {
text-align: center;
padding: 20px;
}
.controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 15px 20px;
border-radius: 25px;
backdrop-filter: blur(10px);
}
.btn {
width: 50px;
height: 50px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-camera {
background: #4CAF50;
color: white;
}
.btn-camera:hover {
background: #45a049;
transform: scale(1.1);
}
.btn-camera:disabled {
background: #666;
cursor: not-allowed;
transform: none;
}
.btn-switch {
background: #2196F3;
color: white;
}
.btn-switch:hover {
background: #1976D2;
transform: scale(1.1);
}
.btn-disconnect {
background: #f44336;
color: white;
}
.btn-disconnect:hover {
background: #d32f2f;
transform: scale(1.1);
}
.device-info {
position: absolute;
top: 80px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
font-size: 12px;
backdrop-filter: blur(10px);
}
.requests-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 20px;
border-radius: 12px;
text-align: center;
display: none;
backdrop-filter: blur(10px);
border: 1px solid #333;
}
.requests-panel.visible {
display: block;
}
.request-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-accept {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.btn-reject {
background: #f44336;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.logs {
position: absolute;
bottom: 120px;
left: 15px;
right: 15px;
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 8px;
font-size: 11px;
max-height: 100px;
overflow-y: auto;
backdrop-filter: blur(10px);
display: none;
}
.logs.visible {
display: block;
}
.log-entry {
margin-bottom: 2px;
opacity: 0.8;
}
.log-entry.info { color: #2196F3; }
.log-entry.success { color: #4CAF50; }
.log-entry.warning { color: #FF9800; }
.log-entry.error { color: #f44336; }
@media (orientation: landscape) {
.controls {
right: 20px;
left: auto;
transform: none;
flex-direction: column;
}
.device-info {
top: 15px;
left: 15px;
}
}
</style>
</head>
<body>
<div class="mobile-container">
<div class="header">
<h1>📱 GodEye Mobile</h1>
<div id="status" class="status disconnected">Отключено</div>
</div>
<div class="video-container">
<video id="localVideo" autoplay playsinline muted></video>
<div id="no-camera" class="no-camera">
<h3>📷 Камера не активна</h3>
<p>Нажмите кнопку включения камеры</p>
</div>
</div>
<div class="device-info" id="device-info">
<div>📱 <strong>Device ID:</strong> <span id="device-id">Генерация...</span></div>
<div>📷 <strong>Camera:</strong> <span id="current-camera">none</span></div>
<div>🔗 <strong>Sessions:</strong> <span id="session-count">0</span></div>
</div>
<div class="controls">
<button id="btn-camera" class="btn btn-camera" title="Включить/выключить камеру">
📷
</button>
<button id="btn-switch" class="btn btn-switch" title="Переключить камеру">
🔄
</button>
<button id="btn-disconnect" class="btn btn-disconnect" title="Отключиться">
</button>
</div>
<div id="requests-panel" class="requests-panel">
<h3>📞 Запрос на доступ к камере</h3>
<p>Оператор <strong id="operator-id"></strong> запрашивает доступ к камере</p>
<div class="request-buttons">
<button id="btn-accept" class="btn-accept">✅ Принять</button>
<button id="btn-reject" class="btn-reject">❌ Отклонить</button>
</div>
</div>
<div id="logs" class="logs">
<!-- Логи здесь -->
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
class MobileCameraApp {
constructor() {
this.socket = null;
this.deviceId = this.generateDeviceId();
this.localStream = null;
this.currentCamera = 'back';
this.activeSessions = new Map();
this.pendingRequests = new Map();
this.peerConnections = new Map(); // WebRTC connections
this.logTimeout = null;
this.isConnected = false;
this.isCameraOn = false;
this.elements = {
status: document.getElementById('status'),
localVideo: document.getElementById('localVideo'),
noCamera: document.getElementById('no-camera'),
deviceId: document.getElementById('device-id'),
currentCamera: document.getElementById('current-camera'),
sessionCount: document.getElementById('session-count'),
requestsPanel: document.getElementById('requests-panel'),
operatorId: document.getElementById('operator-id'),
logs: document.getElementById('logs'),
btnCamera: document.getElementById('btn-camera'),
btnSwitch: document.getElementById('btn-switch'),
btnDisconnect: document.getElementById('btn-disconnect'),
btnAccept: document.getElementById('btn-accept'),
btnReject: document.getElementById('btn-reject')
};
this.init();
}
generateDeviceId() {
// Используем комбинацию для мобильного устройства
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substr(2, 9);
return `mobile_${timestamp}_${random}`;
}
init() {
this.elements.deviceId.textContent = this.deviceId;
this.setupEventListeners();
this.connectToServer();
}
setupEventListeners() {
this.elements.btnCamera.addEventListener('click', () => this.toggleCamera());
this.elements.btnSwitch.addEventListener('click', () => this.switchCamera());
this.elements.btnDisconnect.addEventListener('click', () => this.disconnect());
this.elements.btnAccept.addEventListener('click', () => this.acceptRequest());
this.elements.btnReject.addEventListener('click', () => this.rejectRequest());
// Обработка поворота экрана
window.addEventListener('orientationchange', () => {
setTimeout(() => this.adjustLayout(), 100);
});
}
connectToServer() {
this.log('Подключение к серверу...', 'info');
this.socket = io();
this.socket.on('connect', () => {
this.log('✅ Подключено к серверу', 'success');
this.isConnected = true;
this.updateStatus('connected');
this.registerDevice();
});
this.socket.on('disconnect', () => {
this.log('❌ Отключено от сервера', 'warning');
this.isConnected = false;
this.updateStatus('disconnected');
this.stopCamera();
});
this.socket.on('camera:request', (data) => {
this.handleCameraRequest(data);
});
this.socket.on('camera:switch', (data) => {
this.handleCameraSwitchRequest(data);
});
this.socket.on('camera:disconnect', (data) => {
this.handleDisconnectRequest(data);
});
// WebRTC обработчики
this.socket.on('webrtc:offer', (data) => {
this.handleWebRTCOffer(data);
});
this.socket.on('webrtc:answer', (data) => {
this.handleWebRTCAnswer(data);
});
this.socket.on('webrtc:ice-candidate', (data) => {
this.handleWebRTCIceCandidate(data);
});
this.socket.on('register:success', (data) => {
this.log(`📱 Устройство зарегистрировано: ${data.deviceId}`, 'success');
});
this.socket.on('register:error', (error) => {
this.log(`❌ Ошибка регистрации: ${error.message}`, 'error');
});
}
registerDevice() {
const deviceInfo = {
platform: 'mobile_web',
userAgent: navigator.userAgent,
cameras: ['back', 'front'],
capabilities: {
video: true,
audio: false
}
};
this.socket.emit('register:mobile_web', {
deviceId: this.deviceId,
deviceInfo: deviceInfo
});
}
async toggleCamera() {
if (this.isCameraOn) {
this.stopCamera();
} else {
await this.startCamera();
}
}
async startCamera() {
try {
this.log(`📷 Запуск камеры (${this.currentCamera})...`, 'info');
const constraints = {
video: {
facingMode: this.currentCamera === 'front' ? 'user' : 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
};
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
this.elements.localVideo.srcObject = this.localStream;
this.isCameraOn = true;
this.elements.btnCamera.textContent = '📷';
this.elements.btnCamera.style.background = '#f44336';
this.elements.noCamera.style.display = 'none';
this.elements.localVideo.style.display = 'block';
// Для фронтальной камеры применяем зеркальное отображение
this.elements.localVideo.style.transform =
this.currentCamera === 'front' ? 'scaleX(-1)' : 'scaleX(1)';
this.updateCameraInfo();
this.log(`✅ Камера ${this.currentCamera} запущена`, 'success');
} catch (error) {
this.log(`❌ Ошибка доступа к камере: ${error.message}`, 'error');
this.isCameraOn = false;
}
}
stopCamera() {
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
this.isCameraOn = false;
this.elements.btnCamera.textContent = '📷';
this.elements.btnCamera.style.background = '#4CAF50';
this.elements.localVideo.style.display = 'none';
this.elements.noCamera.style.display = 'block';
this.updateCameraInfo();
this.log('📷 Камера остановлена', 'info');
}
async switchCamera(targetCamera = null) {
if (!this.isCameraOn) {
this.log('⚠️ Сначала включите камеру', 'warning');
return;
}
// Если указан конкретный тип камеры - используем его
if (targetCamera) {
this.currentCamera = targetCamera;
} else {
// Иначе переключаем на противоположную
this.currentCamera = this.currentCamera === 'front' ? 'back' : 'front';
}
this.log(`🔄 Переключение камеры на: ${this.currentCamera}`, 'info');
this.stopCamera();
await this.startCamera();
// Обновляем все активные WebRTC соединения
await this.updateWebRTCStreams();
}
handleCameraRequest(data) {
const { sessionId, operatorId, cameraType } = data;
this.log(`📞 Запрос камеры от оператора ${operatorId}`, 'info');
this.pendingRequests.set(sessionId, { operatorId, cameraType, sessionId });
this.showRequestPanel(operatorId, sessionId);
}
handleCameraSwitchRequest(data) {
const { sessionId, cameraType } = data;
this.log(`🔄 Запрос переключения камеры: ${cameraType}`, 'info');
if (this.activeSessions.has(sessionId)) {
if (this.isCameraOn) {
// Переключаем на конкретно запрошенный тип камеры
this.switchCamera(cameraType);
} else {
// Если камера не включена, просто обновляем тип
this.currentCamera = cameraType;
this.log(`📝 Тип камеры обновлен на: ${cameraType}`, 'info');
}
}
}
handleDisconnectRequest(data) {
const { sessionId } = data;
this.log(`🔌 Запрос отключения сессии: ${sessionId}`, 'info');
if (this.activeSessions.has(sessionId)) {
this.activeSessions.delete(sessionId);
this.updateSessionCount();
}
}
showRequestPanel(operatorId, sessionId) {
this.elements.operatorId.textContent = operatorId;
this.elements.requestsPanel.classList.add('visible');
this.currentRequestSessionId = sessionId;
}
hideRequestPanel() {
this.elements.requestsPanel.classList.remove('visible');
this.currentRequestSessionId = null;
}
async acceptRequest() {
const sessionId = this.currentRequestSessionId;
const requestData = this.pendingRequests.get(sessionId);
if (!requestData) return;
this.log(`✅ Принят запрос сессии: ${sessionId}`, 'success');
// Запускаем камеру если не запущена
if (!this.isCameraOn) {
this.currentCamera = requestData.cameraType || 'back';
await this.startCamera();
}
// Добавляем сессию
this.activeSessions.set(sessionId, {
operatorId: requestData.operatorId,
cameraType: this.currentCamera,
startTime: new Date()
});
// Отправляем подтверждение с поддержкой WebRTC
this.socket.emit('camera:response', {
sessionId: sessionId,
accepted: true,
streamUrl: 'webrtc', // Указываем что используем WebRTC
cameraType: this.currentCamera
});
this.pendingRequests.delete(sessionId);
this.hideRequestPanel();
this.updateSessionCount();
}
rejectRequest() {
const sessionId = this.currentRequestSessionId;
const requestData = this.pendingRequests.get(sessionId);
if (!requestData) return;
this.log(`❌ Отклонен запрос сессии: ${sessionId}`, 'warning');
this.socket.emit('camera:response', {
sessionId: sessionId,
accepted: false,
error: 'Пользователь отклонил запрос'
});
this.pendingRequests.delete(sessionId);
this.hideRequestPanel();
}
// ===== WebRTC Methods =====
async createPeerConnection(sessionId) {
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const peerConnection = new RTCPeerConnection(config);
// Добавляем обработчики событий
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('webrtc:ice-candidate', {
sessionId: sessionId,
candidate: event.candidate
});
this.log('🧊 ICE candidate отправлен', 'info');
}
};
peerConnection.onconnectionstatechange = () => {
this.log(`🔗 WebRTC состояние: ${peerConnection.connectionState}`, 'info');
if (peerConnection.connectionState === 'connected') {
this.log('✅ WebRTC соединение установлено', 'success');
} else if (peerConnection.connectionState === 'failed') {
this.log('❌ WebRTC соединение неудачно', 'error');
}
};
peerConnection.onicegatheringstatechange = () => {
this.log(`❄️ ICE gathering: ${peerConnection.iceGatheringState}`, 'info');
};
// Добавляем локальный поток если есть
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, this.localStream);
this.log(`🎥 Трек добавлен: ${track.kind}`, 'info');
});
}
return peerConnection;
}
async handleWebRTCOffer(data) {
const { sessionId, offer } = data;
this.log(`📞 Получен WebRTC offer для сессии: ${sessionId}`, 'info');
try {
// Создаем новое peer connection для этой сессии
const peerConnection = await this.createPeerConnection(sessionId);
this.peerConnections.set(sessionId, peerConnection);
// Устанавливаем remote description
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.log('📝 Remote description установлен', 'info');
// Создаем answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// Отправляем answer
this.socket.emit('webrtc:answer', {
sessionId: sessionId,
answer: answer
});
this.log('✅ WebRTC answer отправлен', 'success');
} catch (error) {
this.log(`❌ Ошибка WebRTC offer: ${error.message}`, 'error');
this.socket.emit('webrtc:error', {
sessionId: sessionId,
error: error.message
});
}
}
async handleWebRTCAnswer(data) {
const { sessionId, answer } = data;
this.log(`📞 Получен WebRTC answer для сессии: ${sessionId}`, 'info');
try {
const peerConnection = this.peerConnections.get(sessionId);
if (peerConnection) {
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
this.log('✅ WebRTC answer обработан', 'success');
}
} catch (error) {
this.log(`❌ Ошибка WebRTC answer: ${error.message}`, 'error');
}
}
async handleWebRTCIceCandidate(data) {
const { sessionId, candidate } = data;
try {
const peerConnection = this.peerConnections.get(sessionId);
if (peerConnection) {
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
this.log('🧊 ICE candidate добавлен', 'info');
}
} catch (error) {
this.log(`❌ Ошибка ICE candidate: ${error.message}`, 'error');
}
}
async updateWebRTCStreams() {
if (!this.localStream) {
this.log('⚠️ Нет локального потока для обновления', 'warning');
return;
}
this.log('🔄 Обновление WebRTC потоков...', 'info');
// Обновляем все активные peer connections
for (const [sessionId, peerConnection] of this.peerConnections.entries()) {
try {
// Удаляем старые треки
const senders = peerConnection.getSenders();
for (const sender of senders) {
if (sender.track) {
peerConnection.removeTrack(sender);
}
}
// Добавляем новые треки
this.localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, this.localStream);
});
this.log(`✅ WebRTC поток обновлен для сессии: ${sessionId}`, 'success');
} catch (error) {
this.log(`❌ Ошибка обновления WebRTC потока: ${error.message}`, 'error');
}
}
}
disconnect() {
this.log('🔌 Отключение от сервера...', 'info');
this.stopCamera();
// Закрываем все WebRTC соединения
this.peerConnections.forEach((pc, sessionId) => {
pc.close();
this.log(`🔒 WebRTC соединение закрыто: ${sessionId}`, 'info');
});
this.peerConnections.clear();
if (this.socket) {
this.socket.disconnect();
}
}
updateStatus(status) {
this.elements.status.textContent = status === 'connected' ? 'Подключено' : 'Отключено';
this.elements.status.className = `status ${status}`;
}
updateCameraInfo() {
this.elements.currentCamera.textContent =
this.isCameraOn ? this.currentCamera : 'none';
}
updateSessionCount() {
this.elements.sessionCount.textContent = this.activeSessions.size;
}
adjustLayout() {
// Адаптация под поворот экрана
setTimeout(() => {
if (this.localStream && this.elements.localVideo.srcObject) {
this.elements.localVideo.play();
}
}, 100);
}
log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
this.elements.logs.appendChild(logEntry);
this.elements.logs.scrollTop = this.elements.logs.scrollHeight;
// Показываем логи на 3 секунды
this.elements.logs.classList.add('visible');
clearTimeout(this.logTimeout);
this.logTimeout = setTimeout(() => {
this.elements.logs.classList.remove('visible');
}, 3000);
console.log(`[Mobile] ${message}`);
}
}
// Запуск приложения
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new MobileCameraApp();
});
// Предотвращение случайного обновления страницы
window.addEventListener('beforeunload', (e) => {
if (app && app.isConnected) {
e.preventDefault();
e.returnValue = '';
return '';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,486 @@
/**
* ConnectionManager - управляет подключениями между операторами и устройствами
*/
class ConnectionManager {
constructor(sessionManager, deviceManager, logger) {
this.sessionManager = sessionManager;
this.deviceManager = deviceManager;
this.logger = logger;
this.connectionRequests = new Map(); // requestId -> ConnectionRequest
this.activeConnections = new Map(); // connectionId -> Connection
this.connectionTimeouts = new Map(); // connectionId -> timeoutId
this.maxConnectionsPerDevice = 1; // Ограничение: одно соединение на устройство
this.connectionTimeout = 30000; // 30 секунд на установку соединения
}
/**
* Инициация подключения оператора к устройству
* @param {string} operatorId
* @param {string} deviceId
* @param {string} cameraType
* @returns {Promise<object>} {success: boolean, connectionId?: string, sessionId?: string, error?: string}
*/
async initiateConnection(operatorId, deviceId, cameraType = 'back') {
this.logger.info(`🔗 Initiating connection: ${operatorId} -> ${deviceId} (${cameraType})`);
// Проверяем возможность создания соединения
const validation = this.deviceManager.canCreateSession(deviceId, operatorId);
if (!validation.canConnect) {
this.logger.error(`❌ Connection validation failed: ${validation.reason}`);
throw new Error(validation.reason);
}
// Создаем сессию
const session = this.sessionManager.createSession(deviceId, operatorId, cameraType);
const connectionId = session.sessionId;
// Создаем запрос на подключение
const connectionRequest = {
connectionId,
sessionId: connectionId, // Для совместимости
operatorId,
deviceId,
cameraType,
status: 'pending',
createdAt: new Date(),
timeoutAt: new Date(Date.now() + this.connectionTimeout),
// Для прямого WebRTC соединения
webrtc: {
signalingCompleted: false,
directConnection: false,
state: 'initialized',
stunServers: ['stun:stun.l.google.com:19302'] // STUN серверы для NAT traversal
}
};
this.connectionRequests.set(connectionId, connectionRequest);
// Устанавливаем таймаут
const timeoutId = setTimeout(async () => {
await this.handleConnectionTimeout(connectionId);
}, this.connectionTimeout);
this.connectionTimeouts.set(connectionId, timeoutId);
// Отправляем запрос Android устройству
const device = this.deviceManager.getDevice(deviceId);
if (!device || !device.isConnected()) {
this.logger.error(`❌ Device not connected: ${deviceId}`);
this.connectionRequests.delete(connectionId);
clearTimeout(timeoutId);
this.connectionTimeouts.delete(connectionId);
throw new Error('Device not connected');
}
const requestData = {
sessionId: connectionId,
operatorId: operatorId,
cameraType: cameraType
};
this.logger.info(`📱 Sending camera:request to Android device ${deviceId}`);
device.socket.emit('camera:request', requestData);
// Добавляем сессию к участникам
device.addSession(connectionId);
const operator = this.deviceManager.getOperator(operatorId);
if (operator) {
operator.addSession(connectionId);
}
this.logger.info(`✅ Connection request created: ${connectionId}`);
return { success: true, connectionId, sessionId: connectionId };
}
/**
* Принятие запроса на подключение от устройства
* @param {string} connectionId
* @param {object} connectionData
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async acceptConnection(connectionId, connectionData = {}) {
const request = this.connectionRequests.get(connectionId);
if (!request) {
this.logger.error(`❌ Connection request not found: ${connectionId}`);
throw new Error('Connection request not found');
}
// Очищаем таймаут
this.clearConnectionTimeout(connectionId);
// Создаем активное соединение
const connection = {
connectionId,
operatorId: request.operatorId,
deviceId: request.deviceId,
cameraType: request.cameraType,
status: 'active',
establishedAt: new Date(),
streamUrl: connectionData.streamUrl || 'webrtc',
lastActivity: new Date()
};
this.activeConnections.set(connectionId, connection);
this.connectionRequests.delete(connectionId);
// Обновляем сессию
const session = this.sessionManager.getSession(connectionId);
if (session) {
session.updateStatus('active', { streamUrl: connection.streamUrl });
}
this.logger.info(`✅ Connection established: ${connectionId}`);
return connection;
}
/**
* Отклонение запроса на подключение
* @param {string} connectionId
* @param {string} reason
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async rejectConnection(connectionId, reason = 'User rejected') {
const request = this.connectionRequests.get(connectionId);
if (!request) {
this.logger.error(`❌ Connection request not found: ${connectionId}`);
throw new Error('Connection request not found');
}
// Очищаем таймаут
this.clearConnectionTimeout(connectionId);
// Обновляем сессию
const session = this.sessionManager.getSession(connectionId);
if (session) {
session.updateStatus('denied', { error: reason });
}
this.connectionRequests.delete(connectionId);
this.logger.info(`❌ Connection rejected: ${connectionId} - ${reason}`);
return { success: true };
}
/**
* Завершение активного соединения
* @param {string} connectionId
* @param {string} reason
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async terminateConnection(connectionId, reason = 'Connection terminated') {
// Ищем соединение в активных или ожидающих
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
if (!connection && !request) {
this.logger.error(`❌ Connection not found: ${connectionId}`);
throw new Error('Connection not found');
}
// Закрываем сессию
this.sessionManager.closeSession(connectionId);
// Удаляем из соответствующих коллекций
if (connection) {
this.activeConnections.delete(connectionId);
}
if (request) {
this.connectionRequests.delete(connectionId);
}
// Очищаем устройство и оператора
const target = connection || request;
const device = this.deviceManager.getDevice(target.deviceId);
const operator = this.deviceManager.getOperator(target.operatorId);
if (device) {
device.removeSession(connectionId);
}
if (operator) {
operator.removeSession(connectionId);
}
this.logger.info(`🔌 Connection terminated: ${connectionId} - ${reason}`);
return target;
}
/**
* Обработка таймаута соединения
* @param {string} connectionId
*/
async handleConnectionTimeout(connectionId) {
this.logger.warn(`⏰ Connection timeout: ${connectionId}`);
try {
// Проверяем есть ли запрос перед отклонением
const request = this.connectionRequests.get(connectionId);
if (request) {
await this.rejectConnection(connectionId, 'Connection timeout');
} else {
this.logger.info(`Connection ${connectionId} already removed from pending requests`);
}
} catch (error) {
this.logger.error(`Error handling timeout for ${connectionId}:`, error.message);
}
// Уведомляем участников о таймауте
const request = this.connectionRequests.get(connectionId);
if (request) {
const operator = this.deviceManager.getOperator(request.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:timeout', {
connectionId,
deviceId: request.deviceId,
error: 'Connection request timeout'
});
}
}
}
/**
* Очистка таймаута соединения
* @param {string} connectionId
*/
clearConnectionTimeout(connectionId) {
const timeoutId = this.connectionTimeouts.get(connectionId);
if (timeoutId) {
clearTimeout(timeoutId);
this.connectionTimeouts.delete(connectionId);
}
}
/**
* Получение статистики соединений
* @returns {object}
*/
getConnectionStats() {
return {
pendingRequests: this.connectionRequests.size,
activeConnections: this.activeConnections.size,
totalRequestsProcessed: this.connectionRequests.size + this.activeConnections.size,
averageConnectionTime: this.calculateAverageConnectionTime()
};
}
/**
* Расчет среднего времени установки соединения
* @returns {number} время в миллисекундах
*/
calculateAverageConnectionTime() {
if (this.activeConnections.size === 0) return 0;
let totalTime = 0;
let count = 0;
for (const connection of this.activeConnections.values()) {
if (connection.establishedAt) {
// Примерное время установки соединения (можно улучшить, сохраняя время запроса)
totalTime += 2000; // placeholder
count++;
}
}
return count > 0 ? totalTime / count : 0;
}
/**
* Получение активного соединения
* @param {string} connectionId
* @returns {object|null}
*/
getConnection(connectionId) {
return this.activeConnections.get(connectionId) || null;
}
/**
* Получение всех активных соединений для оператора
* @param {string} operatorId
* @returns {Array}
*/
getOperatorConnections(operatorId) {
return Array.from(this.activeConnections.values())
.filter(conn => conn.operatorId === operatorId);
}
/**
* Получение всех активных соединений для устройства
* @param {string} deviceId
* @returns {Array}
*/
getDeviceConnections(deviceId) {
return Array.from(this.activeConnections.values())
.filter(conn => conn.deviceId === deviceId);
}
/**
* Очистка устаревших запросов и неактивных соединений
*/
async cleanup() {
const now = new Date();
// Очищаем устаревшие запросы
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (now > request.timeoutAt) {
await this.handleConnectionTimeout(connectionId);
}
}
// Проверяем активные соединения на отключенные сокеты
for (const [connectionId, connection] of this.activeConnections.entries()) {
const device = this.deviceManager.getDevice(connection.deviceId);
const operator = this.deviceManager.getOperator(connection.operatorId);
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
try {
await this.terminateConnection(connectionId, 'Participant disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
}
this.logger.debug(`🧹 Connection cleanup completed. Active: ${this.activeConnections.size}, Pending: ${this.connectionRequests.size}`);
}
/**
* Очистка всех подключений устройства при его отключении
* @param {string} deviceId
*/
async cleanupDeviceConnections(deviceId) {
this.logger.info(`🧹 Cleaning up connections for device: ${deviceId}`);
const connectionsToTerminate = [];
// Находим все активные подключения устройства
for (const [connectionId, connection] of this.activeConnections.entries()) {
if (connection.deviceId === deviceId) {
connectionsToTerminate.push(connectionId);
}
}
// Находим все ожидающие запросы для устройства
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (request.deviceId === deviceId) {
connectionsToTerminate.push(connectionId);
}
}
// Завершаем все найденные подключения
for (const connectionId of connectionsToTerminate) {
try {
await this.terminateConnection(connectionId, 'Device disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for device ${deviceId}`);
}
/**
* Очистка всех подключений оператора при его отключении
* @param {string} operatorId
*/
async cleanupOperatorConnections(operatorId) {
this.logger.info(`🧹 Cleaning up connections for operator: ${operatorId}`);
const connectionsToTerminate = [];
// Находим все активные подключения оператора
for (const [connectionId, connection] of this.activeConnections.entries()) {
if (connection.operatorId === operatorId) {
connectionsToTerminate.push(connectionId);
}
}
// Находим все ожидающие запросы от оператора
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (request.operatorId === operatorId) {
connectionsToTerminate.push(connectionId);
}
}
// Завершаем все найденные подключения
for (const connectionId of connectionsToTerminate) {
try {
await this.terminateConnection(connectionId, 'Operator disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
}
/**
* Обновление WebRTC состояния соединения
* @param {string} connectionId
* @param {string} state - offer_sent, answer_sent, ice_completed, connected
*/
updateWebRTCState(connectionId, state) {
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
const target = connection || request;
if (!target || !target.webrtc) {
this.logger.error(`❌ Cannot update WebRTC state for connection: ${connectionId}`);
return false;
}
target.webrtc.state = state;
target.webrtc.lastUpdated = new Date();
// Если соединение установлено, переходим в режим прямого соединения
if (state === 'connected') {
target.webrtc.signalingCompleted = true;
target.webrtc.directConnection = true;
this.logger.info(`🔗 WebRTC direct connection established: ${connectionId}`);
}
this.logger.info(`🔄 WebRTC state updated: ${connectionId} -> ${state}`);
return true;
}
/**
* Проверка готовности к прямому соединению
* @param {string} connectionId
* @returns {boolean}
*/
isDirectConnectionReady(connectionId) {
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
const target = connection || request;
if (!target || !target.webrtc) {
return false;
}
return target.webrtc.signalingCompleted && target.webrtc.directConnection;
}
/**
* Получение WebRTC информации соединения
* @param {string} connectionId
* @returns {object|null}
*/
getWebRTCInfo(connectionId) {
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
const target = connection || request;
if (!target || !target.webrtc) {
return null;
}
return {
connectionId,
state: target.webrtc.state,
signalingCompleted: target.webrtc.signalingCompleted,
directConnection: target.webrtc.directConnection,
lastUpdated: target.webrtc.lastUpdated,
stunServers: target.webrtc.stunServers
};
}
}
module.exports = { ConnectionManager };

View File

@@ -0,0 +1,515 @@
/**
* ConnectionManager - управляет подключениями между операторами и устройствами
*/
class ConnectionManager {
constructor(sessionManager, deviceManager, logger) {
this.sessionManager = sessionManager;
this.deviceManager = deviceManager;
this.logger = logger;
this.connectionRequests = new Map(); // requestId -> ConnectionRequest
this.activeConnections = new Map();
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
}
/**
* Обновление WebRTC состояния соединения
* @param {string} connectionId
* @param {string} state - offer_sent, answer_sent, ice_completed, direct_established
*/
updateWebRTCState(connectionId, state) {
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
if (connection && connection.webrtc) {
connection.webrtc.state = state;
connection.webrtc.lastUpdated = new Date();
// Если ICE candidates обменялись, начинаем переход на прямое соединение
if (state === 'ice_completed') {
this.initiateDirectConnection(connectionId);
}
this.logger.info(`🔄 WebRTC state updated: ${connectionId} -> ${state}`);
} else if (request && request.webrtc) {
request.webrtc.state = state;
request.webrtc.lastUpdated = new Date();
this.logger.info(`🔄 WebRTC state updated (pending): ${connectionId} -> ${state}`);
}
}
/**
* Инициация прямого соединения после завершения WebRTC сигналинга
* @param {string} connectionId
*/
async initiateDirectConnection(connectionId) {
const connection = this.activeConnections.get(connectionId);
if (!connection) {
this.logger.error(`❌ Connection not found for direct setup: ${connectionId}`);
return;
}
this.logger.info(`🔗 Initiating direct WebRTC connection: ${connectionId}`);
try {
// Получаем устройство и оператора
const device = this.deviceManager.getDevice(connection.deviceId);
const operator = this.deviceManager.getOperator(connection.operatorId);
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
throw new Error('Device or operator not connected');
}
// Отправляем уведомление о переходе на прямое соединение
device.socket.emit('webrtc:direct-mode', {
connectionId,
message: 'Switching to direct WebRTC connection'
});
operator.socket.emit('webrtc:direct-mode', {
connectionId,
message: 'Switching to direct WebRTC connection'
});
// Обновляем статус
connection.webrtc.directConnection = true;
connection.webrtc.signalingCompleted = true;
this.logger.info(`✅ Direct WebRTC connection initiated: ${connectionId}`);
} catch (error) {
this.logger.error(`❌ Error initiating direct connection: ${connectionId}`, error);
}
}
/**
* Сохранение endpoint информации для прямого соединения
* @param {string} connectionId
* @param {string} type - 'operator' or 'device'
* @param {object} endpoint - {ip, port, candidates}
*/
setEndpoint(connectionId, type, endpoint) {
const connection = this.activeConnections.get(connectionId);
const request = this.connectionRequests.get(connectionId);
const target = connection || request;
if (!target || !target.webrtc) {
this.logger.error(`❌ Cannot set endpoint for connection: ${connectionId}`);
return false;
}
if (type === 'operator') {
target.webrtc.operatorEndpoint = endpoint;
} else if (type === 'device') {
target.webrtc.deviceEndpoint = endpoint;
}
this.logger.info(`📍 Endpoint set for ${type}: ${connectionId}`, endpoint);
return true;
}
/**
* Проверка готовности к прямому соединению
* @param {string} connectionId
* @returns {boolean}
*/
isReadyForDirectConnection(connectionId) {
const connection = this.activeConnections.get(connectionId);
if (!connection || !connection.webrtc) {
return false;
}
return connection.webrtc.operatorEndpoint &&
connection.webrtc.deviceEndpoint &&
connection.webrtc.signalingCompleted;
}
}
module.exports = ConnectionManager;nnectionId -> Connection
this.connectionTimeouts = new Map(); // connectionId -> timeoutId
this.maxConnectionsPerDevice = 1; // Ограничение: одно соединение на устройство
this.connectionTimeout = 30000; // 30 секунд на установку соединения
}
/**
* Инициация подключения оператора к устройству
* @param {string} operatorId
* @param {string} deviceId
* @param {string} cameraType
* @returns {Promise<object>} {success: boolean, connectionId?: string, sessionId?: string, error?: string}
*/
async initiateConnection(operatorId, deviceId, cameraType = 'back') {
this.logger.info(`🔗 Initiating connection: ${operatorId} -> ${deviceId} (${cameraType})`);
// Проверяем возможность создания соединения
const validation = this.deviceManager.canCreateSession(deviceId, operatorId);
if (!validation.canConnect) {
this.logger.error(`❌ Connection validation failed: ${validation.reason}`);
throw new Error(validation.reason);
}
// Создаем сессию
const session = this.sessionManager.createSession(deviceId, operatorId, cameraType);
const connectionId = session.sessionId;
// Создаем запрос на подключение
const connectionRequest = {
connectionId,
sessionId: connectionId, // Для совместимости
operatorId,
deviceId,
cameraType,
status: 'pending',
createdAt: new Date(),
timeoutAt: new Date(Date.now() + this.connectionTimeout),
// Для прямого WebRTC соединения
webrtc: {
signalingCompleted: false,
directConnection: false,
operatorEndpoint: null, // IP:port оператора
deviceEndpoint: null, // IP:port устройства
stunServers: ['stun:stun.l.google.com:19302'] // STUN серверы для NAT traversal
}
};
this.connectionRequests.set(connectionId, connectionRequest);
// Устанавливаем таймаут
const timeoutId = setTimeout(() => {
this.handleConnectionTimeout(connectionId);
}, this.connectionTimeout);
this.connectionTimeouts.set(connectionId, timeoutId);
// Отправляем запрос Android устройству
const device = this.deviceManager.getDevice(deviceId);
if (!device || !device.isConnected()) {
this.logger.error(`❌ Device not connected: ${deviceId}`);
this.connectionRequests.delete(connectionId);
clearTimeout(timeoutId);
this.connectionTimeouts.delete(connectionId);
throw new Error('Device not connected');
}
const requestData = {
sessionId: connectionId,
operatorId: operatorId,
cameraType: cameraType
};
this.logger.info(`📱 Sending camera:request to Android device ${deviceId}`);
device.socket.emit('camera:request', requestData);
// Добавляем сессию к участникам
device.addSession(connectionId);
const operator = this.deviceManager.getOperator(operatorId);
if (operator) {
operator.addSession(connectionId);
}
this.logger.info(`✅ Connection request created: ${connectionId}`);
return { success: true, connectionId, sessionId: connectionId };
}
/**
* Принятие запроса на подключение от устройства
* @param {string} connectionId
* @param {object} connectionData
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async acceptConnection(connectionId, connectionData = {}) {
const request = this.connectionRequests.get(connectionId);
if (!request) {
this.logger.error(`❌ Connection request not found: ${connectionId}`);
throw new Error('Connection request not found');
}
// Очищаем таймаут
this.clearConnectionTimeout(connectionId);
// Создаем активное соединение
const connection = {
connectionId,
operatorId: request.operatorId,
deviceId: request.deviceId,
cameraType: request.cameraType,
status: 'active',
establishedAt: new Date(),
streamUrl: connectionData.streamUrl || 'webrtc',
lastActivity: new Date()
};
this.activeConnections.set(connectionId, connection);
this.connectionRequests.delete(connectionId);
// Обновляем сессию
const session = this.sessionManager.getSession(connectionId);
if (session) {
session.updateStatus('active', { streamUrl: connection.streamUrl });
}
this.logger.info(`✅ Connection established: ${connectionId}`);
return connection;
}
/**
* Отклонение запроса на подключение
* @param {string} connectionId
* @param {string} reason
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async rejectConnection(connectionId, reason = 'User rejected') {
const request = this.connectionRequests.get(connectionId);
if (!request) {
this.logger.error(`❌ Connection request not found: ${connectionId}`);
throw new Error('Connection request not found');
}
// Очищаем таймаут
this.clearConnectionTimeout(connectionId);
// Обновляем сессию
const session = this.sessionManager.getSession(connectionId);
if (session) {
session.updateStatus('denied', { error: reason });
}
this.connectionRequests.delete(connectionId);
this.logger.info(`❌ Connection rejected: ${connectionId} - ${reason}`);
return { success: true };
}
/**
* Завершение активного соединения
* @param {string} connectionId
* @param {string} reason
* @returns {Promise<object>} {success: boolean, error?: string}
*/
async terminateConnection(connectionId, reason = 'Connection terminated') {
const connection = this.activeConnections.get(connectionId);
if (!connection) {
this.logger.error(`❌ Active connection not found: ${connectionId}`);
throw new Error('Active connection not found');
}
// Закрываем сессию
this.sessionManager.closeSession(connectionId);
// Удаляем из активных соединений
this.activeConnections.delete(connectionId);
// Очищаем устройство и оператора
const device = this.deviceManager.getDevice(connection.deviceId);
const operator = this.deviceManager.getOperator(connection.operatorId);
if (device) {
device.removeSession(connectionId);
}
if (operator) {
operator.removeSession(connectionId);
}
this.logger.info(`🔌 Connection terminated: ${connectionId} - ${reason}`);
return connection;
}
/**
* Обработка таймаута соединения
* @param {string} connectionId
*/
handleConnectionTimeout(connectionId) {
this.logger.warn(`⏰ Connection timeout: ${connectionId}`);
this.rejectConnection(connectionId, 'Connection timeout');
// Уведомляем участников о таймауте
const request = this.connectionRequests.get(connectionId);
if (request) {
const operator = this.deviceManager.getOperator(request.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:timeout', {
connectionId,
deviceId: request.deviceId,
error: 'Connection request timeout'
});
}
}
}
/**
* Очистка таймаута соединения
* @param {string} connectionId
*/
clearConnectionTimeout(connectionId) {
const timeoutId = this.connectionTimeouts.get(connectionId);
if (timeoutId) {
clearTimeout(timeoutId);
this.connectionTimeouts.delete(connectionId);
}
}
/**
* Получение статистики соединений
* @returns {object}
*/
getConnectionStats() {
return {
pendingRequests: this.connectionRequests.size,
activeConnections: this.activeConnections.size,
totalRequestsProcessed: this.connectionRequests.size + this.activeConnections.size,
averageConnectionTime: this.calculateAverageConnectionTime()
};
}
/**
* Расчет среднего времени установки соединения
* @returns {number} время в миллисекундах
*/
calculateAverageConnectionTime() {
if (this.activeConnections.size === 0) return 0;
let totalTime = 0;
let count = 0;
for (const connection of this.activeConnections.values()) {
if (connection.establishedAt) {
// Примерное время установки соединения (можно улучшить, сохраняя время запроса)
totalTime += 2000; // placeholder
count++;
}
}
return count > 0 ? totalTime / count : 0;
}
/**
* Получение активного соединения
* @param {string} connectionId
* @returns {object|null}
*/
getConnection(connectionId) {
return this.activeConnections.get(connectionId) || null;
}
/**
* Получение всех активных соединений для оператора
* @param {string} operatorId
* @returns {Array}
*/
getOperatorConnections(operatorId) {
return Array.from(this.activeConnections.values())
.filter(conn => conn.operatorId === operatorId);
}
/**
* Получение всех активных соединений для устройства
* @param {string} deviceId
* @returns {Array}
*/
getDeviceConnections(deviceId) {
return Array.from(this.activeConnections.values())
.filter(conn => conn.deviceId === deviceId);
}
/**
* Очистка устаревших запросов и неактивных соединений
*/
async cleanup() {
const now = new Date();
// Очищаем устаревшие запросы
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (now > request.timeoutAt) {
this.handleConnectionTimeout(connectionId);
}
}
// Проверяем активные соединения на отключенные сокеты
for (const [connectionId, connection] of this.activeConnections.entries()) {
const device = this.deviceManager.getDevice(connection.deviceId);
const operator = this.deviceManager.getOperator(connection.operatorId);
if (!device || !device.isConnected() || !operator || !operator.isConnected()) {
try {
await this.terminateConnection(connectionId, 'Participant disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
}
this.logger.debug(`🧹 Connection cleanup completed. Active: ${this.activeConnections.size}, Pending: ${this.connectionRequests.size}`);
}
/**
* Очистка всех подключений устройства при его отключении
* @param {string} deviceId
*/
async cleanupDeviceConnections(deviceId) {
this.logger.info(`🧹 Cleaning up connections for device: ${deviceId}`);
const connectionsToTerminate = [];
// Находим все активные подключения устройства
for (const [connectionId, connection] of this.activeConnections.entries()) {
if (connection.deviceId === deviceId) {
connectionsToTerminate.push(connectionId);
}
}
// Находим все ожидающие запросы для устройства
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (request.deviceId === deviceId) {
connectionsToTerminate.push(connectionId);
}
}
// Завершаем все найденные подключения
for (const connectionId of connectionsToTerminate) {
try {
await this.terminateConnection(connectionId, 'Device disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for device ${deviceId}`);
}
/**
* Очистка всех подключений оператора при его отключении
* @param {string} operatorId
*/
async cleanupOperatorConnections(operatorId) {
this.logger.info(`🧹 Cleaning up connections for operator: ${operatorId}`);
const connectionsToTerminate = [];
// Находим все активные подключения оператора
for (const [connectionId, connection] of this.activeConnections.entries()) {
if (connection.operatorId === operatorId) {
connectionsToTerminate.push(connectionId);
}
}
// Находим все ожидающие запросы от оператора
for (const [connectionId, request] of this.connectionRequests.entries()) {
if (request.operatorId === operatorId) {
connectionsToTerminate.push(connectionId);
}
}
// Завершаем все найденные подключения
for (const connectionId of connectionsToTerminate) {
try {
await this.terminateConnection(connectionId, 'Operator disconnected');
} catch (error) {
this.logger.error(`Error terminating connection ${connectionId}:`, error);
}
}
this.logger.info(`🧹 Cleaned up ${connectionsToTerminate.length} connections for operator ${operatorId}`);
}
}
module.exports = { ConnectionManager };

View File

@@ -12,9 +12,29 @@ class DeviceManager {
* @returns {DeviceInfo}
*/
registerDevice(deviceId, deviceInfo, socket) {
const device = new DeviceInfo(deviceId, deviceInfo, socket);
this.devices.set(deviceId, device);
return device;
// Проверяем, существует ли уже устройство с этим ID
const existingDevice = this.devices.get(deviceId);
if (existingDevice) {
// Обновляем существующее устройство при переподключении
existingDevice.socket = socket;
existingDevice.updateStatus('connected');
existingDevice.connectedAt = Date.now();
existingDevice.lastSeen = Date.now();
// Обновляем информацию об устройстве
existingDevice.deviceInfo = { ...existingDevice.deviceInfo, ...deviceInfo };
existingDevice.capabilities = existingDevice.parseCapabilities(deviceInfo);
console.log(`Device reconnected: ${deviceId}`);
return existingDevice;
} else {
// Создаем новое устройство
const device = new DeviceInfo(deviceId, deviceInfo, socket);
this.devices.set(deviceId, device);
console.log(`New device registered: ${deviceId}`);
return device;
}
}
/**
@@ -25,9 +45,31 @@ class DeviceManager {
* @returns {OperatorInfo}
*/
registerOperator(operatorId, operatorInfo, socket) {
const operator = new OperatorInfo(operatorId, operatorInfo, socket);
this.operators.set(operatorId, operator);
return operator;
// Проверяем, существует ли уже оператор с этим ID
const existingOperator = this.operators.get(operatorId);
if (existingOperator) {
// Обновляем существующего оператора при переподключении
existingOperator.socket = socket;
existingOperator.status = 'connected';
existingOperator.connectedAt = Date.now();
existingOperator.lastSeen = Date.now();
// Обновляем информацию об операторе только если она предоставлена
if (operatorInfo) {
existingOperator.operatorInfo = { ...existingOperator.operatorInfo, ...operatorInfo };
existingOperator.permissions = operatorInfo.permissions || existingOperator.permissions;
}
console.log(`Operator reconnected: ${operatorId}`);
return existingOperator;
} else {
// Создаем нового оператора
const operator = new OperatorInfo(operatorId, operatorInfo, socket);
this.operators.set(operatorId, operator);
console.log(`New operator registered: ${operatorId}`);
return operator;
}
}
/**
@@ -84,7 +126,8 @@ class DeviceManager {
const device = this.devices.get(deviceId);
if (device) {
device.disconnect();
this.devices.delete(deviceId);
// НЕ удаляем устройство из Map, только обновляем статус
// Устройство может быть переподключено позже
}
}
@@ -96,10 +139,116 @@ class DeviceManager {
const operator = this.operators.get(operatorId);
if (operator) {
operator.disconnect();
this.operators.delete(operatorId);
// НЕ удаляем оператора из Map, только обновляем статус
// Оператор может быть переподключен позже
}
}
/**
* Получение статистики подключений
* @returns {object}
*/
getConnectionStats() {
const connectedDevices = this.getConnectedDevices();
const connectedOperators = this.getConnectedOperators();
return {
devices: {
total: this.devices.size,
connected: connectedDevices.length,
available: connectedDevices.filter(d => d.canAcceptNewSession()).length,
busy: connectedDevices.filter(d => !d.canAcceptNewSession()).length
},
operators: {
total: this.operators.size,
connected: connectedOperators.length,
active: connectedOperators.filter(o => o.hasActiveSessions()).length
}
};
}
/**
* Поиск устройства по сокету
* @param {object} socket
* @returns {DeviceInfo|null}
*/
findDeviceBySocket(socket) {
for (const device of this.devices.values()) {
if (device.socket === socket) {
return device;
}
}
return null;
}
/**
* Поиск оператора по сокету
* @param {object} socket
* @returns {OperatorInfo|null}
*/
findOperatorBySocket(socket) {
for (const operator of this.operators.values()) {
if (operator.socket === socket) {
return operator;
}
}
return null;
}
/**
* Очистка отключенных устройств и операторов
*/
cleanupDisconnected() {
// Очищаем отключенные устройства
for (const [deviceId, device] of this.devices.entries()) {
if (!device.isConnected()) {
console.log(`Removing disconnected device: ${deviceId}`);
this.devices.delete(deviceId);
}
}
// Очищаем отключенных операторов
for (const [operatorId, operator] of this.operators.entries()) {
if (!operator.isConnected()) {
console.log(`Removing disconnected operator: ${operatorId}`);
this.operators.delete(operatorId);
}
}
}
/**
* Проверка возможности создания сессии
* @param {string} deviceId
* @param {string} operatorId
* @returns {object} {canConnect: boolean, reason?: string}
*/
canCreateSession(deviceId, operatorId) {
const device = this.getDevice(deviceId);
const operator = this.getOperator(operatorId);
if (!device) {
return { canConnect: false, reason: 'Device not found' };
}
if (!operator) {
return { canConnect: false, reason: 'Operator not found' };
}
if (!device.isConnected()) {
return { canConnect: false, reason: 'Device not connected' };
}
if (!operator.isConnected()) {
return { canConnect: false, reason: 'Operator not connected' };
}
if (!device.canAcceptNewSession()) {
return { canConnect: false, reason: 'Device busy or unavailable' };
}
return { canConnect: true };
}
/**
* Обновление местоположения устройства
* @param {string} deviceId
@@ -226,7 +375,15 @@ class DeviceInfo {
* @returns {object}
*/
parseCapabilities(deviceInfo) {
const availableCameras = deviceInfo.availableCameras?.split(',') || ['back'];
// Поддерживаем как массив, так и строку для availableCameras
let availableCameras;
if (Array.isArray(deviceInfo.availableCameras)) {
availableCameras = deviceInfo.availableCameras;
} else if (typeof deviceInfo.availableCameras === 'string') {
availableCameras = deviceInfo.availableCameras.split(',');
} else {
availableCameras = ['back']; // fallback
}
return {
cameras: availableCameras,
@@ -243,7 +400,10 @@ class DeviceInfo {
* @returns {boolean}
*/
isConnected() {
return this.socket && this.socket.connected && this.status === 'connected';
// Устройство подключено если есть активный сокет и статус не 'disconnected'
return this.socket &&
this.socket.connected &&
this.status !== 'disconnected';
}
/**
@@ -366,7 +526,7 @@ class OperatorInfo {
this.lastSeen = Date.now();
this.activeSessions = new Set();
this.totalSessions = 0;
this.permissions = this.operatorInfo.permissions || ['view_cameras', 'request_camera'];
this.permissions = this.operatorInfo.permissions || ['view_cameras', 'request_camera', 'initiate_connection'];
}
/**
@@ -403,6 +563,22 @@ class OperatorInfo {
this.activeSessions.delete(sessionId);
}
/**
* Проверка наличия активных сессий
* @returns {boolean}
*/
hasActiveSessions() {
return this.activeSessions.size > 0;
}
/**
* Получение количества активных сессий
* @returns {number}
*/
getActiveSessionsCount() {
return this.activeSessions.size;
}
/**
* Обновление последнего времени активности
*/

View File

@@ -334,4 +334,192 @@ router.post('/ping/:deviceId', authenticateOperator, requirePermission('view_cam
}
});
/**
* POST /api/operators/connections/request
* Инициация подключения к устройству
*/
router.post('/connections/request', authenticateOperator, requirePermission('initiate_connection'), async (req, res) => {
try {
const { deviceId, cameraType = 'back' } = req.body;
const { connectionManager } = req.app.locals;
if (!deviceId) {
return res.status(400).json({ error: 'Device ID required' });
}
const result = await connectionManager.initiateConnection(
req.operator.operatorId,
deviceId,
cameraType
);
res.json({
success: true,
connectionId: result.connectionId,
sessionId: result.sessionId,
message: 'Connection request initiated'
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
/**
* PUT /api/operators/connections/:connectionId/accept
* Принятие подключения (используется Android устройством)
*/
router.put('/connections/:connectionId/accept', async (req, res) => {
try {
const { connectionId } = req.params;
const { connectionManager } = req.app.locals;
const result = await connectionManager.acceptConnection(connectionId, req.body);
res.json({
success: true,
connection: result,
message: 'Connection accepted'
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
/**
* PUT /api/operators/connections/:connectionId/reject
* Отклонение подключения
*/
router.put('/connections/:connectionId/reject', async (req, res) => {
try {
const { connectionId } = req.params;
const { reason = 'User rejected' } = req.body;
const { connectionManager } = req.app.locals;
const result = await connectionManager.rejectConnection(connectionId, reason);
res.json({
success: true,
message: 'Connection rejected',
reason: reason
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
/**
* DELETE /api/operators/connections/:connectionId
* Завершение активного подключения
*/
router.delete('/connections/:connectionId', async (req, res) => {
try {
const { connectionId } = req.params;
const { reason = 'Connection terminated by user' } = req.body;
const { connectionManager } = req.app.locals;
const result = await connectionManager.terminateConnection(connectionId, reason);
res.json({
success: true,
message: 'Connection terminated',
reason: reason
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
/**
* GET /api/operators/connections
* Получение списка подключений оператора
*/
router.get('/connections', authenticateOperator, (req, res) => {
try {
const { connectionManager } = req.app.locals;
const connections = connectionManager.getOperatorConnections(req.operator.operatorId);
res.json({
success: true,
connections: connections,
total: connections.length
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* GET /api/operators/connections/:connectionId
* Получение информации о конкретном подключении
*/
router.get('/connections/:connectionId', (req, res) => {
try {
const { connectionId } = req.params;
const { connectionManager } = req.app.locals;
const connection = connectionManager.getConnection(connectionId);
if (!connection) {
return res.status(404).json({ error: 'Connection not found' });
}
res.json({
success: true,
connection: connection
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* GET /api/operators/connections/:connectionId/webrtc
* Получение WebRTC информации подключения
*/
router.get('/connections/:connectionId/webrtc', (req, res) => {
try {
const { connectionId } = req.params;
const { connectionManager } = req.app.locals;
const webrtcInfo = connectionManager.getWebRTCInfo(connectionId);
if (!webrtcInfo) {
return res.status(404).json({ error: 'WebRTC info not found' });
}
res.json({
success: true,
webrtc: webrtcInfo
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* GET /api/operators/stats
* Получение статистики подключений
*/
router.get('/stats', authenticateOperator, (req, res) => {
try {
const { connectionManager } = req.app.locals;
const stats = connectionManager.getConnectionStats();
res.json({
success: true,
stats: stats
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,3 +1,5 @@
const path = require('path');
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
@@ -8,6 +10,7 @@ const winston = require('winston');
// Импорт наших менеджеров
const { SessionManager } = require('./managers/SessionManager');
const { DeviceManager } = require('./managers/DeviceManager');
const { ConnectionManager } = require('./managers/ConnectionManager');
// Импорт роутов
const operatorsRouter = require('./routes/operators');
@@ -38,15 +41,41 @@ const io = socketIo(server, {
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public')); // Для статических файлов демо
app.use(express.static(path.join(__dirname, '../public'))); // Статические файлы
// Роуты для веб-интерфейсов
app.get('/', (req, res) => {
const userAgent = req.headers['user-agent'] || '';
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
if (isMobile) {
// Перенаправляем мобильные устройства на мобильную версию
res.sendFile(path.join(__dirname, '../public/mobile.html'));
} else {
// Десктопная версия (демо)
res.sendFile(path.join(__dirname, '../public/index.html'));
}
});
// Принудительная мобильная версия
app.get('/mobile', (req, res) => {
res.sendFile(path.join(__dirname, '../public/mobile.html'));
});
// Принудительная десктопная версия
app.get('/demo', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// Инициализация менеджеров
const sessionManager = new SessionManager();
const deviceManager = new DeviceManager();
const connectionManager = new ConnectionManager(sessionManager, deviceManager, logger);
// Делаем менеджеры доступными в роутах
app.locals.sessionManager = sessionManager;
app.locals.deviceManager = deviceManager;
app.locals.connectionManager = connectionManager;
app.locals.logger = logger;
app.locals.io = io;
@@ -81,7 +110,36 @@ app.get('/api/devices', (req, res) => {
// WebSocket обработчики
io.on('connection', (socket) => {
logger.info(`New connection: ${socket.id}`);
const userAgent = socket.handshake.headers['user-agent'] || '';
const isAndroidClient = userAgent.includes('okhttp');
const isMobileWeb = !isAndroidClient && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
logger.info(`New connection: ${socket.id}`, {
address: socket.handshake.address,
userAgent: userAgent,
isAndroid: isAndroidClient,
isMobileWeb: isMobileWeb
});
if (isAndroidClient) {
logger.info(`🤖 Android client connected: ${socket.id}`);
// Логируем все события от Android клиента
socket.onAny((eventName, ...args) => {
logger.info(`📱 Android event: ${eventName}`, args[0]);
});
// Отправляем приветственное сообщение Android клиенту
socket.emit('server:hello', {
message: 'Server ready for registration',
expectedEvent: 'register:android'
});
} else if (isMobileWeb) {
logger.info(`📱 Mobile web client connected: ${socket.id}`);
// Логируем события от мобильного веб-клиента
socket.onAny((eventName, ...args) => {
logger.info(`🌐 Mobile web event: ${eventName}`, args[0]);
});
}
// Регистрация Android клиента
socket.on('register:android', (data) => {
@@ -107,8 +165,46 @@ io.on('connection', (socket) => {
socket.emit('register:success', { deviceId });
});
// Регистрация оператора
// Регистрация мобильного веб-клиента
socket.on('register:mobile_web', (data) => {
const { deviceId, deviceInfo } = data;
// Регистрируем мобильное веб-устройство
const device = deviceManager.registerDevice(deviceId, {
...deviceInfo,
platform: 'mobile_web',
type: 'web_camera'
}, socket);
logger.info(`Mobile web client registered: ${deviceId}`, deviceInfo);
// Уведомляем всех операторов о новом устройстве
const operatorSockets = Array.from(deviceManager.operators.values())
.filter(op => op.isConnected())
.map(op => op.socket);
operatorSockets.forEach(opSocket => {
opSocket.emit('device:connected', {
deviceId,
deviceInfo: device.getSummary()
});
});
socket.emit('register:success', { deviceId });
});
// Fallback: если Android отправляет register:operator вместо register:android
socket.on('register:operator', (data) => {
const userAgent = socket.handshake.headers['user-agent'] || '';
if (userAgent.includes('okhttp')) {
logger.warn(`🚨 Android client sent wrong event! ${socket.id} sent 'register:operator' instead of 'register:android'`);
socket.emit('register:error', {
error: 'Android clients should use register:android event, not register:operator'
});
return;
}
// Обычная обработка для реальных операторов
const { operatorId, operatorInfo } = data;
const finalOperatorId = operatorId || uuidv4();
@@ -127,47 +223,122 @@ io.on('connection', (socket) => {
});
});
// Запрос на подключение к камере (только через WebSocket для обратной совместимости)
socket.on('camera:request', (data) => {
// Запрос на подключение к камере через ConnectionManager
socket.on('camera:request', async (data) => {
const { deviceId, cameraType = 'back' } = data;
logger.info(`📷 Camera request received from operator socket ${socket.id}`);
logger.info(`📷 Request data:`, { deviceId, cameraType });
// Получаем оператора из менеджера устройств
const operator = Array.from(deviceManager.operators.values())
.find(op => op.socket === socket);
if (!operator) {
logger.error(`❌ Operator not found for socket ${socket.id}`);
socket.emit('camera:error', { error: 'Operator not registered' });
return;
}
const device = deviceManager.getDevice(deviceId);
if (!device || !device.canAcceptNewSession()) {
socket.emit('camera:error', { error: 'Device not available' });
return;
logger.info(`✅ Operator found: ${operator.operatorId}`);
try {
// Используем ConnectionManager для создания подключения
const connection = await connectionManager.initiateConnection(
operator.operatorId,
deviceId,
cameraType
);
logger.info(`✅ Connection initiated: ${connection.connectionId}`);
// Уведомляем оператора о создании подключения
socket.emit('connection:initiated', {
connectionId: connection.connectionId,
sessionId: connection.sessionId,
deviceId: deviceId,
cameraType: cameraType,
status: 'pending',
createdAt: new Date().toISOString()
});
} catch (error) {
logger.error(`❌ Failed to initiate connection: ${error.message}`);
socket.emit('camera:error', { error: error.message });
}
// Создаем сессию
const session = sessionManager.createSession(deviceId, operator.operatorId, cameraType);
logger.info(`Camera request: operator ${operator.operatorId} -> device ${deviceId}`);
// Отправляем запрос Android клиенту
device.socket.emit('camera:request', {
sessionId: session.sessionId,
operatorId: operator.operatorId,
cameraType
});
// Добавляем сессию к участникам
device.addSession(session.sessionId);
operator.addSession(session.sessionId);
socket.emit('camera:request-sent', { sessionId: session.sessionId, deviceId });
});
// Ответ от Android клиента на запрос камеры
socket.on('camera:response', (data) => {
// Ответ от Android клиента на запрос камеры через ConnectionManager
socket.on('camera:response', async (data) => {
const { sessionId, accepted, streamUrl, error } = data;
logger.info(`📱 Camera response received from Android: sessionId=${sessionId}, accepted=${accepted}`);
try {
if (accepted) {
// Принимаем подключение через ConnectionManager
const connection = await connectionManager.acceptConnection(sessionId, { streamUrl });
logger.info(`✅ Connection accepted: ${connection.connectionId}`);
// Получаем оператора для уведомления
const operator = deviceManager.getOperator(connection.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:accepted', {
connectionId: connection.connectionId,
sessionId: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType,
streamUrl: streamUrl,
status: 'active'
});
// Отправляем старое событие для обратной совместимости
operator.socket.emit('camera:response', {
success: true,
sessionId: sessionId,
session: {
id: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType
}
});
}
} else {
// Отклоняем подключение через ConnectionManager
await connectionManager.rejectConnection(sessionId, error);
logger.info(`❌ Connection rejected: sessionId=${sessionId}, error=${error}`);
// Находим подключение для получения информации об операторе
const connection = connectionManager.getConnection(sessionId);
if (connection) {
const operator = deviceManager.getOperator(connection.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:rejected', {
sessionId: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType,
error: error
});
// Отправляем старое событие для обратной совместимости
operator.socket.emit('camera:response', {
success: false,
sessionId: sessionId,
message: error
});
}
}
}
} catch (error) {
logger.error(`❌ Failed to handle camera response: ${error.message}`);
socket.emit('camera:error', { error: error.message });
}
// Переключение камеры в активной сессии
socket.on('camera:switch', (data) => {
const { sessionId, cameraType } = data;
const session = sessionManager.getSession(sessionId);
if (!session) {
@@ -175,27 +346,76 @@ io.on('connection', (socket) => {
return;
}
// Получаем участников сессии
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
if (!operator || !operator.isConnected()) {
socket.emit('camera:error', { error: 'Operator not found' });
// Проверяем, что запрос идет от оператора этой сессии
if (!operator || operator.socket !== socket) {
socket.emit('camera:error', { error: 'Unauthorized to switch camera in this session' });
return;
}
if (accepted) {
session.updateStatus('active', { streamUrl });
logger.info(`Camera stream started: session ${sessionId}`);
operator.socket.emit('camera:stream-ready', { sessionId, streamUrl });
} else {
session.updateStatus('denied', { error });
logger.info(`Camera request denied: session ${sessionId}`, error);
operator.socket.emit('camera:denied', { sessionId, error });
// Очищаем отклоненную сессию
const device = deviceManager.getDevice(session.deviceId);
if (device) device.removeSession(sessionId);
operator.removeSession(sessionId);
sessionManager.closeSession(sessionId);
// Проверяем, что сессия активна
if (session.status !== 'active') {
socket.emit('camera:error', { error: 'Session is not active' });
return;
}
logger.info(`Camera switch requested: session ${sessionId}, camera ${cameraType}`);
// Отправляем запрос на переключение устройству
if (device && device.isConnected()) {
device.socket.emit('camera:switch', {
sessionId: sessionId,
cameraType: cameraType
});
// Обновляем тип камеры в сессии
session.cameraType = cameraType;
} else {
socket.emit('camera:error', { error: 'Device not connected' });
}
});
// Завершение сессии по инициативе оператора
socket.on('session:end', (data) => {
const { sessionId } = data;
const session = sessionManager.getSession(sessionId);
if (!session) {
socket.emit('session:error', { error: 'Session not found' });
return;
}
// Получаем участников сессии
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Проверяем, что запрос идет от оператора этой сессии
if (!operator || operator.socket !== socket) {
socket.emit('session:error', { error: 'Unauthorized to end this session' });
return;
}
logger.info(`Session ended by operator: ${sessionId}`);
// Уведомляем устройство о завершении
if (device && device.isConnected()) {
device.socket.emit('camera:disconnect', { sessionId });
device.removeSession(sessionId);
}
// Уведомляем оператора о завершении
operator.socket.emit('session:ended', {
sessionId: sessionId,
deviceId: session.deviceId,
reason: 'Ended by operator'
});
operator.removeSession(sessionId);
// Закрываем сессию
sessionManager.closeSession(sessionId);
});
// WebRTC сигнализация
@@ -212,11 +432,15 @@ io.on('connection', (socket) => {
if (device && device.socket === socket && operator && operator.isConnected()) {
operator.socket.emit('webrtc:offer', { sessionId, offer });
session.updateWebRTCState('offer_sent');
// Обновляем состояние в ConnectionManager
connectionManager.updateWebRTCState(sessionId, 'offer_sent');
}
// Если отправитель - оператор, то получатель - устройство
else if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('webrtc:offer', { sessionId, offer });
session.updateWebRTCState('offer_sent');
// Обновляем состояние в ConnectionManager
connectionManager.updateWebRTCState(sessionId, 'offer_sent');
}
}
});
@@ -233,11 +457,15 @@ io.on('connection', (socket) => {
if (device && device.socket === socket && operator && operator.isConnected()) {
operator.socket.emit('webrtc:answer', { sessionId, answer });
session.updateWebRTCState('answer_sent');
// Обновляем состояние в ConnectionManager
connectionManager.updateWebRTCState(sessionId, 'answer_sent');
}
// Если отправитель - оператор, то получатель - устройство
else if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('webrtc:answer', { sessionId, answer });
session.updateWebRTCState('answer_sent');
// Обновляем состояние в ConnectionManager
connectionManager.updateWebRTCState(sessionId, 'answer_sent');
}
}
});
@@ -259,6 +487,38 @@ io.on('connection', (socket) => {
}
});
// Обработчик установки WebRTC соединения
socket.on('webrtc:connected', (data) => {
const { sessionId } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
session.updateWebRTCState('connected');
// Обновляем состояние в ConnectionManager
connectionManager.updateWebRTCState(sessionId, 'connected');
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Уведомляем обе стороны о переходе в прямой режим
if (device && device.isConnected()) {
device.socket.emit('webrtc:direct-mode', {
sessionId,
message: 'WebRTC connection established, switching to direct mode'
});
}
if (operator && operator.isConnected()) {
operator.socket.emit('webrtc:direct-mode', {
sessionId,
message: 'WebRTC connection established, switching to direct mode'
});
}
logger.info(`🔗 WebRTC connection established: ${sessionId}`);
}
});
// Переключение типа камеры
socket.on('camera:switch', (data) => {
const { sessionId, cameraType } = data;
@@ -294,6 +554,11 @@ io.on('connection', (socket) => {
if (operator && operator.isConnected()) {
operator.socket.emit('camera:disconnected', { sessionId });
operator.socket.emit('session:ended', {
sessionId: sessionId,
deviceId: session.deviceId,
reason: 'Device disconnected'
});
operator.removeSession(sessionId);
}
@@ -309,8 +574,81 @@ io.on('connection', (socket) => {
}
});
// Обработка отключения
socket.on('disconnect', (reason) => {
// Новые события для ConnectionManager
// Завершение подключения от оператора
socket.on('connection:terminate', async (data) => {
const { connectionId } = data;
logger.info(`🔚 Connection termination requested: ${connectionId}`);
try {
await connectionManager.terminateConnection(connectionId);
socket.emit('connection:terminated', {
connectionId: connectionId,
timestamp: new Date().toISOString()
});
logger.info(`✅ Connection terminated: ${connectionId}`);
} catch (error) {
logger.error(`❌ Failed to terminate connection: ${error.message}`);
socket.emit('connection:error', { error: error.message });
}
});
// Запрос статистики подключений
socket.on('connection:status', (data, callback) => {
const stats = connectionManager.getConnectionStats();
if (callback) {
callback({
success: true,
stats: stats,
timestamp: new Date().toISOString()
});
} else {
socket.emit('connection:status_response', {
stats: stats,
timestamp: new Date().toISOString()
});
}
});
});
// Список активных подключений для оператора
socket.on('connection:list', (data, callback) => {
const operator = Array.from(deviceManager.operators.values())
.find(op => op.socket === socket);
if (!operator) {
const error = 'Operator not found';
if (callback) {
callback({ success: false, error });
} else {
socket.emit('connection:error', { error });
}
return;
}
const connections = connectionManager.getOperatorConnections(operator.operatorId);
if (callback) {
callback({
success: true,
connections: connections,
timestamp: new Date().toISOString()
});
} else {
socket.emit('connection:list_response', {
connections: connections,
timestamp: new Date().toISOString()
});
}
});
// Обработка отключения с ConnectionManager
socket.on('disconnect', async (reason) => {
logger.info(`Client disconnected: ${socket.id}, reason: ${reason}`);
// Находим устройство или оператора по сокету
@@ -321,6 +659,9 @@ io.on('connection', (socket) => {
.find(op => op.socket === socket);
if (device) {
// Очищаем подключения устройства через ConnectionManager
await connectionManager.cleanupDeviceConnections(device.deviceId);
// Уведомляем операторов об отключении устройства
const operators = deviceManager.getConnectedOperators();
operators.forEach(op => {
@@ -335,6 +676,9 @@ io.on('connection', (socket) => {
}
if (operator) {
// Очищаем подключения оператора через ConnectionManager
await connectionManager.cleanupOperatorConnections(operator.operatorId);
// Завершаем активные сессии оператора
sessionManager.closeOperatorSessions(operator.operatorId);
deviceManager.disconnectOperator(operator.operatorId);
@@ -370,12 +714,15 @@ process.on('SIGINT', () => {
});
const PORT = process.env.PORT || 3001;
const HOST = process.env.HOST || '0.0.0.0'; // Слушаем на всех интерфейсах для доступа из эмулятора
server.listen(PORT, () => {
logger.info(`GodEye Backend Server running on port ${PORT}`);
console.log(`🚀 Server started on http://localhost:${PORT}`);
server.listen(PORT, HOST, () => {
logger.info(`GodEye Backend Server running on ${HOST}:${PORT}`);
console.log(`🚀 Server started on http://${HOST}:${PORT}`);
console.log(`📊 Admin API: http://localhost:${PORT}/api/admin/stats`);
console.log(`👥 Operators API: http://localhost:${PORT}/api/operators/devices`);
console.log(`📱 Status: http://localhost:${PORT}/api/status`);
console.log(`🌐 Demo: http://localhost:${PORT}/`);
});
console.log(`📱 Android emulator: http://10.0.2.2:${PORT}/`);
});

539
backend/test_chunk.js Normal file
View File

@@ -0,0 +1,539 @@
io.on('connection', (socket) => {
const userAgent = socket.handshake.headers['user-agent'] || '';
const isAndroidClient = userAgent.includes('okhttp');
const isMobileWeb = !isAndroidClient && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
logger.info(`New connection: ${socket.id}`, {
address: socket.handshake.address,
userAgent: userAgent,
isAndroid: isAndroidClient,
isMobileWeb: isMobileWeb
});
if (isAndroidClient) {
logger.info(`🤖 Android client connected: ${socket.id}`);
// Логируем все события от Android клиента
socket.onAny((eventName, ...args) => {
logger.info(`📱 Android event: ${eventName}`, args[0]);
});
// Отправляем приветственное сообщение Android клиенту
socket.emit('server:hello', {
message: 'Server ready for registration',
expectedEvent: 'register:android'
});
} else if (isMobileWeb) {
logger.info(`📱 Mobile web client connected: ${socket.id}`);
// Логируем события от мобильного веб-клиента
socket.onAny((eventName, ...args) => {
logger.info(`🌐 Mobile web event: ${eventName}`, args[0]);
});
}
// Регистрация Android клиента
socket.on('register:android', (data) => {
const { deviceId, deviceInfo } = data;
// Регистрируем устройство через DeviceManager
const device = deviceManager.registerDevice(deviceId, deviceInfo, socket);
logger.info(`Android client registered: ${deviceId}`, deviceInfo);
// Уведомляем всех операторов о новом устройстве
const operatorSockets = Array.from(deviceManager.operators.values())
.filter(op => op.isConnected())
.map(op => op.socket);
operatorSockets.forEach(opSocket => {
opSocket.emit('device:connected', {
deviceId,
deviceInfo: device.getSummary()
});
});
socket.emit('register:success', { deviceId });
});
// Регистрация мобильного веб-клиента
socket.on('register:mobile_web', (data) => {
const { deviceId, deviceInfo } = data;
// Регистрируем мобильное веб-устройство
const device = deviceManager.registerDevice(deviceId, {
...deviceInfo,
platform: 'mobile_web',
type: 'web_camera'
}, socket);
logger.info(`Mobile web client registered: ${deviceId}`, deviceInfo);
// Уведомляем всех операторов о новом устройстве
const operatorSockets = Array.from(deviceManager.operators.values())
.filter(op => op.isConnected())
.map(op => op.socket);
operatorSockets.forEach(opSocket => {
opSocket.emit('device:connected', {
deviceId,
deviceInfo: device.getSummary()
});
});
socket.emit('register:success', { deviceId });
});
// Fallback: если Android отправляет register:operator вместо register:android
socket.on('register:operator', (data) => {
const userAgent = socket.handshake.headers['user-agent'] || '';
if (userAgent.includes('okhttp')) {
logger.warn(`🚨 Android client sent wrong event! ${socket.id} sent 'register:operator' instead of 'register:android'`);
socket.emit('register:error', {
error: 'Android clients should use register:android event, not register:operator'
});
return;
}
// Обычная обработка для реальных операторов
const { operatorId, operatorInfo } = data;
const finalOperatorId = operatorId || uuidv4();
// Регистрируем оператора через DeviceManager
const operator = deviceManager.registerOperator(finalOperatorId, operatorInfo, socket);
logger.info(`Operator registered: ${finalOperatorId}`);
// Отправляем список доступных устройств
const availableDevices = deviceManager.getAvailableDevicesForOperator(finalOperatorId);
const devicesData = availableDevices.map(device => device.getSummary());
socket.emit('register:success', {
operatorId: finalOperatorId,
availableDevices: devicesData
});
});
// Запрос на подключение к камере через ConnectionManager
socket.on('camera:request', async (data) => {
const { deviceId, cameraType = 'back' } = data;
logger.info(`📷 Camera request received from operator socket ${socket.id}`);
logger.info(`📷 Request data:`, { deviceId, cameraType });
// Получаем оператора из менеджера устройств
const operator = Array.from(deviceManager.operators.values())
.find(op => op.socket === socket);
if (!operator) {
logger.error(`❌ Operator not found for socket ${socket.id}`);
socket.emit('camera:error', { error: 'Operator not registered' });
return;
}
logger.info(`✅ Operator found: ${operator.operatorId}`);
try {
// Используем ConnectionManager для создания подключения
const connection = await connectionManager.initiateConnection(
operator.operatorId,
deviceId,
cameraType
);
logger.info(`✅ Connection initiated: ${connection.connectionId}`);
// Уведомляем оператора о создании подключения
socket.emit('connection:initiated', {
connectionId: connection.connectionId,
sessionId: connection.sessionId,
deviceId: deviceId,
cameraType: cameraType,
status: 'pending',
createdAt: new Date().toISOString()
});
} catch (error) {
logger.error(`❌ Failed to initiate connection: ${error.message}`);
socket.emit('camera:error', { error: error.message });
}
});
// Ответ от Android клиента на запрос камеры через ConnectionManager
socket.on('camera:response', async (data) => {
const { sessionId, accepted, streamUrl, error } = data;
logger.info(`📱 Camera response received from Android: sessionId=${sessionId}, accepted=${accepted}`);
try {
if (accepted) {
// Принимаем подключение через ConnectionManager
const connection = await connectionManager.acceptConnection(sessionId, { streamUrl });
logger.info(`✅ Connection accepted: ${connection.connectionId}`);
// Получаем оператора для уведомления
const operator = deviceManager.getOperator(connection.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:accepted', {
connectionId: connection.connectionId,
sessionId: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType,
streamUrl: streamUrl,
status: 'active'
});
// Отправляем старое событие для обратной совместимости
operator.socket.emit('camera:response', {
success: true,
sessionId: sessionId,
session: {
id: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType
}
});
}
} else {
// Отклоняем подключение через ConnectionManager
await connectionManager.rejectConnection(sessionId, error);
logger.info(`❌ Connection rejected: sessionId=${sessionId}, error=${error}`);
// Находим подключение для получения информации об операторе
const connection = connectionManager.getConnection(sessionId);
if (connection) {
const operator = deviceManager.getOperator(connection.operatorId);
if (operator && operator.isConnected()) {
operator.socket.emit('connection:rejected', {
sessionId: sessionId,
deviceId: connection.deviceId,
cameraType: connection.cameraType,
error: error
});
// Отправляем старое событие для обратной совместимости
operator.socket.emit('camera:response', {
success: false,
sessionId: sessionId,
message: error
});
}
}
}
} catch (error) {
logger.error(`❌ Failed to handle camera response: ${error.message}`);
socket.emit('camera:error', { error: error.message });
}
// Переключение камеры в активной сессии
socket.on('camera:switch', (data) => {
const { sessionId, cameraType } = data;
const session = sessionManager.getSession(sessionId);
if (!session) {
socket.emit('camera:error', { error: 'Session not found' });
return;
}
// Получаем участников сессии
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Проверяем, что запрос идет от оператора этой сессии
if (!operator || operator.socket !== socket) {
socket.emit('camera:error', { error: 'Unauthorized to switch camera in this session' });
return;
}
// Проверяем, что сессия активна
if (session.status !== 'active') {
socket.emit('camera:error', { error: 'Session is not active' });
return;
}
logger.info(`Camera switch requested: session ${sessionId}, camera ${cameraType}`);
// Отправляем запрос на переключение устройству
if (device && device.isConnected()) {
device.socket.emit('camera:switch', {
sessionId: sessionId,
cameraType: cameraType
});
// Обновляем тип камеры в сессии
session.cameraType = cameraType;
} else {
socket.emit('camera:error', { error: 'Device not connected' });
}
});
// Завершение сессии по инициативе оператора
socket.on('session:end', (data) => {
const { sessionId } = data;
const session = sessionManager.getSession(sessionId);
if (!session) {
socket.emit('session:error', { error: 'Session not found' });
return;
}
// Получаем участников сессии
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Проверяем, что запрос идет от оператора этой сессии
if (!operator || operator.socket !== socket) {
socket.emit('session:error', { error: 'Unauthorized to end this session' });
return;
}
logger.info(`Session ended by operator: ${sessionId}`);
// Уведомляем устройство о завершении
if (device && device.isConnected()) {
device.socket.emit('camera:disconnect', { sessionId });
device.removeSession(sessionId);
}
// Уведомляем оператора о завершении
operator.socket.emit('session:ended', {
sessionId: sessionId,
deviceId: session.deviceId,
reason: 'Ended by operator'
});
operator.removeSession(sessionId);
// Закрываем сессию
sessionManager.closeSession(sessionId);
});
// WebRTC сигнализация
socket.on('webrtc:offer', (data) => {
const { sessionId, offer } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
// Определяем получателя (Android -> Operator или Operator -> Android)
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Если отправитель - устройство, то получатель - оператор
if (device && device.socket === socket && operator && operator.isConnected()) {
operator.socket.emit('webrtc:offer', { sessionId, offer });
session.updateWebRTCState('offer_sent');
}
// Если отправитель - оператор, то получатель - устройство
else if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('webrtc:offer', { sessionId, offer });
session.updateWebRTCState('offer_sent');
}
}
});
socket.on('webrtc:answer', (data) => {
const { sessionId, answer } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Если отправитель - устройство, то получатель - оператор
if (device && device.socket === socket && operator && operator.isConnected()) {
operator.socket.emit('webrtc:answer', { sessionId, answer });
session.updateWebRTCState('answer_sent');
}
// Если отправитель - оператор, то получатель - устройство
else if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('webrtc:answer', { sessionId, answer });
session.updateWebRTCState('answer_sent');
}
}
});
socket.on('webrtc:ice-candidate', (data) => {
const { sessionId, candidate } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Пересылаем ICE кандидата другой стороне
if (device && device.socket === socket && operator && operator.isConnected()) {
operator.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
} else if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('webrtc:ice-candidate', { sessionId, candidate });
}
}
});
// Переключение типа камеры
socket.on('camera:switch', (data) => {
const { sessionId, cameraType } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Проверяем права доступа
if (operator && operator.socket === socket && device && device.isConnected()) {
device.socket.emit('camera:switch', { sessionId, cameraType });
session.switchCamera(cameraType);
logger.info(`Camera switch requested: ${sessionId} -> ${cameraType}`);
}
}
});
// Завершение сессии
socket.on('camera:disconnect', (data) => {
const { sessionId } = data;
const session = sessionManager.getSession(sessionId);
if (session) {
const device = deviceManager.getDevice(session.deviceId);
const operator = deviceManager.getOperator(session.operatorId);
// Уведомляем участников
if (device && device.isConnected()) {
device.socket.emit('camera:disconnect', { sessionId });
device.removeSession(sessionId);
}
if (operator && operator.isConnected()) {
operator.socket.emit('camera:disconnected', { sessionId });
operator.socket.emit('session:ended', {
sessionId: sessionId,
deviceId: session.deviceId,
reason: 'Device disconnected'
});
operator.removeSession(sessionId);
}
sessionManager.closeSession(sessionId);
logger.info(`Camera session ended: ${sessionId}`);
}
});
// Ping-Pong для проверки соединения
socket.on('ping', (data, callback) => {
if (callback) {
callback({ timestamp: Date.now(), ...data });
}
});
// Новые события для ConnectionManager
// Завершение подключения от оператора
socket.on('connection:terminate', async (data) => {
const { connectionId } = data;
logger.info(`🔚 Connection termination requested: ${connectionId}`);
try {
await connectionManager.terminateConnection(connectionId);
socket.emit('connection:terminated', {
connectionId: connectionId,
timestamp: new Date().toISOString()
});
logger.info(`✅ Connection terminated: ${connectionId}`);
} catch (error) {
logger.error(`❌ Failed to terminate connection: ${error.message}`);
socket.emit('connection:error', { error: error.message });
}
});
// Запрос статистики подключений
socket.on('connection:status', (data, callback) => {
const stats = connectionManager.getConnectionStats();
if (callback) {
callback({
success: true,
stats: stats,
timestamp: new Date().toISOString()
});
} else {
socket.emit('connection:status_response', {
stats: stats,
timestamp: new Date().toISOString()
});
}
});
// Список активных подключений для оператора
socket.on('connection:list', (data, callback) => {
const operator = Array.from(deviceManager.operators.values())
.find(op => op.socket === socket);
if (!operator) {
const error = 'Operator not found';
if (callback) {
callback({ success: false, error });
} else {
socket.emit('connection:error', { error });
}
return;
}
const connections = connectionManager.getOperatorConnections(operator.operatorId);
if (callback) {
callback({
success: true,
connections: connections,
timestamp: new Date().toISOString()
});
} else {
socket.emit('connection:list_response', {
connections: connections,
timestamp: new Date().toISOString()
});
}
});
// Обработка отключения с ConnectionManager
socket.on('disconnect', (reason) => {
logger.info(`Client disconnected: ${socket.id}, reason: ${reason}`);
// Находим устройство или оператора по сокету
const device = Array.from(deviceManager.devices.values())
.find(d => d.socket === socket);
const operator = Array.from(deviceManager.operators.values())
.find(op => op.socket === socket);
if (device) {
// Очищаем подключения устройства через ConnectionManager
connectionManager.cleanupDeviceConnections(device.deviceId);
// Уведомляем операторов об отключении устройства
const operators = deviceManager.getConnectedOperators();
operators.forEach(op => {
op.socket.emit('device:disconnected', {
deviceId: device.deviceId
});
});
// Завершаем активные сессии устройства
sessionManager.closeDeviceSessions(device.deviceId);
deviceManager.disconnectDevice(device.deviceId);
}
if (operator) {
// Очищаем подключения оператора через ConnectionManager
connectionManager.cleanupOperatorConnections(operator.operatorId);
// Завершаем активные сессии оператора
sessionManager.closeOperatorSessions(operator.operatorId);
deviceManager.disconnectOperator(operator.operatorId);
}
});
});
// Периодическая очистка старых сессий и устройств
setInterval(() => {
try {

View File

@@ -6,7 +6,8 @@ class GodEyeOperator {
constructor() {
this.socket = null;
this.operatorId = uuidv4();
this.currentSession = null;
this.activeSessions = new Map(); // sessionId -> sessionData
this.currentActiveSession = null; // Currently viewing session
this.localConnection = null;
this.remoteStream = null;
this.mediaRecorder = null;
@@ -18,6 +19,7 @@ class GodEyeOperator {
this.isFullscreen = false;
this.isZoomed = false;
this.config = null;
this.isConnected = false;
// UI Elements
this.elements = {
@@ -152,6 +154,56 @@ class GodEyeOperator {
this.elements.connectBtn.addEventListener('click', () => this.connect());
this.elements.disconnectBtn.addEventListener('click', () => this.disconnect());
this.elements.clearLogs.addEventListener('click', () => this.clearLogs());
// Logs panel toggle
const logsToggle = document.getElementById('logs-toggle');
const logsContent = document.getElementById('logs-content');
const logsCollapseIcon = logsToggle?.querySelector('.collapse-icon');
const controlPanel = document.querySelector('.control-panel');
if (logsToggle) {
logsToggle.addEventListener('click', () => {
const isCollapsed = logsContent.classList.contains('collapsed');
if (isCollapsed) {
// Разворачиваем
logsContent.classList.remove('collapsed');
logsCollapseIcon.classList.remove('collapsed');
controlPanel.classList.remove('logs-collapsed');
logsCollapseIcon.textContent = '▲';
} else {
// Сворачиваем
logsContent.classList.add('collapsed');
logsCollapseIcon.classList.add('collapsed');
controlPanel.classList.add('logs-collapsed');
logsCollapseIcon.textContent = '▼';
}
});
}
// Connection panel toggle
const connectionToggle = document.getElementById('connection-toggle');
const connectionContent = document.getElementById('connection-content');
const connectionCollapseIcon = connectionToggle?.querySelector('.collapse-icon');
if (connectionToggle) {
connectionToggle.addEventListener('click', () => {
const isCollapsed = connectionContent.classList.contains('collapsed');
if (isCollapsed) {
// Разворачиваем
connectionContent.classList.remove('collapsed');
connectionCollapseIcon.classList.remove('collapsed');
connectionCollapseIcon.textContent = '▲';
} else {
// Сворачиваем
connectionContent.classList.add('collapsed');
connectionCollapseIcon.classList.add('collapsed');
connectionCollapseIcon.textContent = '▼';
}
});
}
// Refresh devices
const refreshBtn = document.getElementById('refresh-devices');
if (refreshBtn) {
@@ -288,6 +340,11 @@ class GodEyeOperator {
}
connect() {
if (this.isConnected) {
this.disconnect();
return;
}
const serverUrl = this.elements.serverUrl.value.trim();
if (!serverUrl) {
this.log('Введите URL сервера', 'error');
@@ -321,7 +378,7 @@ class GodEyeOperator {
this.log('Отключен от сервера', 'warning');
this.updateConnectionStatus(false);
this.clearDevicesList();
this.clearSessionsList();
this.updateSessionsList();
this.stopPingMonitoring();
});
@@ -381,6 +438,133 @@ class GodEyeOperator {
this.elements.pingIndicator.textContent = `Ping: ${ping}ms`;
this.elements.pingIndicator.style.color = ping < 100 ? '#4CAF50' : ping < 300 ? '#FF9800' : '#f44336';
});
// Session events
this.socket.on('session:created', (data) => {
this.log(`Сессия создана: ${data.sessionId}`, 'info');
this.activeSessions.set(data.sessionId, {
...data,
status: 'pending'
});
this.updateSessionsList();
});
// Новые события ConnectionManager
this.socket.on('connection:initiated', (data) => {
this.log(`Подключение инициировано: ${data.connectionId}`, 'info');
this.activeSessions.set(data.sessionId, {
connectionId: data.connectionId,
sessionId: data.sessionId,
deviceId: data.deviceId,
cameraType: data.cameraType,
status: 'pending',
createdAt: data.createdAt
});
this.updateSessionsList();
});
this.socket.on('connection:accepted', (data) => {
this.log(`Подключение принято: ${data.connectionId}`, 'success');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'active';
session.streamUrl = data.streamUrl;
if (!this.currentActiveSession) {
this.currentActiveSession = data.sessionId;
this.currentSession = {
id: data.sessionId,
deviceId: data.deviceId,
cameraType: data.cameraType
};
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
this.updateCameraButtons(session.cameraType);
this.initWebRTC();
}
this.updateSessionsList();
}
});
this.socket.on('connection:rejected', (data) => {
this.log(`Подключение отклонено: ${data.sessionId} - ${data.error}`, 'error');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'rejected';
session.error = data.error;
this.updateSessionsList();
}
});
this.socket.on('connection:terminated', (data) => {
this.log(`Подключение завершено: ${data.connectionId}`, 'info');
// Находим сессию по connectionId
for (const [sessionId, session] of this.activeSessions.entries()) {
if (session.connectionId === data.connectionId) {
this.activeSessions.delete(sessionId);
if (this.currentActiveSession === sessionId) {
this.currentActiveSession = null;
this.currentSession = null;
this.elements.sessionInfo.textContent = '';
this.closeWebRTC();
}
break;
}
}
this.updateSessionsList();
});
this.socket.on('connection:error', (data) => {
this.log(`Ошибка подключения: ${data.error}`, 'error');
});
// Сохраняем старые события для обратной совместимости
this.socket.on('session:created', (data) => {
this.log(`Сессия создана: ${data.sessionId}`, 'info');
this.activeSessions.set(data.sessionId, {
...data,
status: 'pending'
});
this.updateSessionsList();
});
this.socket.on('session:rejected', (data) => {
this.log(`Сессия отклонена: ${data.sessionId}`, 'error');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'rejected';
this.updateSessionsList();
}
});
this.socket.on('session:ended', (data) => {
this.log(`Сессия завершена: ${data.sessionId}`, 'info');
this.activeSessions.delete(data.sessionId);
if (this.currentActiveSession === data.sessionId) {
this.currentActiveSession = null;
this.elements.sessionInfo.textContent = '';
}
this.updateSessionsList();
});
// Дополнительный обработчик для совместимости
this.socket.on('camera:disconnected', (data) => {
this.log(`Камера отключена: ${data.sessionId}`, 'warning');
const session = this.activeSessions.get(data.sessionId);
if (session) {
session.status = 'ended';
this.updateSessionsList();
}
});
// Обработчик обновления списка устройств
this.socket.on('device:connected', (data) => {
this.log(`Новое устройство подключено: ${data.deviceId}`, 'info');
this.requestDevicesList(); // Обновляем список устройств
});
this.socket.on('device:disconnected', (data) => {
this.log(`Устройство отключено: ${data.deviceId}`, 'warning');
this.requestDevicesList(); // Обновляем список устройств
});
}
registerOperator() {
@@ -404,18 +588,46 @@ class GodEyeOperator {
}
updateConnectionStatus(connected) {
this.elements.connectBtn.disabled = connected;
this.elements.disconnectBtn.disabled = !connected;
this.isConnected = connected;
// Обновляем заголовок панели подключения
const connectionToggle = document.getElementById('connection-toggle');
const connectionTitle = connectionToggle?.querySelector('h3');
if (connected) {
this.elements.connectBtn.textContent = 'Отключиться';
this.elements.connectBtn.className = 'btn-danger';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = false;
this.elements.connectionIndicator.textContent = '● Подключен';
this.elements.connectionIndicator.className = 'status connected';
this.elements.connectionStatusText.textContent = 'Подключен к серверу';
// Обновляем заголовок с индикатором подключения
if (connectionTitle) {
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #4CAF50;">●</span>';
}
} else {
this.elements.connectBtn.textContent = 'Подключиться';
this.elements.connectBtn.className = 'btn-primary';
this.elements.connectBtn.disabled = false;
this.elements.disconnectBtn.disabled = true;
this.elements.connectionIndicator.textContent = '● Отключен';
this.elements.connectionIndicator.className = 'status disconnected';
this.elements.connectionStatusText.textContent = 'Не подключен';
this.elements.sessionInfo.textContent = '';
// Обновляем заголовок с индикатором отключения
if (connectionTitle) {
connectionTitle.innerHTML = '🔗 Подключение к серверу <span style="color: #f44336;">●</span>';
}
// Очищаем сессии при отключении
if (this.activeSessions) {
this.activeSessions.clear();
this.currentActiveSession = null;
this.updateSessionsList();
}
}
}
@@ -431,17 +643,31 @@ class GodEyeOperator {
devices.forEach(device => {
const deviceElement = document.createElement('div');
deviceElement.className = 'device-item';
// Проверяем, есть ли активные сессии с этим устройством
const activeSessions = Array.from(this.activeSessions.values())
.filter(session => session.deviceId === device.deviceId && session.status === 'active');
const hasActiveSessions = activeSessions.length > 0;
// Сокращенный ID для компактности
const shortId = device.deviceId.length > 12 ?
device.deviceId.substring(0, 8) + '...' : device.deviceId;
deviceElement.innerHTML = `
<div class="device-info">
<strong>ID:</strong> ${device.deviceId}<br>
<strong>Статус:</strong> ${device.isConnected ? 'Онлайн' : 'Офлайн'}
<strong>ID:</strong> ${shortId}<br>
<strong>Статус:</strong> ${device.isConnected ? '🟢 Онлайн' : '🔴 Офлайн'}
${hasActiveSessions ? `<br><strong>Сессии:</strong> ${activeSessions.length}` : ''}
</div>
<div class="device-capabilities">
Камеры: ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || ''))}
📷 ${(Array.isArray(device.capabilities) ? device.capabilities.join(', ') : (device.capabilities?.cameras?.join(', ') || 'back'))}
</div>
<div class="device-actions">
<button class="btn-device" onclick="operator.requestCamera('${device.deviceId}', 'back')">
Подключиться
<button class="btn-device ${hasActiveSessions ? 'btn-success' : 'btn-primary'}"
onclick="operator.requestCamera('${device.deviceId}', 'back')"
title="${hasActiveSessions ? 'Добавить новую сессию' : 'Подключиться к устройству'}">
${hasActiveSessions ? ' Добавить' : '🔗 Подключить'}
</button>
</div>
`;
@@ -454,8 +680,129 @@ class GodEyeOperator {
this.elements.devicesList.innerHTML = '<div class="no-devices">Нет подключенных устройств</div>';
}
clearSessionsList() {
this.elements.sessionsList.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
updateSessionsList() {
const container = this.elements.sessionsList;
if (this.activeSessions.size === 0) {
container.innerHTML = '<div class="no-sessions">Нет активных сессий</div>';
return;
}
container.innerHTML = '';
this.activeSessions.forEach((session, sessionId) => {
const sessionElement = document.createElement('div');
sessionElement.className = `session-item session-${session.status} ${this.currentActiveSession === sessionId ? 'active' : ''}`;
const statusText = {
'pending': '🟠 Ожидание',
'active': '🟢 Активна',
'rejected': '🔴 Отклонена',
'ended': '⚫ Завершена'
};
// Сокращенный ID устройства
const shortDeviceId = session.deviceId.length > 10 ?
session.deviceId.substring(0, 8) + '...' : session.deviceId;
sessionElement.innerHTML = `
<div class="session-header">
<strong>📱 ${shortDeviceId}</strong> | <strong>📷 ${session.cameraType}</strong><br>
<span class="status-${session.status}">${statusText[session.status] || session.status}</span>
</div>
<div class="session-actions">
${session.status === 'active' ? `
<button class="btn-small ${this.currentActiveSession === sessionId ? 'btn-success' : 'btn-primary'}"
onclick="operator.switchToSession('${sessionId}')"
title="${this.currentActiveSession === sessionId ? 'Активная сессия' : 'Переключиться на эту сессию'}">
${this.currentActiveSession === sessionId ? '✓ Активна' : '🔄 Переключить'}
</button>
<button class="btn-small btn-secondary" onclick="operator.switchCamera('${sessionId}', 'front')"
title="Переключить на фронтальную камеру">
📷
</button>
${session.connectionId ? `
<button class="btn-small btn-warning" onclick="operator.terminateConnection('${session.connectionId}', '${sessionId}')"
title="Завершить подключение">
🔌❌
</button>
` : `
<button class="btn-small btn-danger" onclick="operator.endSession('${sessionId}')"
title="Завершить сессию">
</button>
`}
` : `
<button class="btn-small btn-secondary" onclick="operator.endSession('${sessionId}')"
title="Удалить из списка">
🗑️
</button>
`}
</div>
`;
container.appendChild(sessionElement);
});
}
switchToSession(sessionId) {
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключиться на сессию ${sessionId}`, 'error');
return;
}
// Если это уже активная сессия, ничего не делаем
if (this.currentActiveSession === sessionId) {
this.log('Эта сессия уже активна', 'info');
return;
}
this.currentActiveSession = sessionId;
// Обновляем currentSession для обратной совместимости
this.currentSession = {
id: sessionId,
deviceId: session.deviceId,
cameraType: session.cameraType
};
this.log(`Переключение на сессию: ${sessionId} (устройство: ${session.deviceId})`, 'info');
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
// Обновляем кнопки камеры
this.updateCameraButtons(session.cameraType);
// Обновляем список сессий
this.updateSessionsList();
// TODO: Переключить видеопоток
// В будущем здесь будет логика переключения WebRTC потоков
}
switchCamera(sessionId, cameraType) {
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
return;
}
this.log(`Переключение камеры в сессии ${sessionId} на ${cameraType}`, 'info');
if (this.socket) {
this.socket.emit('camera:switch', {
sessionId: sessionId,
cameraType: cameraType
});
}
// Обновляем информацию о сессии
session.cameraType = cameraType;
this.updateSessionsList();
if (this.currentActiveSession === sessionId) {
this.elements.sessionInfo.textContent = `Активная сессия: ${session.deviceId} (${session.cameraType})`;
}
}
requestCamera(deviceId, cameraType = 'back') {
@@ -466,6 +813,7 @@ class GodEyeOperator {
this.log(`Запрос доступа к камере ${cameraType} устройства ${deviceId}`, 'info');
// Используем новое событие для подключения через ConnectionManager
this.socket.emit('camera:request', {
deviceId: deviceId,
operatorId: this.operatorId,
@@ -475,11 +823,27 @@ class GodEyeOperator {
handleCameraResponse(data) {
if (data.success) {
this.currentSession = data.session;
this.elements.sessionInfo.textContent = `Сессия: ${data.session.id}`;
this.log(`Доступ к камере получен. Сессия: ${data.session.id}`, 'success');
this.updateCameraButtons(data.session.cameraType);
this.initWebRTC();
// Обновляем сессию в коллекции
const sessionData = {
sessionId: data.sessionId || data.session.id,
deviceId: data.session.deviceId,
cameraType: data.session.cameraType,
status: 'active'
};
this.activeSessions.set(sessionData.sessionId, sessionData);
// Если нет текущей активной сессии, сделаем эту активной
if (!this.currentActiveSession) {
this.currentActiveSession = sessionData.sessionId;
this.currentSession = data.session; // Сохраняем для обратной совместимости
this.elements.sessionInfo.textContent = `Активная сессия: ${sessionData.deviceId} (${sessionData.cameraType})`;
this.updateCameraButtons(sessionData.cameraType);
this.initWebRTC();
}
this.log(`Доступ к камере получен. Сессия: ${sessionData.sessionId}`, 'success');
this.updateSessionsList();
} else {
this.log(`Отказ в доступе к камере: ${data.message}`, 'error');
}
@@ -494,20 +858,106 @@ class GodEyeOperator {
});
}
switchCamera(cameraType) {
if (!this.currentSession) {
switchCamera(sessionIdOrType, cameraType) {
// Определяем, передан sessionId или это старый вызов
let sessionId, targetCameraType;
if (cameraType) {
// Новый вызов: switchCamera(sessionId, cameraType)
sessionId = sessionIdOrType;
targetCameraType = cameraType;
} else {
// Старый вызов: switchCamera(cameraType)
targetCameraType = sessionIdOrType;
sessionId = this.currentActiveSession;
if (!sessionId && this.currentSession) {
// Обратная совместимость со старым кодом
sessionId = this.currentSession.id;
}
}
if (!sessionId) {
this.log('Нет активной сессии для переключения камеры', 'warning');
return;
}
this.log(`Переключение на камеру: ${cameraType}`, 'info');
const session = this.activeSessions.get(sessionId);
if (!session || session.status !== 'active') {
this.log(`Нельзя переключить камеру в сессии ${sessionId}`, 'error');
return;
}
this.log(`Переключение на камеру: ${targetCameraType} в сессии ${sessionId}`, 'info');
this.socket.emit('camera:switch', {
sessionId: this.currentSession.id,
cameraType: cameraType
sessionId: sessionId,
cameraType: targetCameraType
});
this.updateCameraButtons(cameraType);
// Обновляем состояние кнопок только для активной сессии
if (sessionId === this.currentActiveSession) {
this.updateCameraButtons(targetCameraType);
}
}
// Новые методы для управления подключениями через ConnectionManager
terminateConnection(connectionId, sessionId) {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.log(`Завершение подключения: ${connectionId}`, 'info');
this.socket.emit('connection:terminate', {
connectionId: connectionId
});
// Локально обновляем состояние сессии
const session = this.activeSessions.get(sessionId);
if (session) {
session.status = 'terminating';
this.updateSessionsList();
}
}
getConnectionStatus() {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.socket.emit('connection:status', {}, (response) => {
if (response.success) {
this.log('Статистика подключений получена', 'info');
console.log('Connection Stats:', response.stats);
} else {
this.log(`Ошибка получения статистики: ${response.error}`, 'error');
}
});
}
listMyConnections() {
if (!this.socket || !this.socket.connected) {
this.log('Нет подключения к серверу', 'error');
return;
}
this.socket.emit('connection:list', {}, (response) => {
if (response.success) {
this.log(`Получен список подключений: ${response.connections.length}`, 'info');
console.log('My Connections:', response.connections);
// Можно обновить UI с информацией о подключениях
response.connections.forEach(conn => {
console.log(`Connection ${conn.connectionId}: ${conn.deviceId} -> ${conn.status}`);
});
} else {
this.log(`Ошибка получения списка подключений: ${response.error}`, 'error');
}
});
}
async initWebRTC() {

View File

@@ -100,52 +100,61 @@
<!-- Right Panel - Devices & Sessions -->
<div class="control-panel">
<!-- Connection Settings -->
<div class="connection-panel">
<h3>Подключение к серверу</h3>
<div class="input-group">
<label for="server-url">URL сервера:</label>
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
<div class="connection-panel panel-section collapsible">
<div class="panel-header clickable" id="connection-toggle">
<h3>🔌 Подключение к серверу</h3>
<span class="collapse-icon collapsed"></span>
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
</label>
</div>
<div class="button-group">
<button id="connect-btn" class="btn-primary">Подключиться</button>
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
</div>
<div id="connection-info" class="connection-info">
<span id="connection-status-text">Не подключен</span>
<span id="ping-indicator">Ping: --</span>
<div class="panel-content collapsed" id="connection-content">
<div class="input-group">
<label for="server-url">URL сервера:</label>
<input type="text" id="server-url" value="http://localhost:3001" placeholder="http://localhost:3001">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="auto-connect"> Автоматически подключаться при запуске
</label>
</div>
<div class="button-group">
<button id="connect-btn" class="btn-primary">Подключиться</button>
<button id="disconnect-btn" class="btn-secondary" disabled>Отключиться</button>
</div>
<div id="connection-info" class="connection-info">
<span id="connection-status-text">Не подключен</span>
<span id="ping-indicator">Ping: --</span>
</div>
</div>
</div>
<!-- Available Devices -->
<div class="devices-panel">
<h3>Доступные устройства</h3>
<div class="devices-header" style="display: flex; justify-content: space-between; align-items: center;">
<span></span>
<button id="refresh-devices" class="btn-secondary btn-small" title="Обновить список устройств">🔄 Обновить</button>
<div class="devices-panel panel-section">
<div class="panel-header">
<h3>📱 Доступные устройства</h3>
<button id="refresh-devices" class="btn-icon" title="Обновить список">🔄</button>
</div>
<div id="devices-list" class="devices-list">
<div id="devices-list" class="devices-list compact-list">
<div class="no-devices">Нет подключенных устройств</div>
</div>
</div>
<!-- Active Sessions -->
<div class="sessions-panel">
<h3>Активные сессии</h3>
<div id="sessions-list" class="sessions-list">
<div class="sessions-panel panel-section">
<h3>🔗 Активные сессии</h3>
<div id="sessions-list" class="sessions-list compact-list">
<div class="no-sessions">Нет активных сессий</div>
</div>
</div>
<!-- Logs -->
<div class="logs-panel">
<h3>Журнал событий</h3>
<div id="logs-container" class="logs-container" style="max-height: 180px; overflow-y: auto;"></div>
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
<div class="logs-panel panel-section collapsible">
<div class="panel-header clickable" id="logs-toggle">
<h3>📋 Журнал событий</h3>
<span class="collapse-icon"></span>
</div>
<div class="panel-content" id="logs-content">
<div id="logs-container" class="logs-container"></div>
<button id="clear-logs" class="btn-secondary btn-small">Очистить</button>
</div>
</div>
</div>
</div>

View File

@@ -306,19 +306,91 @@ body {
background: #252525;
display: flex;
flex-direction: column;
max-width: 350px;
max-width: 380px;
min-width: 300px;
transition: max-width 0.3s ease;
}
.control-panel.logs-collapsed {
max-width: 320px;
}
.control-panel > div {
padding: 15px;
border-bottom: 1px solid #333;
}
/* Panel Sections */
.panel-section {
padding: 12px;
transition: all 0.3s ease;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.panel-header.clickable {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.panel-header.clickable:hover {
background: rgba(255, 255, 255, 0.05);
margin: -5px;
padding: 5px;
border-radius: 4px;
}
.collapse-icon {
font-size: 12px;
transition: transform 0.3s ease;
color: #888;
}
.collapse-icon.collapsed {
transform: rotate(180deg);
}
.panel-content {
transition: all 0.3s ease;
overflow: hidden;
}
.panel-content.collapsed {
max-height: 0;
padding: 0;
opacity: 0;
}
/* Connection Panel */
.connection-panel {
transition: all 0.3s ease;
}
.connection-panel.collapsed {
flex: none;
}
.connection-panel h3 {
margin-bottom: 15px;
margin-bottom: 8px;
color: #4CAF50;
font-size: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
justify-content: space-between;
width: 100%;
}
.connection-panel h3 span {
font-size: 10px;
margin-left: auto;
}
.input-group {
@@ -385,32 +457,53 @@ button:disabled {
cursor: not-allowed;
}
/* Devices Panel */
.devices-panel h3,
.sessions-panel h3 {
margin-bottom: 15px;
color: #2196F3;
font-size: 14px;
/* Devices and Sessions Panels */
.devices-panel,
.sessions-panel {
flex: 1;
min-height: 0;
}
.devices-list,
.sessions-list {
max-height: 200px;
.devices-panel h3,
.sessions-panel h3 {
margin-bottom: 8px;
color: #2196F3;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.compact-list {
max-height: 180px;
overflow-y: auto;
transition: max-height 0.3s ease;
}
.control-panel.logs-collapsed .compact-list {
max-height: 250px;
}
.device-item,
.session-item {
background: #3a3a3a;
margin-bottom: 8px;
padding: 10px;
margin-bottom: 6px;
padding: 8px;
border-radius: 4px;
border-left: 3px solid #4CAF50;
transition: all 0.2s ease;
}
.device-item:hover,
.session-item:hover {
background: #404040;
transform: translateY(-1px);
}
.device-info {
font-size: 12px;
margin-bottom: 5px;
font-size: 11px;
margin-bottom: 4px;
line-height: 1.3;
}
.device-info strong {
@@ -418,28 +511,56 @@ button:disabled {
}
.device-capabilities {
font-size: 11px;
font-size: 10px;
color: #888;
margin-bottom: 8px;
margin-bottom: 6px;
line-height: 1.2;
}
.device-actions {
display: flex;
gap: 5px;
gap: 4px;
flex-wrap: wrap;
}
.btn-device {
background: #2196F3;
border: none;
color: white;
padding: 4px 8px;
padding: 3px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
font-size: 9px;
transition: all 0.2s ease;
}
.btn-device:hover {
background: #1976D2;
transform: scale(1.05);
}
.btn-device.btn-success {
background: #4CAF50;
}
.btn-device.btn-success:hover {
background: #45a049;
}
.btn-icon {
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 14px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
}
.btn-icon:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.no-devices,
@@ -450,29 +571,119 @@ button:disabled {
padding: 20px;
}
/* Session styles */
.session-item {
border-left: 3px solid #2196F3;
}
.session-item.session-pending {
border-left-color: #FF9800;
}
.session-item.session-active {
border-left-color: #4CAF50;
}
.session-item.session-rejected {
border-left-color: #f44336;
}
.session-item.session-ended {
border-left-color: #666;
opacity: 0.7;
}
.session-item.active {
background: #4a4a4a;
border: 2px solid #4CAF50;
border-left: 3px solid #4CAF50;
}
.session-header {
font-size: 12px;
margin-bottom: 8px;
}
.session-header strong {
color: #4CAF50;
}
.status-pending { color: #FF9800; }
.status-active { color: #4CAF50; }
.status-rejected { color: #f44336; }
.status-ended { color: #666; }
.session-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.btn-small {
padding: 2px 6px;
font-size: 10px;
border: none;
border-radius: 2px;
cursor: pointer;
color: white;
}
.btn-small.btn-primary {
background: #2196F3;
}
.btn-small.btn-success {
background: #4CAF50;
}
.btn-small.btn-secondary {
background: #757575;
}
.btn-small.btn-danger {
background: #f44336;
}
.btn-small:hover {
opacity: 0.8;
}
/* Logs Panel */
.logs-panel {
flex: 1;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.logs-panel.collapsed {
flex: none;
}
.logs-panel h3 {
margin-bottom: 15px;
margin-bottom: 8px;
color: #FF9800;
font-size: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
.logs-container {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 4px;
padding: 8px;
padding: 6px;
font-family: 'Courier New', monospace;
font-size: 11px;
font-size: 10px;
overflow-y: auto;
margin-bottom: 10px;
margin-bottom: 8px;
max-height: 120px;
transition: max-height 0.3s ease;
}
.control-panel.logs-collapsed .logs-container {
max-height: 0;
padding: 0;
margin: 0;
border: none;
}
.log-entry {

View File

@@ -0,0 +1,69 @@
# 🔧 Критические исправления - ОБЯЗАТЕЛЬНО К ВЫПОЛНЕНИЮ
## ⚠️ ВАЖНО! Проблема с кэшированием браузера
**Проблема**: В логах сервера все еще видно `register:android` вместо `register:mobile_web`, хотя код исправлен.
**Причина**: Браузер кэширует старую версию mobile.html
## 🚀 РЕШЕНИЕ - Принудительное обновление
### На телефоне (мобильный браузер):
1. **⚠️ ЗАКРОЙТЕ ВСЕ ВКЛАДКИ** с мобильной версией
2. **🔄 ОЧИСТИТЕ КЭШ БРАУЗЕРА**:
- Chrome: Settings → Privacy → Clear browsing data → Cached images and files
- Safari: Settings → Safari → Clear History and Website Data
3. **📱 ПОЛНОСТЬЮ ПЕРЕЗАПУСТИТЕ БРАУЗЕР** (закройте и откройте приложение)
4. **🌐 ОТКРОЙТЕ НОВУЮ ВКЛАДКУ** и перейдите: `http://192.168.219.108:3001/mobile`
### В Desktop Operator:
1. **🔄 ПЕРЕЗАПУСТИТЕ ПРИЛОЖЕНИЕ** полностью
2. **🔌 ПЕРЕПОДКЛЮЧИТЕСЬ** к серверу
## ✅ Что должно быть в логах после исправления:
### Правильные логи сервера:
```
📱 Mobile web client connected: [ID]
🌐 Mobile web event: register:mobile_web ← ДОЛЖНО БЫТЬ ТАК!
Mobile web client registered: [device-id]
```
### НЕ должно быть:
```
🌐 Mobile web event: register:android ← НЕПРАВИЛЬНО!
```
## 🧪 Проверка исправлений
После очистки кэша и переподключения:
1. **Мобильная регистрация**: В логах должно быть `register:mobile_web`
2. **Переключение камеры**: Должны видеть в логах мобильного браузера:
```
🔄 Переключение камеры на: [тип]
🔄 Обновление WebRTC потоков...
✅ Видео трек обновлен для сессии: [ID]
```
3. **Ошибка "undefined"**: Должна исчезнуть
4. **WebRTC поток**: Должен обновляться при переключении камеры
## 🔧 Технические исправления (выполнены):
- ✅ Исправлено событие регистрации: `register:android` → `register:mobile_web`
- ✅ Добавлена функция `updateWebRTCStreams()` для обновления видео треков
- ✅ Исправлена `switchCamera()` с поддержкой параметров
- ✅ Улучшена `handleCameraSwitchRequest()` для правильной обработки
## 🎯 Ожидаемый результат:
После выполнения всех шагов:
-**Исчезнет**: Ошибка "undefined" при переключении камеры
-**Появится**: Корректное переключение между front/back камерами
-**Работает**: WebRTC поток обновляется в real-time
-**Видно в операторе**: Смена видео при переключении камеры
---
**🚨 КРИТИЧЕСКИ ВАЖНО**: Обязательно очистите кэш браузера на телефоне, иначе исправления не будут работать!

View File

@@ -0,0 +1,162 @@
# Инструкция по тестированию полностью обновленного интерфейса
## Новые возможности интерфейса v2.0:
### 🔧 1. Сворачиваемые панели
- **Панель подключения**: по умолчанию свернута, статус отображается в заголовке
- **Панель логов**: сворачивается для экономии места
- **Адаптивные размеры**: панели автоматически подстраиваются под доступное пространство
### 📱 2. Компактное отображение
- **Устройства**: сокращенные ID, эмодзи-статусы, иконочные кнопки
- **Сессии**: формат "📱 device | 📷 camera", цветовая кодировка статусов
- **Максимум информации** в минимальном пространстве
### 🎨 3. Современный дизайн
- **Эмодзи-иконки** для быстрого визуального восприятия
- **Плавные анимации** переходов и hover-эффектов
- **Цветовые индикаторы** для статусов и состояний
## Последовательность тестирования:
### Шаг 1: Первый запуск
1. Запустите десктопное приложение
2. **Проверьте начальное состояние**:
- Панель подключения свернута с красным индикатором ● (отключено)
- Панель логов развернута
- Панели устройств и сессий пустые
### Шаг 2: Управление панелями
1. **Тест сворачивания подключения**:
- Кликните на "🔗 Подключение к серверу ●"
- Панель должна плавно развернуться
- Кликните снова - должна свернуться
2. **Тест сворачивания логов**:
- Кликните на "📋 Журнал событий ▲"
- Логи должны скрыться, иконка измениться на ▼
- Панели устройств/сессий должны увеличиться
### Шаг 3: Подключение к серверу
1. Разверните панель подключения
2. Нажмите "Подключиться"
3. **Проверьте изменения**:
- Кнопка изменилась на "Отключиться"
- Индикатор в заголовке стал зеленым ●
- В логах появилось сообщение о подключении
### Шаг 4: Работа с устройствами
1. В веб-демо (http://localhost:3001) симулируйте Android устройство
2. **Проверьте компактное отображение**:
- ID устройства сокращен (если длинный)
- Статус показан как 🟢 Онлайн
- Кнопка показывает "🔗 Подключить"
- Иконка 📷 перед типами камер
### Шаг 5: Создание сессий
1. Нажмите "🔗 Подключить" на устройстве
2. В веб-демо примите запрос
3. **Проверьте отображение сессии**:
- Формат: "📱 device123... | 📷 back"
- Статус: 🟢 Активна
- Кнопки: ✓ Активна, 📷 Камера, ❌ Завершить
### Шаг 6: Множественные сессии
1. Создайте еще 2-3 сессии
2. **Проверьте адаптивность**:
- Все сессии помещаются в компактном списке
- Прокрутка работает при большом количестве
- Кнопка устройства изменилась на " Добавить"
### Шаг 7: Оптимизация пространства
1. Сверните панель логов
2. **Проверьте увеличение списков**:
- Панели устройств/сессий стали выше
- Больше элементов видно без прокрутки
- Переходы плавные
## Визуальные индикаторы:
### Панель подключения (свернута):
```
🔗 Подключение к серверу ● ▼
```
-**Красный** = отключено
-**Зеленый** = подключено
### Устройства (компактно):
```
📱 Устройства 🔄
┌─────────────────┐
│ ID: device12... │
│ Статус: 🟢 Онлайн│
│ Сессии: 2 │
│ 📷 back, front │
│ [ Добавить] │
└─────────────────┘
```
### Сессии (компактно):
```
🔗 Активные сессии
┌─────────────────┐
│ 📱 dev123... | 📷 back │
│ 🟢 Активна │
│ [✓][📷][❌] │
└─────────────────┘
```
### Логи (сворачиваемые):
```
📋 Журнал событий ▲
┌─────────────────┐
│ [12:34] INFO... │
│ [12:35] SUCCESS │
└─────────────────┘
При сворачивании:
📋 Журнал событий ▼
```
## Преимущества нового интерфейса:
### 🎯 Эффективность пространства:
- **На 40% больше информации** в том же окне
- **Гибкое управление** видимостью панелей
- **Адаптивные размеры** под количество элементов
### 👀 Улучшенная читаемость:
- **Эмодзи-иконки** для мгновенного распознавания
- **Цветовая кодировка** статусов
- **Компактные форматы** без потери информации
### ⚡ Быстрая навигация:
- **Меньше кликов** для основных действий
- **Интуитивные иконки** вместо текста
- **Плавные анимации** для комфорта
### 📊 Масштабируемость:
- **Поддержка множественных устройств** без переполнения интерфейса
- **Эффективная прокрутка** в компактных списках
- **Автоматическая адаптация** под контент
## Тестовые сценарии:
### Сценарий 1: Минималистичный режим
1. Сверните панель подключения
2. Сверните панель логов
3. **Результат**: максимум места для устройств и сессий
### Сценарий 2: Полная видимость
1. Разверните все панели
2. **Результат**: полный контроль и мониторинг всех процессов
### Сценарий 3: Рабочий режим
1. Подключитесь к серверу
2. Сверните панель подключения (статус виден в заголовке)
3. Оставьте логи развернутыми для мониторинга
4. **Результат**: оптимальный баланс между контролем и пространством
Новый интерфейс готов для эффективной работы с любым количеством устройств и сессий! 🚀

316
docs/CONNECTION_MANAGER.md Normal file
View File

@@ -0,0 +1,316 @@
# ConnectionManager API Документация
## Обзор
`ConnectionManager` - это центральный компонент системы GodEye Signal Center, который управляет полным жизненным циклом подключений между операторами и Android устройствами. Он обеспечивает надежное создание, управление и завершение подключений с таймаутами и валидацией.
## Архитектура
```
Desktop Operator ←→ ConnectionManager ←→ Android Device
SessionManager
DeviceManager
```
### Основные компоненты:
- **ConnectionManager**: Управление подключениями и их жизненным циклом
- **DeviceManager**: Управление устройствами и операторами
- **SessionManager**: Управление WebRTC сессиями
- **Socket.IO**: Транспорт для событий подключения
## События WebSocket
### Новые события ConnectionManager
#### `connection:initiated`
Подключение инициировано ConnectionManager
```javascript
{
connectionId: "conn_uuid",
sessionId: "session_uuid",
deviceId: "device_id",
cameraType: "back",
status: "pending",
createdAt: "2023-..."
}
```
#### `connection:accepted`
Устройство приняло запрос подключения
```javascript
{
connectionId: "conn_uuid",
sessionId: "session_uuid",
deviceId: "device_id",
cameraType: "back",
streamUrl: "webrtc_stream_url",
status: "active"
}
```
#### `connection:rejected`
Устройство отклонило запрос подключения
```javascript
{
sessionId: "session_uuid",
deviceId: "device_id",
cameraType: "back",
error: "Device camera is busy"
}
```
#### `connection:terminated`
Подключение завершено
```javascript
{
connectionId: "conn_uuid",
timestamp: "2023-..."
}
```
#### `connection:error`
Ошибка подключения
```javascript
{
error: "Error message"
}
```
### Команды от клиента
#### `connection:terminate`
Завершить подключение
```javascript
{
connectionId: "conn_uuid"
}
```
#### `connection:status`
Запросить статистику подключений
```javascript
{} // Ответ в callback или connection:status_response
```
#### `connection:list`
Список подключений оператора
```javascript
{} // Ответ в callback или connection:list_response
```
## Desktop Operator API
### Новые методы
#### `terminateConnection(connectionId, sessionId)`
Завершает подключение через ConnectionManager
```javascript
operator.terminateConnection('conn_123', 'session_456');
```
#### `getConnectionStatus()`
Получает статистику всех подключений системы
```javascript
operator.getConnectionStatus();
```
#### `listMyConnections()`
Получает список подключений текущего оператора
```javascript
operator.listMyConnections();
```
### Обновленные методы
#### `requestCamera(deviceId, cameraType)`
Теперь использует ConnectionManager для создания подключений
```javascript
operator.requestCamera('device_123', 'back');
```
## Backend ConnectionManager API
### Основные методы
#### `async initiateConnection(operatorId, deviceId, cameraType)`
Инициирует новое подключение между оператором и устройством
```javascript
const connection = await connectionManager.initiateConnection(
'operator_123',
'device_456',
'back'
);
```
#### `async acceptConnection(sessionId, metadata)`
Принимает подключение со стороны устройства
```javascript
const connection = await connectionManager.acceptConnection(
'session_123',
{ streamUrl: 'webrtc://...' }
);
```
#### `async rejectConnection(sessionId, reason)`
Отклоняет подключение
```javascript
await connectionManager.rejectConnection(
'session_123',
'Camera is busy'
);
```
#### `async terminateConnection(connectionId)`
Завершает активное подключение
```javascript
await connectionManager.terminateConnection('conn_123');
```
### Утилиты
#### `getConnection(sessionId)`
Получает подключение по ID сессии
```javascript
const connection = connectionManager.getConnection('session_123');
```
#### `getConnectionStats()`
Получает статистику всех подключений
```javascript
const stats = connectionManager.getConnectionStats();
// { total: 5, active: 2, pending: 1, completed: 2 }
```
#### `getOperatorConnections(operatorId)`
Получает все подключения оператора
```javascript
const connections = connectionManager.getOperatorConnections('operator_123');
```
#### `cleanup*()`
Методы очистки подключений при отключении участников
```javascript
connectionManager.cleanupDeviceConnections('device_123');
connectionManager.cleanupOperatorConnections('operator_456');
```
## Жизненный цикл подключения
### 1. Инициирование подключения
```
Desktop Operator → camera:request → ConnectionManager.initiateConnection()
Создание Connection объекта
Отправка camera:request → Android Device
Desktop Operator ← connection:initiated ← ConnectionManager
```
### 2. Ответ устройства
```
Android Device → camera:response → ConnectionManager.acceptConnection()
Обновление Connection статуса
Desktop Operator ← connection:accepted ← ConnectionManager
Инициирование WebRTC
```
### 3. Завершение подключения
```
Desktop Operator → connection:terminate → ConnectionManager.terminateConnection()
Очистка ресурсов
connection:terminated ← ConnectionManager
```
## Конфигурация таймаутов
```javascript
// В ConnectionManager.js
CONNECTION_TIMEOUT: 30000, // 30 секунд на установку подключения
CLEANUP_INTERVAL: 60000 // Очистка каждую минуту
```
## Обратная совместимость
Система поддерживает старые события для совместимости:
- `session:created``connection:initiated`
- `session:accepted``connection:accepted`
- `session:rejected``connection:rejected`
- `camera:response` (продолжает работать)
## Логирование
ConnectionManager использует Winston logger для отслеживания:
- ✅ Успешные подключения
- ❌ Ошибки и отклонения
- 🔄 Переходы состояний
- ⏰ Таймауты подключений
- 🧹 Операции очистки
## Примеры использования
### Полный цикл подключения от Desktop Operator
```javascript
// 1. Инициирование подключения
operator.requestCamera('device_123', 'back');
// 2. Обработка ответа
operator.socket.on('connection:accepted', (data) => {
console.log('Подключение установлено:', data.connectionId);
// Запускаем WebRTC...
});
// 3. Завершение подключения
operator.terminateConnection(connectionId, sessionId);
```
### Мониторинг подключений
```javascript
// Получение статистики
operator.getConnectionStatus();
// Список моих подключений
operator.listMyConnections();
// Обработка ошибок
operator.socket.on('connection:error', (data) => {
console.error('Ошибка подключения:', data.error);
});
```
## Миграция с старой системы
Старые методы продолжают работать, но рекомендуется переход на новые:
### Было:
```javascript
operator.socket.on('session:created', ...);
operator.socket.on('session:accepted', ...);
```
### Стало:
```javascript
operator.socket.on('connection:initiated', ...);
operator.socket.on('connection:accepted', ...);
```
### UI обновления
- Кнопки завершения сессий теперь используют `terminateConnection()`
- Статус подключений отображается с `connectionId`
- Добавлены индикаторы состояния подключений
---
Для дополнительной информации см. исходный код:
- `/backend/src/managers/ConnectionManager.js`
- `/backend/src/server.js` (обработчики событий)
- `/desktop-operator/src/renderer/app.js` (клиентские методы)

View File

@@ -0,0 +1,72 @@
# Руководство по тестированию десктопного приложения GodEye
## Новые функции в десктопном приложении
### 1. Изменение кнопки подключения
- **До подключения**: кнопка показывает "Подключиться" (синий цвет)
- **После подключения**: кнопка показывает "Отключиться" (красный цвет)
- **Поведение**: кнопка переключается между подключением и отключением
### 2. Список активных сессий
- **Расположение**: правая панель, секция "Активные сессии"
- **Отображение**: показывает все сессии текущего оператора
- **Информация для каждой сессии**:
- ID устройства
- Тип камеры (back, front, wide, telephoto)
- Статус сессии (ожидание, активна, отклонена, завершена)
- Цветная индикация статуса
### 3. Управление множественными сессиями
- **Переключение между сессиями**: кнопка "Переключиться"
- **Переключение камеры**: кнопка "Фронтальная" для смены типа камеры
- **Завершение сессии**: кнопка "Завершить" для закрытия сессии
- **Активная сессия**: выделена зеленой рамкой
## Как протестировать
### Тестирование с веб-демо
1. Откройте веб-демо: http://localhost:3001
2. В разделе "Test Android Device" нажмите "Simulate Android Connection"
3. В десктопном приложении:
- Подключитесь к серверу (кнопка должна измениться на "Отключиться")
- В списке устройств должно появиться тестовое устройство
- Нажмите "Подключиться" к устройству
- В веб-демо примите запрос кнопкой "Accept Request"
- В десктопном приложении должна появиться активная сессия в списке сессий
### Тестирование множественных сессий
1. Создайте несколько сессий, повторяя процесс подключения
2. В списке сессий попробуйте:
- Переключиться между активными сессиями
- Переключить камеру в сессии
- Завершить одну из сессий
### Состояния сессий
- 🟠 **Ожидание** (pending): запрос отправлен, ожидается ответ от устройства
- 🟢 **Активна** (active): соединение установлено, видео передается
- 🔴 **Отклонена** (rejected): устройство отклонило запрос
-**Завершена** (ended): сессия была закрыта
## Улучшения пользовательского интерфейса
### Кнопки устройств
- Если с устройством есть активные сессии: показывает "Добавить сессию" (зеленая кнопка)
- Если нет активных сессий: показывает "Подключиться" (синяя кнопка)
### Индикаторы состояния
- Количество активных сессий отображается для каждого устройства
- Цветовая кодировка для быстрого визуального определения статуса
- Активная сессия выделена в списке
## События в журнале
Все действия фиксируются в журнале событий:
- Создание и принятие сессий
- Переключение между сессиями
- Переключение камер
- Завершение сессий
## Клавиатурные сокращения
- **Ctrl+S**: снимок экрана
- **Ctrl+F**: полноэкранный режим
- **Ctrl+R**: начать/остановить запись
- **ESC**: выйти из полноэкранного режима

View File

@@ -0,0 +1,112 @@
# Инструкция по тестированию исправленной системы сессий
## Проблемы, которые были исправлены:
### ✅ 1. Оператор не получал уведомления о новых запросах
**Исправление**: Добавлены правильные события `session:created`, `session:accepted`, `session:rejected`, `session:ended`
### ✅ 2. Сессии не отображались в приложении при подтверждении
**Исправление**: Обновлена обработка событий сессий в десктопном приложении
### ✅ 3. Правильная сущность сессий для переключения
**Исправление**: Полная реализация системы множественных сессий с переключением
## Последовательность тестирования:
### Шаг 1: Подключение к серверу
1. Запустите десктопное приложение
2. **НОВОЕ**: Панель подключения по умолчанию свернута с индикатором статуса 🔗 ● в заголовке
3. Кликните на заголовок "🔗 Подключение к серверу" чтобы развернуть панель
4. Нажмите "Подключиться" - кнопка должна изменить текст на "Отключиться"
5. **НОВОЕ**: Индикатор в заголовке изменится на зеленый ● для показа активного подключения
6. В журнале должно появиться сообщение о подключении
### Шаг 2: Симуляция Android устройства
1. В веб-демо (http://localhost:3001) найдите секцию "Test Android Device"
2. Нажмите "Simulate Android Connection"
3. В десктопном приложении должно появиться новое устройство в списке
### Шаг 3: Создание первой сессии
1. В десктопном приложении нажмите "Подключиться" к устройству
2. **НОВОЕ**: В списке сессий должна немедленно появиться сессия со статусом "Ожидание" (🟠)
3. В веб-демо появится запрос "Camera request from operator"
4. Нажмите "Accept Request" в веб-демо
5. **НОВОЕ**: Сессия в приложении должна изменить статус на "Активна" (🟢)
### Шаг 4: Создание множественных сессий
1. Повторите шаг 3 еще 2-3 раза для создания нескольких сессий
2. **НОВОЕ**: Кнопка устройства должна изменить текст на "Добавить сессию"
3. В списке сессий должны отображаться все активные сессии
### Шаг 5: Переключение между сессиями
1. Кликните "Переключиться" на любой неактивной сессии
2. **НОВОЕ**: Активная сессия должна выделиться зеленой рамкой
3. В верхней части должна отобразиться информация об активной сессии
### Шаг 6: Переключение камеры
1. В активной сессии нажмите "Фронтальная"
2. **НОВОЕ**: Тип камеры должен обновиться в информации о сессии
3. В журнале должно появиться сообщение о переключении
### Шаг 7: Завершение сессии
1. Нажмите "Завершить" на любой сессии
2. **НОВОЕ**: Сессия должна исчезнуть из списка
3. Если это была активная сессия, должна автоматически активироваться другая
## Новые события в журнале:
### События сессий:
- `Сессия создана: [sessionId]` - при запросе доступа к камере
- `Сессия принята: [sessionId]` - при подтверждении на устройстве
- `Сессия отклонена: [sessionId]` - при отказе на устройстве
- `Сессия завершена: [sessionId]` - при закрытии сессии
### События устройств:
- `Новое устройство подключено: [deviceId]` - при подключении Android
- `Устройство отключено: [deviceId]` - при отключении Android
### События управления:
- `Переключение на сессию: [sessionId]` - при смене активной сессии
- `Переключение на камеру: [type] в сессии [sessionId]` - при смене камеры
- `Завершение сессии: [sessionId]` - при инициировании закрытия
## Визуальные индикаторы:
### Статусы сессий:
- 🟠 **Ожидание** - запрос отправлен, ждем ответа от устройства
- 🟢 **Активна** - сессия установлена и работает
- 🔴 **Отклонена** - устройство отклонило запрос
-**Завершена** - сессия была закрыта
### Кнопки устройств:
- **"Подключиться"** (синяя) - нет активных сессий с устройством
- **"Добавить сессию"** (зеленая) - есть активные сессии, можно добавить еще
### Активная сессия:
- Выделена **зеленой рамкой** в списке
- Отображается в верхней части: `Активная сессия: [device] ([camera])`
- Кнопка показывает **"Активна"** вместо "Переключиться"
## Проверка исправлений:
### ✅ Проблема 1: Получение запросов
- Сессия должна появляться в списке **сразу** при нажатии "Подключиться"
- Статус должен меняться с "Ожидание" на "Активна" при подтверждении
### ✅ Проблема 2: Отображение сессий
- После "Accept Request" в веб-демо, сессия **немедленно** становится активной
- Все данные сессии корректно отображаются в списке
### ✅ Проблема 3: Переключение сессий
- Можно создать несколько активных сессий одновременно
- Переключение между сессиями работает мгновенно
- Управление камерами работает для каждой сессии независимо
## Ожидаемое поведение:
1. **Мгновенная отзывчивость** - все действия отражаются в UI немедленно
2. **Множественные сессии** - можно управлять несколькими устройствами одновременно
3. **Четкая индикация** - всегда ясно, какая сессия активна и каков ее статус
4. **Правильная синхронизация** - состояние между сервером и клиентом всегда синхронизировано
Теперь система сессий работает полноценно и позволяет эффективно управлять множественными подключениями!

View File

@@ -0,0 +1,116 @@
# Инструкция по тестированию улучшенного интерфейса
## Новые возможности интерфейса:
### ✅ 1. Компактное отображение устройств
- **Сокращенные ID**: длинные ID устройств обрезаются до 8 символов + "..."
- **Эмодзи-индикаторы**: 🟢 для онлайн, 🔴 для офлайн статуса
- **Компактные кнопки**: "🔗 Подключить" / " Добавить" вместо длинного текста
- **Иконки камер**: 📷 перед списком доступных камер
### ✅ 2. Сворачиваемый журнал событий
- **Кликабельный заголовок**: нажмите на "📋 Журнал событий" для сворачивания
- **Анимированная иконка**: ▲ (развернут) / ▼ (свернут)
- **Плавная анимация**: 0.3s transition для всех изменений
### ✅ 3. Адаптивные размеры панелей
- **При развернутых логах**: панель устройств/сессий ограничена высотой 180px
- **При свернутых логах**: панель увеличивается до 250px для лучшего обзора
- **Плавные переходы**: все изменения размеров анимированы
### ✅ 4. Улучшенное отображение сессий
- **Компактный формат**: "📱 device123... | 📷 back"
- **Эмодзи-статусы**: 🟠 Ожидание, 🟢 Активна, 🔴 Отклонена, ⚫ Завершена
- **Иконочные кнопки**: ✓ Активна, 🔄 Переключить, 📷 Камера, ❌ Завершить, 🗑️ Удалить
## Как протестировать:
### Тест 1: Сворачивание логов
1. Откройте десктопное приложение
2. Подключитесь к серверу (появятся сообщения в логах)
3. Кликните на заголовок "📋 Журнал событий"
4. **Ожидаемый результат**:
- Логи плавно скрываются
- Иконка меняется на ▼
- Панели устройств и сессий увеличиваются в высоту
### Тест 2: Компактные устройства
1. В веб-демо (http://localhost:3001) симулируйте Android устройство
2. В десктопном приложении должно появиться устройство с:
- Сокращенным ID (если длинный)
- Статусом 🟢 Онлайн
- Кнопкой "🔗 Подключить"
- Иконкой 📷 перед типами камер
### Тест 3: Множественные устройства
1. Симулируйте несколько устройств в веб-демо
2. **Проверьте**:
- Все устройства помещаются в компактную панель
- Прокрутка работает если устройств много
- Каждое устройство отображается корректно
### Тест 4: Компактные сессии
1. Создайте несколько сессий с разными устройствами
2. **Проверьте**:
- Сессии отображаются в формате "📱 device | 📷 camera"
- Статусы показываются эмодзи
- Кнопки имеют иконки вместо текста
- Активная сессия выделена зеленым
### Тест 5: Адаптивность интерфейса
1. Попробуйте сворачивать/разворачивать логи при разном количестве устройств/сессий
2. **Ожидаемый результат**:
- При свернутых логах больше места для списков
- Все анимации плавные
- Интерфейс остается читаемым
## Визуальные улучшения:
### Устройства:
```
📱 Устройства
🔄
┌─────────────────────────┐
│ ID: device12... │
│ Статус: 🟢 Онлайн │
│ Сессии: 2 │
│ 📷 back, front │
│ [ Добавить] │
└─────────────────────────┘
```
### Сессии:
```
🔗 Активные сессии
┌─────────────────────────┐
│ 📱 device123... | 📷 back │
│ 🟢 Активна │
│ [✓][📷][❌] │
└─────────────────────────┘
```
### Логи (сворачиваемые):
```
📋 Журнал событий ▲
┌─────────────────────────┐
│ [12:34:56] INFO: ... │
│ [12:34:57] SUCCESS: ... │
└─────────────────────────┘
[Очистить]
При клике на заголовок:
📋 Журнал событий ▼
[Очистить]
```
## Преимущества нового интерфейса:
1. **Больше информации** в том же пространстве
2. **Лучшая читаемость** благодаря иконкам
3. **Гибкость** - можно скрыть логи для больших списков
4. **Современный вид** с анимациями и эмодзи
5. **Быстрая навигация** по устройствам и сессиям
Теперь интерфейс готов для работы с большим количеством устройств и эффективного управления множественными сессиями! 🚀

107
docs/MOBILE_TEST_GUIDE.md Normal file
View File

@@ -0,0 +1,107 @@
# 📱 Руководство по тестированию мобильной версии GodEye
## 🚀 Запуск системы
1. **Сервер запущен** на: `http://localhost:3001`
2. **Мобильная версия**: `http://localhost:3001/mobile`
3. **Десктопная версия**: `http://localhost:3001/demo`
## 📱 Тестирование с телефона
### Вариант 1: Прямое подключение
1. Откройте браузер на телефоне
2. Перейдите по адресу: `http://[IP_СЕРВЕРА]:3001`
3. Система автоматически определит мобильное устройство и покажет мобильную версию
### Вариант 2: Принудительная мобильная версия
- Перейдите по адресу: `http://[IP_СЕРВЕРА]:3001/mobile`
## 🔧 Настройка доступа с телефона
### Узнать IP адрес сервера:
```bash
ip addr show | grep "inet.*192\|inet.*10\."
```
### Альтернативно (для локальной сети):
```bash
hostname -I
```
## 📋 Пошаговое тестирование
### 1. Подготовка
- ✅ Сервер запущен на порту 3001
- ✅ Мобильная версия создана
- ✅ Роуты настроены
### 2. Тестирование камеры на телефоне
1. Откройте `http://[IP]:3001` на телефоне
2. Разрешите доступ к камере
3. Проверьте переключение фронт/тыл камера
4. Проверьте отображение превью
### 3. Тестирование соединения с оператором
1. Откройте **Desktop Operator** (`/desktop-operator/`)
2. Подключитесь к серверу
3. Найдите мобильное устройство в списке
4. Попробуйте запросить доступ к камере
### 4. Тестирование WebRTC соединения
1. Оператор отправляет запрос
2. На телефоне появляется уведомление
3. Принимаете запрос на телефоне
4. Проверяете видеопоток в Desktop Operator
## 🌐 Доступные URL
- **Главная** (авто-определение): `http://[IP]:3001/`
- **Мобильная версия**: `http://[IP]:3001/mobile`
- **Десктоп демо**: `http://[IP]:3001/demo`
- **API статус**: `http://[IP]:3001/api/status`
## ✨ Возможности мобильной версии
### Интерфейс
- 📱 Адаптивный дизайн для телефонов
- 🎥 Превью камеры в реальном времени
- 🔄 Переключение фронт/тыл камера
- 📊 Статус соединения
- 🔔 Уведомления о запросах
### Функциональность
- 🔌 WebSocket соединение с сервером
- 📹 WebRTC для передачи видео
- 🎯 Обработка запросов от операторов
- ✅ Принятие/отклонение запросов
- 🔄 Поддержка нескольких сессий
## 🐛 Отладка
### Проверка соединения
1. Откройте консоль разработчика в браузере
2. Проверьте WebSocket соединение
3. Проверьте ошибки WebRTC
### Логи сервера
```bash
tail -f /home/data/god_eye/backend/god-eye.log
```
### Проверка устройств
```bash
curl http://localhost:3001/api/status
```
## 📝 Примечания
- **HTTPS**: Для работы камеры в production нужен HTTPS
- **Локальная сеть**: В локальной сети работает через HTTP
- **Порты**: Убедитесь, что порт 3001 открыт в файрволе
- **Браузер**: Рекомендуется Chrome/Safari для лучшей поддержки WebRTC
## 🚨 Известные ограничения
1. **HTTP vs HTTPS**: Камера работает только на localhost через HTTP или везде через HTTPS
2. **Файрвол**: Нужно открыть порт 3001 для внешних подключений
3. **WebRTC**: Некоторые сетевые настройки могут блокировать P2P соединения

View File

@@ -0,0 +1,288 @@
# Руководство: Подключение Desktop Operator к Android телефону
## 🔗 Как работает подключение оператора к телефону
### Архитектура подключения
```
Desktop Operator ←→ WebSocket ←→ Сервер ←→ WebSocket ←→ Android телефон
ConnectionManager
(управляет соединениями)
WebRTC P2P соединение
(прямая передача видео)
```
### 1. 📱 Регистрация Android устройства
**Android телефон подключается к серверу:**
```kotlin
// Android код
val socket = IO.socket("ws://192.168.1.100:3001")
socket.emit("register:android", JSONObject().apply {
put("deviceId", "android_unique_id")
put("deviceInfo", JSONObject().apply {
put("manufacturer", "Samsung")
put("model", "Galaxy S21")
put("androidVersion", "11")
put("availableCameras", JSONArray(listOf("back", "front", "ultra_wide")))
put("appVersion", "1.0.0")
})
})
```
**Сервер подтверждает регистрацию:**
```javascript
// Лог сервера
{"level":"info","message":"Android client registered: android_unique_id","timestamp":"..."}
```
### 2. 💻 Подключение Desktop Operator
**Operator приложение подключается:**
```javascript
// Desktop Operator код
const socket = io('ws://192.168.1.100:3001');
socket.emit('register:operator', {
operatorId: 'operator-uuid',
operatorInfo: {
name: 'Иван Петров',
organization: 'Служба безопасности',
permissions: ['view_cameras', 'request_camera', 'initiate_connection']
}
});
```
### 3. 🔍 Проверка доступных устройств
**REST API запрос списка устройств:**
```bash
curl -H "x-operator-id: operator-uuid" \
http://localhost:3001/api/operators/devices
```
**Ответ сервера:**
```json
{
"success": true,
"devices": [
{
"deviceId": "android_unique_id",
"model": "Galaxy S21",
"manufacturer": "Samsung",
"isConnected": true,
"availableCameras": ["back", "front", "ultra_wide"],
"canAcceptNewSession": true,
"activeSessions": 0
}
],
"total": 1
}
```
### 4. 📞 Инициация подключения к телефону
**Оператор запрашивает доступ к камере:**
```bash
curl -X POST http://localhost:3001/api/operators/connections/request \
-H "Content-Type: application/json" \
-H "x-operator-id: operator-uuid" \
-d '{
"deviceId": "android_unique_id",
"cameraType": "back"
}'
```
**Сервер создает подключение:**
```json
{
"success": true,
"connectionId": "conn-uuid",
"sessionId": "session-uuid",
"message": "Connection request initiated"
}
```
### 5. 📲 Android получает запрос подтверждения
**WebSocket событие на Android:**
```javascript
// Сервер → Android
socket.emit('connection:request', {
connectionId: 'conn-uuid',
sessionId: 'session-uuid',
operatorId: 'operator-uuid',
operatorInfo: {
name: 'Иван Петров',
organization: 'Служба безопасности',
reason: 'Проверка безопасности'
},
cameraType: 'back',
timestamp: '2025-10-04T12:00:00.000Z',
expiresAt: '2025-10-04T12:05:00.000Z' // 5 минут на ответ
});
```
**Android показывает диалог пользователю:**
```
┌─────────────────────────────────┐
│ Запрос доступа к камере │
├─────────────────────────────────┤
│ Оператор: Иван Петров │
│ Организация: Служба безопасности│
│ Причина: Проверка безопасности │
│ Камера: Задняя │
│ │
│ Разрешить доступ к камере? │
│ │
│ [Разрешить] [Отклонить] │
└─────────────────────────────────┘
```
### 6. ✅ Пользователь принимает/отклоняет запрос
**Если пользователь принимает:**
```kotlin
// Android → Сервер
socket.emit("connection:accept", JSONObject().apply {
put("connectionId", connectionId)
put("sessionId", sessionId)
put("cameraType", "back")
put("webrtcInfo", JSONObject().apply {
put("supported", true)
put("codecs", JSONArray(listOf("H264", "VP8")))
put("resolutions", JSONArray(listOf("720p", "1080p")))
})
})
```
**Сервер уведомляет оператора:**
```javascript
// Сервер → Desktop Operator
socket.emit('connection:accepted', {
connectionId: 'conn-uuid',
sessionId: 'session-uuid',
webrtcInfo: {
supported: true,
codecs: ['H264', 'VP8'],
resolutions: ['720p', '1080p']
}
});
```
### 7. 🎥 Установка WebRTC соединения
**Обмен WebRTC сигналами:**
```
Desktop Operator → Сервер → Android: webrtc:offer
Android → Сервер → Desktop Operator: webrtc:answer
Desktop Operator ↔ Сервер ↔ Android: webrtc:ice-candidate
```
**После успешного установления WebRTC:**
```
Desktop Operator ←-----WebRTC P2P-----→ Android
(прямое видео)
```
## 🧪 Тестирование подключения
### Проверка 1: Статус системы
```bash
curl http://localhost:3001/api/status | jq
```
### Проверка 2: Список устройств
```bash
curl -H "x-operator-id: YOUR_OPERATOR_ID" \
http://localhost:3001/api/operators/devices | jq
```
### Проверка 3: Инициация подключения
```bash
curl -X POST http://localhost:3001/api/operators/connections/request \
-H "Content-Type: application/json" \
-H "x-operator-id: YOUR_OPERATOR_ID" \
-d '{
"deviceId": "android_device_id",
"cameraType": "back"
}' | jq
```
### Проверка 4: Статус подключений
```bash
curl -H "x-operator-id: YOUR_OPERATOR_ID" \
http://localhost:3001/api/operators/connections | jq
```
## 🐞 Диагностика проблем
### Проблема: "Invalid or disconnected operator"
**Причина:** Оператор не подключен к WebSocket
**Решение:**
1. Откройте веб-демо: http://localhost:3001
2. Проверьте WebSocket подключение в консоли браузера
3. Используйте Operator ID из консоли
### Проблема: "Device not found"
**Причина:** Android устройство не подключено
**Решение:**
1. Перезапустите Android приложение
2. Проверьте сетевое соединение
3. Убедитесь что устройство регистрируется
### Проблема: "Device busy or unavailable"
**Причина:** Устройство занято другой сессией
**Решение:**
1. Завершите активные сессии
2. Подождите таймаут (30 секунд)
3. Попробуйте снова
## 📊 Мониторинг соединений
### Логи сервера
```bash
tail -f /home/data/god_eye/backend/god-eye.log
```
### Административная статистика
```bash
curl http://localhost:3001/api/admin/stats | jq
```
### Real-time статус через WebSocket
```javascript
socket.on('device:connected', (data) => {
console.log('Устройство подключено:', data.deviceId);
});
socket.on('connection:accepted', (data) => {
console.log('Подключение принято:', data.connectionId);
});
```
## 🔒 Безопасность
1. **Разрешения пользователя:** Android требует подтверждения каждого запроса
2. **Таймауты:** Запросы автоматически истекают через 5 минут
3. **Аутентификация:** Проверка Operator ID для всех API запросов
4. **Логирование:** Все операции записываются в логи
## 🚀 Быстрый старт
1. **Запустите сервер:**
```bash
cd /home/data/god_eye/backend && npm start
```
2. **Откройте веб-демо:**
http://localhost:3001
3. **Запустите Android приложение** и подключите к серверу
4. **Инициируйте подключение** через веб-интерфейс или API
5. **Примите запрос** на Android устройстве
6. **Наслаждайтесь** прямым WebRTC видеопотоком!

View File

@@ -0,0 +1,484 @@
# Протокол запросов от сервера к Android устройству
## Обзор
Данный документ описывает протокол WebSocket событий, которые сервер отправляет на Android устройство для запроса подтверждения подключения оператора и открытия сеанса камеры.
## Схема работы
1. **Оператор инициирует подключение** через REST API или WebSocket
2. **Сервер создает соединение** в ConnectionManager
3. **Сервер отправляет запрос на Android** через WebSocket
4. **Android отображает диалог** пользователю
5. **Пользователь принимает/отклоняет** запрос
6. **Android отправляет ответ** серверу
7. **Сервер уведомляет оператора** о результате
## События от сервера к Android
### 1. `connection:request` - Запрос на подключение
Отправляется Android устройству когда оператор запрашивает доступ к камере.
```javascript
// Сервер → Android
socket.emit('connection:request', {
connectionId: 'uuid-connection-id',
sessionId: 'uuid-session-id',
operatorId: 'uuid-operator-id',
operatorInfo: {
name: 'Имя оператора',
organization: 'Организация',
reason: 'Причина запроса доступа'
},
cameraType: 'back', // 'back', 'front', 'wide', 'telephoto'
timestamp: '2025-10-04T12:00:00.000Z',
expiresAt: '2025-10-04T12:05:00.000Z' // Время истечения запроса (5 минут)
});
```
**Ожидаемый ответ от Android:**
- `connection:accept` - пользователь принял запрос
- `connection:reject` - пользователь отклонил запрос
- Timeout через 5 минут если нет ответа
### 2. `camera:request` - Запрос доступа к камере (Legacy)
Для совместимости со старой системой. Используется при прямом запросе камеры.
```javascript
// Сервер → Android
socket.emit('camera:request', {
sessionId: 'uuid-session-id',
operatorId: 'uuid-operator-id',
cameraType: 'back',
timestamp: '2025-10-04T12:00:00.000Z'
});
```
### 3. `camera:switch` - Переключение камеры
Запрос на переключение камеры во время активного сеанса.
```javascript
// Сервер → Android
socket.emit('camera:switch', {
sessionId: 'uuid-session-id',
cameraType: 'front', // Новый тип камеры
timestamp: '2025-10-04T12:00:00.000Z'
});
```
### 4. `camera:disconnect` - Завершение сеанса
Уведомление о завершении сеанса камеры.
```javascript
// Сервер → Android
socket.emit('camera:disconnect', {
sessionId: 'uuid-session-id',
reason: 'operator_disconnect', // 'operator_disconnect', 'timeout', 'error'
timestamp: '2025-10-04T12:00:00.000Z'
});
```
## События от Android к серверу
### 1. `connection:accept` - Принятие подключения
```javascript
// Android → Сервер
socket.emit('connection:accept', {
connectionId: 'uuid-connection-id',
sessionId: 'uuid-session-id',
cameraType: 'back',
webrtcInfo: {
supported: true,
codecs: ['H264', 'VP8'],
resolutions: ['720p', '1080p']
},
timestamp: '2025-10-04T12:00:00.000Z'
});
```
### 2. `connection:reject` - Отклонение подключения
```javascript
// Android → Сервер
socket.emit('connection:reject', {
connectionId: 'uuid-connection-id',
reason: 'user_denied', // 'user_denied', 'camera_busy', 'permission_denied'
timestamp: '2025-10-04T12:00:00.000Z'
});
```
### 3. `camera:response` - Ответ на запрос камеры (Legacy)
```javascript
// Android → Сервер
socket.emit('camera:response', {
sessionId: 'uuid-session-id',
accepted: true, // true/false
reason: 'camera_granted', // или причина отказа
cameraType: 'back',
timestamp: '2025-10-04T12:00:00.000Z'
});
```
## Жизненный цикл подключения
### Успешное подключение
```mermaid
sequenceDiagram
participant O as Оператор
participant S as Сервер
participant A as Android
O->>S: POST /api/operators/connections/request
S->>S: Создание connection в ConnectionManager
S->>A: connection:request
A->>A: Показ диалога пользователю
A->>S: connection:accept
S->>S: Обновление connection (status: accepted)
S->>O: connection:accepted (WebSocket)
Note over O,A: Начало WebRTC сеанса
```
### Отклонение подключения
```mermaid
sequenceDiagram
participant O as Оператор
participant S as Сервер
participant A as Android
O->>S: POST /api/operators/connections/request
S->>S: Создание connection в ConnectionManager
S->>A: connection:request
A->>A: Показ диалога пользователю
A->>S: connection:reject
S->>S: Обновление connection (status: rejected)
S->>O: connection:rejected (WebSocket)
```
### Таймаут подключения
```mermaid
sequenceDiagram
participant O as Оператор
participant S as Сервер
participant A as Android
O->>S: POST /api/operators/connections/request
S->>S: Создание connection в ConnectionManager
S->>A: connection:request
Note over A: Пользователь не отвечает
S->>S: Timeout через 5 минут
S->>S: Обновление connection (status: timeout)
S->>O: connection:timeout (WebSocket)
S->>A: connection:timeout (уведомление)
```
## Обработка в ConnectionManager
### Инициация подключения
```javascript
// В файле /backend/src/managers/ConnectionManager.js
async initiateConnection(operatorId, deviceId, cameraType = 'back') {
// 1. Создание connection объекта
const connection = {
connectionId: uuidv4(),
sessionId: uuidv4(),
operatorId,
deviceId,
cameraType,
status: 'pending',
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
};
// 2. Сохранение в память
this.connections.set(connection.connectionId, connection);
// 3. Отправка запроса на Android
const device = this.deviceManager.getDevice(deviceId);
device.socket.emit('connection:request', {
connectionId: connection.connectionId,
sessionId: connection.sessionId,
operatorId,
operatorInfo: operator.operatorInfo,
cameraType,
timestamp: connection.createdAt,
expiresAt: connection.expiresAt
});
// 4. Установка таймаута
setTimeout(() => {
if (connection.status === 'pending') {
this.handleConnectionTimeout(connection.connectionId);
}
}, 5 * 60 * 1000);
return connection;
}
```
### Принятие подключения
```javascript
async acceptConnection(connectionId, responseData) {
const connection = this.connections.get(connectionId);
// Обновление статуса
connection.status = 'accepted';
connection.acceptedAt = new Date().toISOString();
connection.webrtcInfo = responseData.webrtcInfo;
// Создание сессии
const session = this.sessionManager.createSession(
connection.deviceId,
connection.operatorId,
connection.cameraType
);
// Уведомление оператора
const operator = this.deviceManager.getOperator(connection.operatorId);
operator.socket.emit('connection:accepted', {
connectionId,
sessionId: session.sessionId,
webrtcInfo: connection.webrtcInfo
});
return connection;
}
```
## Обработка в Android приложении
### Регистрация обработчиков событий
```kotlin
// В Android клиенте
socket.on("connection:request") { args ->
val data = args[0] as JSONObject
val connectionId = data.getString("connectionId")
val operatorInfo = data.getJSONObject("operatorInfo")
val cameraType = data.getString("cameraType")
// Показ диалога пользователю
showConnectionRequestDialog(connectionId, operatorInfo, cameraType)
}
socket.on("camera:request") { args ->
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
val operatorId = data.getString("operatorId")
val cameraType = data.getString("cameraType")
// Legacy обработка для совместимости
handleCameraRequest(sessionId, operatorId, cameraType)
}
```
### Ответ на запрос подключения
```kotlin
private fun acceptConnection(connectionId: String, cameraType: String) {
val response = JSONObject().apply {
put("connectionId", connectionId)
put("sessionId", sessionId)
put("cameraType", cameraType)
put("webrtcInfo", JSONObject().apply {
put("supported", true)
put("codecs", JSONArray(listOf("H264", "VP8")))
put("resolutions", JSONArray(listOf("720p", "1080p")))
})
put("timestamp", Instant.now().toString())
}
socket.emit("connection:accept", response)
// Начало подготовки камеры
startCameraPreview(cameraType)
}
private fun rejectConnection(connectionId: String, reason: String) {
val response = JSONObject().apply {
put("connectionId", connectionId)
put("reason", reason)
put("timestamp", Instant.now().toString())
}
socket.emit("connection:reject", response)
}
```
## UI диалог на Android
### Пример диалога подтверждения
```kotlin
private fun showConnectionRequestDialog(
connectionId: String,
operatorInfo: JSONObject,
cameraType: String
) {
val dialog = AlertDialog.Builder(this)
.setTitle("Запрос доступа к камере")
.setMessage("""
Оператор: ${operatorInfo.getString("name")}
Организация: ${operatorInfo.getString("organization")}
Причина: ${operatorInfo.getString("reason")}
Камера: ${getCameraDisplayName(cameraType)}
Разрешить доступ к камере?
""".trimIndent())
.setPositiveButton("Разрешить") { _, _ ->
acceptConnection(connectionId, cameraType)
}
.setNegativeButton("Отклонить") { _, _ ->
rejectConnection(connectionId, "user_denied")
}
.setCancelable(false)
.create()
dialog.show()
// Автоматическое закрытие через 5 минут
Handler(Looper.getMainLooper()).postDelayed({
if (dialog.isShowing) {
dialog.dismiss()
rejectConnection(connectionId, "timeout")
}
}, 5 * 60 * 1000)
}
```
## Безопасность и валидация
### Проверки на сервере
1. **Валидация оператора**: проверка разрешений и статуса подключения
2. **Валидация устройства**: проверка доступности и возможности принять сессию
3. **Лимиты времени**: автоматическое завершение запросов через 5 минут
4. **Лимиты сессий**: проверка максимального количества активных сессий
### Проверки на Android
1. **Валидация connectionId**: проверка существования активного запроса
2. **Проверка разрешений**: доступ к камере и микрофону
3. **Проверка состояния**: доступность камеры для использования
4. **Защита от спама**: лимит на количество запросов в минуту
## Логирование и мониторинг
### События для логирования
```javascript
// Сервер
logger.info('Connection request initiated', {
connectionId,
operatorId,
deviceId,
cameraType
});
logger.info('Connection accepted by device', {
connectionId,
sessionId,
responseTime: Date.now() - connection.createdAt
});
logger.warn('Connection rejected by device', {
connectionId,
reason,
operatorId,
deviceId
});
logger.error('Connection timeout', {
connectionId,
operatorId,
deviceId,
duration: 5 * 60 * 1000
});
```
### Метрики для мониторинга
- Время ответа Android устройств на запросы
- Процент принятых/отклоненных подключений
- Количество таймаутов
- Средняя продолжительность сессий
- Ошибки WebRTC соединений
## Совместимость
Система поддерживает как новый протокол подключений (`connection:*` события), так и старый протокол (`camera:*` события) для обратной совместимости.
### Миграция со старого протокола
1. **Этап 1**: Добавление поддержки новых событий в Android
2. **Этап 2**: Постепенный переход операторов на новый API
3. **Этап 3**: Удаление старых обработчиков после полной миграции
## Примеры использования
### Тестирование через WebSocket
```javascript
// Подключение к серверу
const socket = io('ws://localhost:3001');
// Симуляция Android устройства
socket.emit('register:android', {
deviceId: 'test-device-001',
deviceInfo: {
manufacturer: 'Samsung',
model: 'Galaxy S21',
availableCameras: ['back', 'front'],
androidVersion: '11'
}
});
// Обработка запросов
socket.on('connection:request', (data) => {
console.log('Получен запрос подключения:', data);
// Автоматическое принятие для тестирования
setTimeout(() => {
socket.emit('connection:accept', {
connectionId: data.connectionId,
sessionId: data.sessionId,
cameraType: data.cameraType,
webrtcInfo: {
supported: true,
codecs: ['H264'],
resolutions: ['1080p']
}
});
}, 2000);
});
```
### Тестирование через REST API
```bash
# Инициация подключения
curl -X POST http://localhost:3001/api/operators/connections/request \
-H "Content-Type: application/json" \
-H "x-operator-id: operator-uuid" \
-d '{
"deviceId": "test-device-001",
"cameraType": "back"
}'
# Проверка статуса подключений
curl -H "x-operator-id: operator-uuid" \
http://localhost:3001/api/operators/connections
```
Этот протокол обеспечивает надежную и безопасную систему запросов доступа к камере Android устройств с полным контролем пользователя над разрешениями.

134
docs/WEBRTC_FIXES_GUIDE.md Normal file
View File

@@ -0,0 +1,134 @@
# 🛠️ Исправления WebRTC проблем - Руководство по тестированию
## ✅ Что было исправлено
### 1. **Событие регистрации мобильного клиента**
-**Было**: `register:android` (неправильно)
-**Стало**: `register:mobile_web` (правильно)
### 2. **Добавлена полная поддержка WebRTC**
- Добавлен класс `RTCPeerConnection`
- Обработчики WebRTC событий: `webrtc:offer`, `webrtc:answer`, `webrtc:ice-candidate`
- Правильная обработка ICE candidates
- Обработка состояний соединения
### 3. **Улучшена диагностика ошибок**
- Подробные логи WebRTC процесса
- Отслеживание состояний соединения
- Более информативные сообщения об ошибках
## 🧪 Как протестировать исправления
### Шаг 1: Перезапустить сервер (✅ Выполнено)
```bash
cd /home/data/god_eye/backend
node src/server.js
# Сервер работает на http://localhost:3001
```
### Шаг 2: Тестирование с мобильного телефона
1. **Откройте браузер на телефоне**
2. **Перейдите**: `http://[IP_СЕРВЕРА]:3001/mobile`
3. **Проверьте регистрацию**: В логах должно появиться "Mobile web client registered"
4. **Включите камеру**: Нажмите кнопку 📷
5. **Проверьте превью**: Видео должно отображаться
### Шаг 3: Тестирование с Desktop Operator
1. **Запустите Desktop Operator приложение**
2. **Подключитесь к серверу**: `ws://localhost:3001`
3. **Найдите мобильное устройство** в списке устройств
4. **Отправьте запрос** на доступ к камере
### Шаг 4: Полный цикл WebRTC
1. **На телефоне**: Принмите запрос (✅ Принять)
2. **Проверьте логи** в браузере телефона (F12 -> Console)
3. **В Desktop Operator**: Должно появиться видео с телефона
## 📊 Что проверить в логах
### Логи сервера (терминал):
```
📱 Mobile web client connected: [ID]
🌐 Mobile web event: register:mobile_web
Mobile web client registered: [device-id]
```
### Логи мобильного браузера (F12 -> Console):
```
📱 Устройство зарегистрировано
📞 Получен WebRTC offer
📝 Remote description установлен
✅ WebRTC answer отправлен
🧊 ICE candidate добавлен
🔗 WebRTC состояние: connected
```
### Логи Desktop Operator:
```
Session created: [session-id]
WebRTC offer sent
WebRTC answer received
ICE candidate received
Video stream connected
```
## 🚨 Возможные проблемы и решения
### Проблема: "Ошибка камеры: undefined"
**Решение**:
- Проверьте разрешения камеры в браузере
- Попробуйте обновить страницу
- Убедитесь что камера не занята другими приложениями
### Проблема: WebRTC соединение не устанавливается
**Решение**:
- Проверьте что оба устройства в одной сети
- Посмотрите логи на предмет ошибок ICE
- Попробуйте другой браузер
### Проблема: Видео не отображается в операторе
**Решение**:
- Проверьте что мобильный клиент правильно отправляет видео поток
- Убедитесь что Desktop Operator правильно обрабатывает WebRTC
## 🔧 Команды для отладки
### Проверить статус сервера:
```bash
curl http://localhost:3001/api/status
```
### Проверить подключенные устройства:
```bash
curl http://localhost:3001/api/devices
```
### Просмотр логов сервера в реальном времени:
```bash
tail -f /home/data/god_eye/backend/god-eye.log
```
## 📱 Узнать IP для доступа с телефона
### Linux:
```bash
hostname -I | awk '{print $1}'
```
### Или:
```bash
ip addr show | grep "inet.*192\|inet.*10\." | head -1
```
## ✨ Ожидаемый результат
После всех исправлений должно работать:
1. ✅ Мобильный браузер подключается как `mobile_web` устройство
2. ✅ Desktop Operator видит мобильное устройство в списке
3. ✅ WebRTC соединение устанавливается успешно
4. ✅ Видео с телефона транслируется в Desktop Operator
5. ✅ Переключение камер работает корректно
---
**🎯 Основная цель**: Добиться стабильной работы видеопотока между мобильным браузером и desktop оператором через WebRTC.

52
god-eye.log Normal file
View File

@@ -0,0 +1,52 @@
{"level":"info","message":"GodEye Backend Server running on 0.0.0.0:3001","timestamp":"2025-10-03T09:21:49.526Z"}
{"address":"192.168.219.109","isAndroid":true,"isMobileWeb":false,"level":"info","message":"New connection: NER453_lZs-P7q5WAAAB","timestamp":"2025-10-03T09:23:25.687Z","userAgent":"okhttp/3.12.12"}
{"level":"info","message":"🤖 Android client connected: NER453_lZs-P7q5WAAAB","timestamp":"2025-10-03T09:23:25.688Z"}
{"deviceId":"android_bcac6e0d","deviceInfo":{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","manufacturer":"LGE","model":"LGM-G600S","platform":"android","registrationTime":1759483406842,"sdkVersion":28},"level":"info","message":"📱 Android event: register:android","timestamp":"2025-10-03T09:23:27.674Z"}
{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","level":"info","manufacturer":"LGE","message":"Android client registered: android_bcac6e0d","model":"LGM-G600S","platform":"android","registrationTime":1759483406842,"sdkVersion":28,"timestamp":"2025-10-03T09:23:27.675Z"}
{"level":"info","message":"Client disconnected: NER453_lZs-P7q5WAAAB, reason: client namespace disconnect","timestamp":"2025-10-03T09:23:49.911Z"}
{"level":"info","message":"🧹 Cleaning up connections for device: android_bcac6e0d","timestamp":"2025-10-03T09:23:49.911Z"}
{"level":"info","message":"🧹 Cleaned up 0 connections for device android_bcac6e0d","timestamp":"2025-10-03T09:23:49.912Z"}
{"address":"192.168.219.109","isAndroid":true,"isMobileWeb":false,"level":"info","message":"New connection: XmheaS4oNeAN1rF6AAAD","timestamp":"2025-10-03T09:23:53.505Z","userAgent":"okhttp/3.12.12"}
{"level":"info","message":"🤖 Android client connected: XmheaS4oNeAN1rF6AAAD","timestamp":"2025-10-03T09:23:53.506Z"}
{"deviceId":"android_bcac6e0d","deviceInfo":{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","manufacturer":"LGE","model":"LGM-G600S","platform":"android","registrationTime":1759483434675,"sdkVersion":28},"level":"info","message":"📱 Android event: register:android","timestamp":"2025-10-03T09:23:55.504Z"}
{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","level":"info","manufacturer":"LGE","message":"Android client registered: android_bcac6e0d","model":"LGM-G600S","platform":"android","registrationTime":1759483434675,"sdkVersion":28,"timestamp":"2025-10-03T09:23:55.504Z"}
{"address":"192.168.219.108","isAndroid":false,"isMobileWeb":false,"level":"info","message":"New connection: jNxCYnfejCi6oqVKAAAF","timestamp":"2025-10-03T09:23:57.765Z","userAgent":""}
{"level":"info","message":"Operator registered: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:23:57.767Z"}
{"level":"info","message":"📷 Camera request received from operator socket jNxCYnfejCi6oqVKAAAF","timestamp":"2025-10-03T09:23:59.977Z"}
{"cameraType":"back","deviceId":"android_bcac6e0d","level":"info","message":"📷 Request data:","timestamp":"2025-10-03T09:23:59.977Z"}
{"level":"info","message":"✅ Operator found: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:23:59.977Z"}
{"level":"info","message":"🔗 Initiating connection: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d -> android_bcac6e0d (back)","timestamp":"2025-10-03T09:23:59.978Z"}
{"level":"info","message":"📱 Sending camera:request to Android device android_bcac6e0d","timestamp":"2025-10-03T09:23:59.979Z"}
{"level":"info","message":"✅ Connection request created: 2ba9186f-daf4-4e42-a906-20c09777ee7d","timestamp":"2025-10-03T09:23:59.979Z"}
{"level":"info","message":"✅ Connection initiated: 2ba9186f-daf4-4e42-a906-20c09777ee7d","timestamp":"2025-10-03T09:23:59.979Z"}
{"level":"info","message":"Client disconnected: jNxCYnfejCi6oqVKAAAF, reason: client namespace disconnect","timestamp":"2025-10-03T09:24:14.296Z"}
{"level":"info","message":"🧹 Cleaning up connections for operator: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:24:14.296Z"}
{"level":"info","message":"🔌 Connection terminated: 2ba9186f-daf4-4e42-a906-20c09777ee7d - Operator disconnected","timestamp":"2025-10-03T09:24:14.297Z"}
{"level":"info","message":"🧹 Cleaned up 1 connections for operator fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:24:14.297Z"}
{"level":"info","message":"Client disconnected: XmheaS4oNeAN1rF6AAAD, reason: client namespace disconnect","timestamp":"2025-10-03T09:24:15.073Z"}
{"level":"info","message":"🧹 Cleaning up connections for device: android_bcac6e0d","timestamp":"2025-10-03T09:24:15.073Z"}
{"level":"info","message":"🧹 Cleaned up 0 connections for device android_bcac6e0d","timestamp":"2025-10-03T09:24:15.073Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","timestamp":"2025-10-03T09:24:18.879Z"}
{"level":"info","message":"GodEye Backend Server running on 0.0.0.0:3001","timestamp":"2025-10-03T09:24:36.934Z"}
{"address":"192.168.219.109","isAndroid":true,"isMobileWeb":false,"level":"info","message":"New connection: qw8HXUTNBUAqooGfAAAB","timestamp":"2025-10-03T09:24:46.398Z","userAgent":"okhttp/3.12.12"}
{"level":"info","message":"🤖 Android client connected: qw8HXUTNBUAqooGfAAAB","timestamp":"2025-10-03T09:24:46.399Z"}
{"address":"192.168.219.109","isAndroid":true,"isMobileWeb":false,"level":"info","message":"New connection: Ndzw4a69YdAN7-jjAAAD","timestamp":"2025-10-03T09:24:51.582Z","userAgent":"okhttp/3.12.12"}
{"level":"info","message":"🤖 Android client connected: Ndzw4a69YdAN7-jjAAAD","timestamp":"2025-10-03T09:24:51.582Z"}
{"deviceId":"android_bcac6e0d","deviceInfo":{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","manufacturer":"LGE","model":"LGM-G600S","platform":"android","registrationTime":1759483492752,"sdkVersion":28},"level":"info","message":"📱 Android event: register:android","timestamp":"2025-10-03T09:24:53.591Z"}
{"androidVersion":"9","appVersion":"1.0.0","availableCameras":["back","front","ultra_wide","telephoto"],"brandName":"lge","capabilities":{"audio":true,"cameraSwitch":true,"video":true,"webrtc":true},"deviceName":"lucye","level":"info","manufacturer":"LGE","message":"Android client registered: android_bcac6e0d","model":"LGM-G600S","platform":"android","registrationTime":1759483492752,"sdkVersion":28,"timestamp":"2025-10-03T09:24:53.592Z"}
{"address":"192.168.219.108","isAndroid":false,"isMobileWeb":false,"level":"info","message":"New connection: YCjZFUSgVZczdpBuAAAF","timestamp":"2025-10-03T09:24:53.948Z","userAgent":""}
{"level":"info","message":"Operator registered: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:24:53.950Z"}
{"level":"info","message":"📷 Camera request received from operator socket YCjZFUSgVZczdpBuAAAF","timestamp":"2025-10-03T09:24:55.137Z"}
{"cameraType":"back","deviceId":"android_bcac6e0d","level":"info","message":"📷 Request data:","timestamp":"2025-10-03T09:24:55.137Z"}
{"level":"info","message":"✅ Operator found: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d","timestamp":"2025-10-03T09:24:55.137Z"}
{"level":"info","message":"🔗 Initiating connection: fcb2582a-9ee8-4674-b4ce-eab15aef8c5d -> android_bcac6e0d (back)","timestamp":"2025-10-03T09:24:55.138Z"}
{"level":"info","message":"📱 Sending camera:request to Android device android_bcac6e0d","timestamp":"2025-10-03T09:24:55.139Z"}
{"level":"info","message":"✅ Connection request created: 91b293f4-8a54-4505-a586-09ae218e137a","timestamp":"2025-10-03T09:24:55.139Z"}
{"level":"info","message":"✅ Connection initiated: 91b293f4-8a54-4505-a586-09ae218e137a","timestamp":"2025-10-03T09:24:55.140Z"}
{"level":"warn","message":"⏰ Connection timeout: 91b293f4-8a54-4505-a586-09ae218e137a","timestamp":"2025-10-03T09:25:25.140Z"}
{"level":"info","message":"❌ Connection rejected: 91b293f4-8a54-4505-a586-09ae218e137a - Connection timeout","timestamp":"2025-10-03T09:25:25.140Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","timestamp":"2025-10-03T09:26:52.612Z"}
{"level":"info","message":"Client disconnected: Ndzw4a69YdAN7-jjAAAD, reason: client namespace disconnect","timestamp":"2025-10-03T09:29:57.232Z"}
{"level":"info","message":"🧹 Cleaning up connections for device: android_bcac6e0d","timestamp":"2025-10-03T09:29:57.233Z"}
{"level":"info","message":"🧹 Cleaned up 0 connections for device android_bcac6e0d","timestamp":"2025-10-03T09:29:57.233Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","timestamp":"2025-10-03T09:30:02.434Z"}

363
node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,363 @@
{
"name": "god_eye",
"lockfileVersion": 3,
"requires": true,
"packages": {
"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/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"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/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"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-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/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"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-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/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/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"
}
}
}
}

24
node_modules/@socket.io/component-emitter/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,24 @@
(The MIT License)
Copyright (c) 2014 Component contributors <dev@component.io>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

79
node_modules/@socket.io/component-emitter/Readme.md generated vendored Normal file
View File

@@ -0,0 +1,79 @@
# `@socket.io/component-emitter`
Event emitter component.
This project is a fork of the [`component-emitter`](https://github.com/sindresorhus/component-emitter) project, with [Socket.IO](https://socket.io/)-specific TypeScript typings.
## Installation
```
$ npm i @socket.io/component-emitter
```
## API
### Emitter(obj)
The `Emitter` may also be used as a mixin. For example
a "plain" object may become an emitter, or you may
extend an existing prototype.
As an `Emitter` instance:
```js
import { Emitter } from '@socket.io/component-emitter';
var emitter = new Emitter;
emitter.emit('something');
```
As a mixin:
```js
import { Emitter } from '@socket.io/component-emitter';
var user = { name: 'tobi' };
Emitter(user);
user.emit('im a user');
```
As a prototype mixin:
```js
import { Emitter } from '@socket.io/component-emitter';
Emitter(User.prototype);
```
### Emitter#on(event, fn)
Register an `event` handler `fn`.
### Emitter#once(event, fn)
Register a single-shot `event` handler `fn`,
removed immediately after it is invoked the
first time.
### Emitter#off(event, fn)
* Pass `event` and `fn` to remove a listener.
* Pass `event` to remove all listeners on that event.
* Pass nothing to remove all listeners on all events.
### Emitter#emit(event, ...)
Emit an `event` with variable option args.
### Emitter#listeners(event)
Return an array of callbacks, or an empty array.
### Emitter#hasListeners(event)
Check if this emitter has `event` handlers.
## License
MIT

View File

@@ -0,0 +1,179 @@
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any;
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>;
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>;
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void | Promise<void>
: T;
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export class Emitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
> {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Removes the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev?: Ev,
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this;
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): this;
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[];
/**
* Returns true if there is a listener for this event.
*
* @param event Event name
* @returns boolean
*/
hasListeners<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(event: Ev): boolean;
/**
* Removes the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
removeListener<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(
ev?: Ev,
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Removes all `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
*/
removeAllListeners<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(ev?: Ev): this;
}

View File

@@ -0,0 +1,176 @@
/**
* Expose `Emitter`.
*/
exports.Emitter = Emitter;
/**
* Initialize a new `Emitter`.
*
* @api public
*/
function Emitter(obj) {
if (obj) return mixin(obj);
}
/**
* Mixin the emitter properties.
*
* @param {Object} obj
* @return {Object}
* @api private
*/
function mixin(obj) {
for (var key in Emitter.prototype) {
obj[key] = Emitter.prototype[key];
}
return obj;
}
/**
* Listen on the given `event` with `fn`.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.on =
Emitter.prototype.addEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
(this._callbacks['$' + event] = this._callbacks['$' + event] || [])
.push(fn);
return this;
};
/**
* Adds an `event` listener that will be invoked a single
* time then automatically removed.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.once = function(event, fn){
function on() {
this.off(event, on);
fn.apply(this, arguments);
}
on.fn = fn;
this.on(event, on);
return this;
};
/**
* Remove the given callback for `event` or all
* registered callbacks.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.off =
Emitter.prototype.removeListener =
Emitter.prototype.removeAllListeners =
Emitter.prototype.removeEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
// all
if (0 == arguments.length) {
this._callbacks = {};
return this;
}
// specific event
var callbacks = this._callbacks['$' + event];
if (!callbacks) return this;
// remove all handlers
if (1 == arguments.length) {
delete this._callbacks['$' + event];
return this;
}
// remove specific handler
var cb;
for (var i = 0; i < callbacks.length; i++) {
cb = callbacks[i];
if (cb === fn || cb.fn === fn) {
callbacks.splice(i, 1);
break;
}
}
// Remove event specific arrays for event types that no
// one is subscribed for to avoid memory leak.
if (callbacks.length === 0) {
delete this._callbacks['$' + event];
}
return this;
};
/**
* Emit `event` with the given args.
*
* @param {String} event
* @param {Mixed} ...
* @return {Emitter}
*/
Emitter.prototype.emit = function(event){
this._callbacks = this._callbacks || {};
var args = new Array(arguments.length - 1)
, callbacks = this._callbacks['$' + event];
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
if (callbacks) {
callbacks = callbacks.slice(0);
for (var i = 0, len = callbacks.length; i < len; ++i) {
callbacks[i].apply(this, args);
}
}
return this;
};
// alias used for reserved events (protected method)
Emitter.prototype.emitReserved = Emitter.prototype.emit;
/**
* Return array of callbacks for `event`.
*
* @param {String} event
* @return {Array}
* @api public
*/
Emitter.prototype.listeners = function(event){
this._callbacks = this._callbacks || {};
return this._callbacks['$' + event] || [];
};
/**
* Check if this emitter has `event` handlers.
*
* @param {String} event
* @return {Boolean}
* @api public
*/
Emitter.prototype.hasListeners = function(event){
return !! this.listeners(event).length;
};

View File

@@ -0,0 +1,4 @@
{
"name": "@socket.io/component-emitter",
"type": "commonjs"
}

View File

@@ -0,0 +1,179 @@
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any;
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>;
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>;
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void | Promise<void>
: T;
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export class Emitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
> {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Removes the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev?: Ev,
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this;
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): this;
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[];
/**
* Returns true if there is a listener for this event.
*
* @param event Event name
* @returns boolean
*/
hasListeners<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(event: Ev): boolean;
/**
* Removes the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
removeListener<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(
ev?: Ev,
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this;
/**
* Removes all `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
*/
removeAllListeners<
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
>(ev?: Ev): this;
}

View File

@@ -0,0 +1,169 @@
/**
* Initialize a new `Emitter`.
*
* @api public
*/
export function Emitter(obj) {
if (obj) return mixin(obj);
}
/**
* Mixin the emitter properties.
*
* @param {Object} obj
* @return {Object}
* @api private
*/
function mixin(obj) {
for (var key in Emitter.prototype) {
obj[key] = Emitter.prototype[key];
}
return obj;
}
/**
* Listen on the given `event` with `fn`.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.on =
Emitter.prototype.addEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
(this._callbacks['$' + event] = this._callbacks['$' + event] || [])
.push(fn);
return this;
};
/**
* Adds an `event` listener that will be invoked a single
* time then automatically removed.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.once = function(event, fn){
function on() {
this.off(event, on);
fn.apply(this, arguments);
}
on.fn = fn;
this.on(event, on);
return this;
};
/**
* Remove the given callback for `event` or all
* registered callbacks.
*
* @param {String} event
* @param {Function} fn
* @return {Emitter}
* @api public
*/
Emitter.prototype.off =
Emitter.prototype.removeListener =
Emitter.prototype.removeAllListeners =
Emitter.prototype.removeEventListener = function(event, fn){
this._callbacks = this._callbacks || {};
// all
if (0 == arguments.length) {
this._callbacks = {};
return this;
}
// specific event
var callbacks = this._callbacks['$' + event];
if (!callbacks) return this;
// remove all handlers
if (1 == arguments.length) {
delete this._callbacks['$' + event];
return this;
}
// remove specific handler
var cb;
for (var i = 0; i < callbacks.length; i++) {
cb = callbacks[i];
if (cb === fn || cb.fn === fn) {
callbacks.splice(i, 1);
break;
}
}
// Remove event specific arrays for event types that no
// one is subscribed for to avoid memory leak.
if (callbacks.length === 0) {
delete this._callbacks['$' + event];
}
return this;
};
/**
* Emit `event` with the given args.
*
* @param {String} event
* @param {Mixed} ...
* @return {Emitter}
*/
Emitter.prototype.emit = function(event){
this._callbacks = this._callbacks || {};
var args = new Array(arguments.length - 1)
, callbacks = this._callbacks['$' + event];
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
if (callbacks) {
callbacks = callbacks.slice(0);
for (var i = 0, len = callbacks.length; i < len; ++i) {
callbacks[i].apply(this, args);
}
}
return this;
};
// alias used for reserved events (protected method)
Emitter.prototype.emitReserved = Emitter.prototype.emit;
/**
* Return array of callbacks for `event`.
*
* @param {String} event
* @return {Array}
* @api public
*/
Emitter.prototype.listeners = function(event){
this._callbacks = this._callbacks || {};
return this._callbacks['$' + event] || [];
};
/**
* Check if this emitter has `event` handlers.
*
* @param {String} event
* @return {Boolean}
* @api public
*/
Emitter.prototype.hasListeners = function(event){
return !! this.listeners(event).length;
};

View File

@@ -0,0 +1,4 @@
{
"name": "@socket.io/component-emitter",
"type": "module"
}

28
node_modules/@socket.io/component-emitter/package.json generated vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@socket.io/component-emitter",
"description": "Event emitter",
"version": "3.1.2",
"license": "MIT",
"devDependencies": {
"mocha": "*",
"should": "*"
},
"component": {
"scripts": {
"emitter/index.js": "index.js"
}
},
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/cjs/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/socketio/emitter.git"
},
"scripts": {
"test": "make test"
},
"files": [
"lib/"
]
}

21
node_modules/asynckit/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Alex Indigo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

233
node_modules/asynckit/README.md generated vendored Normal file
View File

@@ -0,0 +1,233 @@
# asynckit [![NPM Module](https://img.shields.io/npm/v/asynckit.svg?style=flat)](https://www.npmjs.com/package/asynckit)
Minimal async jobs utility library, with streams support.
[![PhantomJS Build](https://img.shields.io/travis/alexindigo/asynckit/v0.4.0.svg?label=browser&style=flat)](https://travis-ci.org/alexindigo/asynckit)
[![Linux Build](https://img.shields.io/travis/alexindigo/asynckit/v0.4.0.svg?label=linux:0.12-6.x&style=flat)](https://travis-ci.org/alexindigo/asynckit)
[![Windows Build](https://img.shields.io/appveyor/ci/alexindigo/asynckit/v0.4.0.svg?label=windows:0.12-6.x&style=flat)](https://ci.appveyor.com/project/alexindigo/asynckit)
[![Coverage Status](https://img.shields.io/coveralls/alexindigo/asynckit/v0.4.0.svg?label=code+coverage&style=flat)](https://coveralls.io/github/alexindigo/asynckit?branch=master)
[![Dependency Status](https://img.shields.io/david/alexindigo/asynckit/v0.4.0.svg?style=flat)](https://david-dm.org/alexindigo/asynckit)
[![bitHound Overall Score](https://www.bithound.io/github/alexindigo/asynckit/badges/score.svg)](https://www.bithound.io/github/alexindigo/asynckit)
<!-- [![Readme](https://img.shields.io/badge/readme-tested-brightgreen.svg?style=flat)](https://www.npmjs.com/package/reamde) -->
AsyncKit provides harness for `parallel` and `serial` iterators over list of items represented by arrays or objects.
Optionally it accepts abort function (should be synchronously return by iterator for each item), and terminates left over jobs upon an error event. For specific iteration order built-in (`ascending` and `descending`) and custom sort helpers also supported, via `asynckit.serialOrdered` method.
It ensures async operations to keep behavior more stable and prevent `Maximum call stack size exceeded` errors, from sync iterators.
| compression | size |
| :----------------- | -------: |
| asynckit.js | 12.34 kB |
| asynckit.min.js | 4.11 kB |
| asynckit.min.js.gz | 1.47 kB |
## Install
```sh
$ npm install --save asynckit
```
## Examples
### Parallel Jobs
Runs iterator over provided array in parallel. Stores output in the `result` array,
on the matching positions. In unlikely event of an error from one of the jobs,
will terminate rest of the active jobs (if abort function is provided)
and return error along with salvaged data to the main callback function.
#### Input Array
```javascript
var parallel = require('asynckit').parallel
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
, target = []
;
parallel(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// async job accepts one element from the array
// and a callback function
function asyncJob(item, cb)
{
// different delays (in ms) per item
var delay = item * 25;
// pretend different jobs take different time to finish
// and not in consequential order
var timeoutId = setTimeout(function() {
target.push(item);
cb(null, item * 2);
}, delay);
// allow to cancel "leftover" jobs upon error
// return function, invoking of which will abort this job
return clearTimeout.bind(null, timeoutId);
}
```
More examples could be found in [test/test-parallel-array.js](test/test-parallel-array.js).
#### Input Object
Also it supports named jobs, listed via object.
```javascript
var parallel = require('asynckit/parallel')
, assert = require('assert')
;
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
, expectedKeys = [ 'first', 'one', 'two', 'four', 'eight', 'sixteen', 'thirtyTwo', 'sixtyFour' ]
, target = []
, keys = []
;
parallel(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
assert.deepEqual(keys, expectedKeys);
});
// supports full value, key, callback (shortcut) interface
function asyncJob(item, key, cb)
{
// different delays (in ms) per item
var delay = item * 25;
// pretend different jobs take different time to finish
// and not in consequential order
var timeoutId = setTimeout(function() {
keys.push(key);
target.push(item);
cb(null, item * 2);
}, delay);
// allow to cancel "leftover" jobs upon error
// return function, invoking of which will abort this job
return clearTimeout.bind(null, timeoutId);
}
```
More examples could be found in [test/test-parallel-object.js](test/test-parallel-object.js).
### Serial Jobs
Runs iterator over provided array sequentially. Stores output in the `result` array,
on the matching positions. In unlikely event of an error from one of the jobs,
will not proceed to the rest of the items in the list
and return error along with salvaged data to the main callback function.
#### Input Array
```javascript
var serial = require('asynckit/serial')
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
, target = []
;
serial(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// extended interface (item, key, callback)
// also supported for arrays
function asyncJob(item, key, cb)
{
target.push(key);
// it will be automatically made async
// even it iterator "returns" in the same event loop
cb(null, item * 2);
}
```
More examples could be found in [test/test-serial-array.js](test/test-serial-array.js).
#### Input Object
Also it supports named jobs, listed via object.
```javascript
var serial = require('asynckit').serial
, assert = require('assert')
;
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
, target = []
;
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
, expectedTarget = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
, target = []
;
serial(source, asyncJob, function(err, result)
{
assert.deepEqual(result, expectedResult);
assert.deepEqual(target, expectedTarget);
});
// shortcut interface (item, callback)
// works for object as well as for the arrays
function asyncJob(item, cb)
{
target.push(item);
// it will be automatically made async
// even it iterator "returns" in the same event loop
cb(null, item * 2);
}
```
More examples could be found in [test/test-serial-object.js](test/test-serial-object.js).
_Note: Since _object_ is an _unordered_ collection of properties,
it may produce unexpected results with sequential iterations.
Whenever order of the jobs' execution is important please use `serialOrdered` method._
### Ordered Serial Iterations
TBD
For example [compare-property](compare-property) package.
### Streaming interface
TBD
## Want to Know More?
More examples can be found in [test folder](test/).
Or open an [issue](https://github.com/alexindigo/asynckit/issues) with questions and/or suggestions.
## License
AsyncKit is licensed under the MIT license.

76
node_modules/asynckit/bench.js generated vendored Normal file
View File

@@ -0,0 +1,76 @@
/* eslint no-console: "off" */
var asynckit = require('./')
, async = require('async')
, assert = require('assert')
, expected = 0
;
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var source = [];
for (var z = 1; z < 100; z++)
{
source.push(z);
expected += z;
}
suite
// add tests
.add('async.map', function(deferred)
{
var total = 0;
async.map(source,
function(i, cb)
{
setImmediate(function()
{
total += i;
cb(null, total);
});
},
function(err, result)
{
assert.ifError(err);
assert.equal(result[result.length - 1], expected);
deferred.resolve();
});
}, {'defer': true})
.add('asynckit.parallel', function(deferred)
{
var total = 0;
asynckit.parallel(source,
function(i, cb)
{
setImmediate(function()
{
total += i;
cb(null, total);
});
},
function(err, result)
{
assert.ifError(err);
assert.equal(result[result.length - 1], expected);
deferred.resolve();
});
}, {'defer': true})
// add listeners
.on('cycle', function(ev)
{
console.log(String(ev.target));
})
.on('complete', function()
{
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ 'async': true });

6
node_modules/asynckit/index.js generated vendored Normal file
View File

@@ -0,0 +1,6 @@
module.exports =
{
parallel : require('./parallel.js'),
serial : require('./serial.js'),
serialOrdered : require('./serialOrdered.js')
};

29
node_modules/asynckit/lib/abort.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
// API
module.exports = abort;
/**
* Aborts leftover active jobs
*
* @param {object} state - current state object
*/
function abort(state)
{
Object.keys(state.jobs).forEach(clean.bind(state));
// reset leftover jobs
state.jobs = {};
}
/**
* Cleans up leftover job by invoking abort function for the provided job id
*
* @this state
* @param {string|number} key - job id to abort
*/
function clean(key)
{
if (typeof this.jobs[key] == 'function')
{
this.jobs[key]();
}
}

34
node_modules/asynckit/lib/async.js generated vendored Normal file
View File

@@ -0,0 +1,34 @@
var defer = require('./defer.js');
// API
module.exports = async;
/**
* Runs provided callback asynchronously
* even if callback itself is not
*
* @param {function} callback - callback to invoke
* @returns {function} - augmented callback
*/
function async(callback)
{
var isAsync = false;
// check if async happened
defer(function() { isAsync = true; });
return function async_callback(err, result)
{
if (isAsync)
{
callback(err, result);
}
else
{
defer(function nextTick_callback()
{
callback(err, result);
});
}
};
}

26
node_modules/asynckit/lib/defer.js generated vendored Normal file
View File

@@ -0,0 +1,26 @@
module.exports = defer;
/**
* Runs provided function on next iteration of the event loop
*
* @param {function} fn - function to run
*/
function defer(fn)
{
var nextTick = typeof setImmediate == 'function'
? setImmediate
: (
typeof process == 'object' && typeof process.nextTick == 'function'
? process.nextTick
: null
);
if (nextTick)
{
nextTick(fn);
}
else
{
setTimeout(fn, 0);
}
}

75
node_modules/asynckit/lib/iterate.js generated vendored Normal file
View File

@@ -0,0 +1,75 @@
var async = require('./async.js')
, abort = require('./abort.js')
;
// API
module.exports = iterate;
/**
* Iterates over each job object
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {object} state - current job status
* @param {function} callback - invoked when all elements processed
*/
function iterate(list, iterator, state, callback)
{
// store current index
var key = state['keyedList'] ? state['keyedList'][state.index] : state.index;
state.jobs[key] = runJob(iterator, key, list[key], function(error, output)
{
// don't repeat yourself
// skip secondary callbacks
if (!(key in state.jobs))
{
return;
}
// clean up jobs
delete state.jobs[key];
if (error)
{
// don't process rest of the results
// stop still active jobs
// and reset the list
abort(state);
}
else
{
state.results[key] = output;
}
// return salvaged results
callback(error, state.results);
});
}
/**
* Runs iterator over provided job element
*
* @param {function} iterator - iterator to invoke
* @param {string|number} key - key/index of the element in the list of jobs
* @param {mixed} item - job description
* @param {function} callback - invoked after iterator is done with the job
* @returns {function|mixed} - job abort function or something else
*/
function runJob(iterator, key, item, callback)
{
var aborter;
// allow shortcut if iterator expects only two arguments
if (iterator.length == 2)
{
aborter = iterator(item, async(callback));
}
// otherwise go with full three arguments
else
{
aborter = iterator(item, key, async(callback));
}
return aborter;
}

91
node_modules/asynckit/lib/readable_asynckit.js generated vendored Normal file
View File

@@ -0,0 +1,91 @@
var streamify = require('./streamify.js')
, defer = require('./defer.js')
;
// API
module.exports = ReadableAsyncKit;
/**
* Base constructor for all streams
* used to hold properties/methods
*/
function ReadableAsyncKit()
{
ReadableAsyncKit.super_.apply(this, arguments);
// list of active jobs
this.jobs = {};
// add stream methods
this.destroy = destroy;
this._start = _start;
this._read = _read;
}
/**
* Destroys readable stream,
* by aborting outstanding jobs
*
* @returns {void}
*/
function destroy()
{
if (this.destroyed)
{
return;
}
this.destroyed = true;
if (typeof this.terminator == 'function')
{
this.terminator();
}
}
/**
* Starts provided jobs in async manner
*
* @private
*/
function _start()
{
// first argument runner function
var runner = arguments[0]
// take away first argument
, args = Array.prototype.slice.call(arguments, 1)
// second argument - input data
, input = args[0]
// last argument - result callback
, endCb = streamify.callback.call(this, args[args.length - 1])
;
args[args.length - 1] = endCb;
// third argument - iterator
args[1] = streamify.iterator.call(this, args[1]);
// allow time for proper setup
defer(function()
{
if (!this.destroyed)
{
this.terminator = runner.apply(null, args);
}
else
{
endCb(null, Array.isArray(input) ? [] : {});
}
}.bind(this));
}
/**
* Implement _read to comply with Readable streams
* Doesn't really make sense for flowing object mode
*
* @private
*/
function _read()
{
}

25
node_modules/asynckit/lib/readable_parallel.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
var parallel = require('../parallel.js');
// API
module.exports = ReadableParallel;
/**
* Streaming wrapper to `asynckit.parallel`
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} callback - invoked when all elements processed
* @returns {stream.Readable#}
*/
function ReadableParallel(list, iterator, callback)
{
if (!(this instanceof ReadableParallel))
{
return new ReadableParallel(list, iterator, callback);
}
// turn on object mode
ReadableParallel.super_.call(this, {objectMode: true});
this._start(parallel, list, iterator, callback);
}

25
node_modules/asynckit/lib/readable_serial.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
var serial = require('../serial.js');
// API
module.exports = ReadableSerial;
/**
* Streaming wrapper to `asynckit.serial`
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} callback - invoked when all elements processed
* @returns {stream.Readable#}
*/
function ReadableSerial(list, iterator, callback)
{
if (!(this instanceof ReadableSerial))
{
return new ReadableSerial(list, iterator, callback);
}
// turn on object mode
ReadableSerial.super_.call(this, {objectMode: true});
this._start(serial, list, iterator, callback);
}

29
node_modules/asynckit/lib/readable_serial_ordered.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
var serialOrdered = require('../serialOrdered.js');
// API
module.exports = ReadableSerialOrdered;
// expose sort helpers
module.exports.ascending = serialOrdered.ascending;
module.exports.descending = serialOrdered.descending;
/**
* Streaming wrapper to `asynckit.serialOrdered`
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} sortMethod - custom sort function
* @param {function} callback - invoked when all elements processed
* @returns {stream.Readable#}
*/
function ReadableSerialOrdered(list, iterator, sortMethod, callback)
{
if (!(this instanceof ReadableSerialOrdered))
{
return new ReadableSerialOrdered(list, iterator, sortMethod, callback);
}
// turn on object mode
ReadableSerialOrdered.super_.call(this, {objectMode: true});
this._start(serialOrdered, list, iterator, sortMethod, callback);
}

37
node_modules/asynckit/lib/state.js generated vendored Normal file
View File

@@ -0,0 +1,37 @@
// API
module.exports = state;
/**
* Creates initial state object
* for iteration over list
*
* @param {array|object} list - list to iterate over
* @param {function|null} sortMethod - function to use for keys sort,
* or `null` to keep them as is
* @returns {object} - initial state object
*/
function state(list, sortMethod)
{
var isNamedList = !Array.isArray(list)
, initState =
{
index : 0,
keyedList: isNamedList || sortMethod ? Object.keys(list) : null,
jobs : {},
results : isNamedList ? {} : [],
size : isNamedList ? Object.keys(list).length : list.length
}
;
if (sortMethod)
{
// sort array keys based on it's values
// sort object's keys just on own merit
initState.keyedList.sort(isNamedList ? sortMethod : function(a, b)
{
return sortMethod(list[a], list[b]);
});
}
return initState;
}

141
node_modules/asynckit/lib/streamify.js generated vendored Normal file
View File

@@ -0,0 +1,141 @@
var async = require('./async.js');
// API
module.exports = {
iterator: wrapIterator,
callback: wrapCallback
};
/**
* Wraps iterators with long signature
*
* @this ReadableAsyncKit#
* @param {function} iterator - function to wrap
* @returns {function} - wrapped function
*/
function wrapIterator(iterator)
{
var stream = this;
return function(item, key, cb)
{
var aborter
, wrappedCb = async(wrapIteratorCallback.call(stream, cb, key))
;
stream.jobs[key] = wrappedCb;
// it's either shortcut (item, cb)
if (iterator.length == 2)
{
aborter = iterator(item, wrappedCb);
}
// or long format (item, key, cb)
else
{
aborter = iterator(item, key, wrappedCb);
}
return aborter;
};
}
/**
* Wraps provided callback function
* allowing to execute snitch function before
* real callback
*
* @this ReadableAsyncKit#
* @param {function} callback - function to wrap
* @returns {function} - wrapped function
*/
function wrapCallback(callback)
{
var stream = this;
var wrapped = function(error, result)
{
return finisher.call(stream, error, result, callback);
};
return wrapped;
}
/**
* Wraps provided iterator callback function
* makes sure snitch only called once,
* but passes secondary calls to the original callback
*
* @this ReadableAsyncKit#
* @param {function} callback - callback to wrap
* @param {number|string} key - iteration key
* @returns {function} wrapped callback
*/
function wrapIteratorCallback(callback, key)
{
var stream = this;
return function(error, output)
{
// don't repeat yourself
if (!(key in stream.jobs))
{
callback(error, output);
return;
}
// clean up jobs
delete stream.jobs[key];
return streamer.call(stream, error, {key: key, value: output}, callback);
};
}
/**
* Stream wrapper for iterator callback
*
* @this ReadableAsyncKit#
* @param {mixed} error - error response
* @param {mixed} output - iterator output
* @param {function} callback - callback that expects iterator results
*/
function streamer(error, output, callback)
{
if (error && !this.error)
{
this.error = error;
this.pause();
this.emit('error', error);
// send back value only, as expected
callback(error, output && output.value);
return;
}
// stream stuff
this.push(output);
// back to original track
// send back value only, as expected
callback(error, output && output.value);
}
/**
* Stream wrapper for finishing callback
*
* @this ReadableAsyncKit#
* @param {mixed} error - error response
* @param {mixed} output - iterator output
* @param {function} callback - callback that expects final results
*/
function finisher(error, output, callback)
{
// signal end of the stream
// only for successfully finished streams
if (!error)
{
this.push(null);
}
// back to original track
callback(error, output);
}

29
node_modules/asynckit/lib/terminator.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
var abort = require('./abort.js')
, async = require('./async.js')
;
// API
module.exports = terminator;
/**
* Terminates jobs in the attached state context
*
* @this AsyncKitState#
* @param {function} callback - final callback to invoke after termination
*/
function terminator(callback)
{
if (!Object.keys(this.jobs).length)
{
return;
}
// fast forward iteration index
this.index = this.size;
// abort jobs
abort(this);
// send back results we have so far
async(callback)(null, this.results);
}

63
node_modules/asynckit/package.json generated vendored Normal file
View File

@@ -0,0 +1,63 @@
{
"name": "asynckit",
"version": "0.4.0",
"description": "Minimal async jobs utility library, with streams support",
"main": "index.js",
"scripts": {
"clean": "rimraf coverage",
"lint": "eslint *.js lib/*.js test/*.js",
"test": "istanbul cover --reporter=json tape -- 'test/test-*.js' | tap-spec",
"win-test": "tape test/test-*.js",
"browser": "browserify -t browserify-istanbul test/lib/browserify_adjustment.js test/test-*.js | obake --coverage | tap-spec",
"report": "istanbul report",
"size": "browserify index.js | size-table asynckit",
"debug": "tape test/test-*.js"
},
"pre-commit": [
"clean",
"lint",
"test",
"browser",
"report",
"size"
],
"repository": {
"type": "git",
"url": "git+https://github.com/alexindigo/asynckit.git"
},
"keywords": [
"async",
"jobs",
"parallel",
"serial",
"iterator",
"array",
"object",
"stream",
"destroy",
"terminate",
"abort"
],
"author": "Alex Indigo <iam@alexindigo.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/alexindigo/asynckit/issues"
},
"homepage": "https://github.com/alexindigo/asynckit#readme",
"devDependencies": {
"browserify": "^13.0.0",
"browserify-istanbul": "^2.0.0",
"coveralls": "^2.11.9",
"eslint": "^2.9.0",
"istanbul": "^0.4.3",
"obake": "^0.1.2",
"phantomjs-prebuilt": "^2.1.7",
"pre-commit": "^1.1.3",
"reamde": "^1.1.0",
"rimraf": "^2.5.2",
"size-table": "^0.2.0",
"tap-spec": "^4.1.1",
"tape": "^4.5.1"
},
"dependencies": {}
}

43
node_modules/asynckit/parallel.js generated vendored Normal file
View File

@@ -0,0 +1,43 @@
var iterate = require('./lib/iterate.js')
, initState = require('./lib/state.js')
, terminator = require('./lib/terminator.js')
;
// Public API
module.exports = parallel;
/**
* Runs iterator over provided array elements in parallel
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} callback - invoked when all elements processed
* @returns {function} - jobs terminator
*/
function parallel(list, iterator, callback)
{
var state = initState(list);
while (state.index < (state['keyedList'] || list).length)
{
iterate(list, iterator, state, function(error, result)
{
if (error)
{
callback(error, result);
return;
}
// looks like it's the last one
if (Object.keys(state.jobs).length === 0)
{
callback(null, state.results);
return;
}
});
state.index++;
}
return terminator.bind(state, callback);
}

17
node_modules/asynckit/serial.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
var serialOrdered = require('./serialOrdered.js');
// Public API
module.exports = serial;
/**
* Runs iterator over provided array elements in series
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} callback - invoked when all elements processed
* @returns {function} - jobs terminator
*/
function serial(list, iterator, callback)
{
return serialOrdered(list, iterator, null, callback);
}

75
node_modules/asynckit/serialOrdered.js generated vendored Normal file
View File

@@ -0,0 +1,75 @@
var iterate = require('./lib/iterate.js')
, initState = require('./lib/state.js')
, terminator = require('./lib/terminator.js')
;
// Public API
module.exports = serialOrdered;
// sorting helpers
module.exports.ascending = ascending;
module.exports.descending = descending;
/**
* Runs iterator over provided sorted array elements in series
*
* @param {array|object} list - array or object (named list) to iterate over
* @param {function} iterator - iterator to run
* @param {function} sortMethod - custom sort function
* @param {function} callback - invoked when all elements processed
* @returns {function} - jobs terminator
*/
function serialOrdered(list, iterator, sortMethod, callback)
{
var state = initState(list, sortMethod);
iterate(list, iterator, state, function iteratorHandler(error, result)
{
if (error)
{
callback(error, result);
return;
}
state.index++;
// are we there yet?
if (state.index < (state['keyedList'] || list).length)
{
iterate(list, iterator, state, iteratorHandler);
return;
}
// done here
callback(null, state.results);
});
return terminator.bind(state, callback);
}
/*
* -- Sort methods
*/
/**
* sort helper to sort array elements in ascending order
*
* @param {mixed} a - an item to compare
* @param {mixed} b - an item to compare
* @returns {number} - comparison result
*/
function ascending(a, b)
{
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* sort helper to sort array elements in descending order
*
* @param {mixed} a - an item to compare
* @param {mixed} b - an item to compare
* @returns {number} - comparison result
*/
function descending(a, b)
{
return -1 * ascending(a, b);
}

21
node_modules/asynckit/stream.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
var inherits = require('util').inherits
, Readable = require('stream').Readable
, ReadableAsyncKit = require('./lib/readable_asynckit.js')
, ReadableParallel = require('./lib/readable_parallel.js')
, ReadableSerial = require('./lib/readable_serial.js')
, ReadableSerialOrdered = require('./lib/readable_serial_ordered.js')
;
// API
module.exports =
{
parallel : ReadableParallel,
serial : ReadableSerial,
serialOrdered : ReadableSerialOrdered,
};
inherits(ReadableAsyncKit, Readable);
inherits(ReadableParallel, ReadableAsyncKit);
inherits(ReadableSerial, ReadableAsyncKit);
inherits(ReadableSerialOrdered, ReadableAsyncKit);

1295
node_modules/axios/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

7
node_modules/axios/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,7 @@
# Copyright (c) 2014-present Matt Zabriskie & Collaborators
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
node_modules/axios/MIGRATION_GUIDE.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Migration Guide
## 0.x.x -> 1.1.0

1756
node_modules/axios/README.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

4396
node_modules/axios/dist/axios.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/axios/dist/axios.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

3
node_modules/axios/dist/axios.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/axios/dist/axios.min.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

3840
node_modules/axios/dist/browser/axios.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/axios/dist/browser/axios.cjs.map generated vendored Normal file

File diff suppressed because one or more lines are too long

3863
node_modules/axios/dist/esm/axios.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/axios/dist/esm/axios.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

3
node_modules/axios/dist/esm/axios.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/axios/dist/esm/axios.min.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

4988
node_modules/axios/dist/node/axios.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/axios/dist/node/axios.cjs.map generated vendored Normal file

File diff suppressed because one or more lines are too long

568
node_modules/axios/index.d.cts generated vendored Normal file
View File

@@ -0,0 +1,568 @@
interface RawAxiosHeaders {
[key: string]: axios.AxiosHeaderValue;
}
type MethodsHeaders = Partial<{
[Key in axios.Method as Lowercase<Key>]: AxiosHeaders;
} & {common: AxiosHeaders}>;
type AxiosHeaderMatcher = string | RegExp | ((this: AxiosHeaders, value: string, name: string) => boolean);
type AxiosHeaderParser = (this: AxiosHeaders, value: axios.AxiosHeaderValue, header: string) => any;
type CommonRequestHeadersList = 'Accept' | 'Content-Length' | 'User-Agent'| 'Content-Encoding' | 'Authorization';
type ContentType = axios.AxiosHeaderValue | 'text/html' | 'text/plain' | 'multipart/form-data' | 'application/json' | 'application/x-www-form-urlencoded' | 'application/octet-stream';
type CommonResponseHeadersList = 'Server' | 'Content-Type' | 'Content-Length' | 'Cache-Control'| 'Content-Encoding';
type BrowserProgressEvent = any;
declare class AxiosHeaders {
constructor(
headers?: RawAxiosHeaders | AxiosHeaders | string
);
[key: string]: any;
set(headerName?: string, value?: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): axios.AxiosHeaderValue;
has(header: string, matcher?: AxiosHeaderMatcher): boolean;
delete(header: string | string[], matcher?: AxiosHeaderMatcher): boolean;
clear(matcher?: AxiosHeaderMatcher): boolean;
normalize(format: boolean): AxiosHeaders;
concat(...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>): AxiosHeaders;
toJSON(asStrings?: boolean): RawAxiosHeaders;
static from(thing?: AxiosHeaders | RawAxiosHeaders | string): AxiosHeaders;
static accessor(header: string | string[]): AxiosHeaders;
static concat(...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>): AxiosHeaders;
setContentType(value: ContentType, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentType(parser?: RegExp): RegExpExecArray | null;
getContentType(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasContentType(matcher?: AxiosHeaderMatcher): boolean;
setContentLength(value: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentLength(parser?: RegExp): RegExpExecArray | null;
getContentLength(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasContentLength(matcher?: AxiosHeaderMatcher): boolean;
setAccept(value: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAccept(parser?: RegExp): RegExpExecArray | null;
getAccept(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasAccept(matcher?: AxiosHeaderMatcher): boolean;
setUserAgent(value: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getUserAgent(parser?: RegExp): RegExpExecArray | null;
getUserAgent(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasUserAgent(matcher?: AxiosHeaderMatcher): boolean;
setContentEncoding(value: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentEncoding(parser?: RegExp): RegExpExecArray | null;
getContentEncoding(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasContentEncoding(matcher?: AxiosHeaderMatcher): boolean;
setAuthorization(value: axios.AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAuthorization(parser?: RegExp): RegExpExecArray | null;
getAuthorization(matcher?: AxiosHeaderMatcher): axios.AxiosHeaderValue;
hasAuthorization(matcher?: AxiosHeaderMatcher): boolean;
getSetCookie(): string[];
[Symbol.iterator](): IterableIterator<[string, axios.AxiosHeaderValue]>;
}
declare class AxiosError<T = unknown, D = any> extends Error {
constructor(
message?: string,
code?: string,
config?: axios.InternalAxiosRequestConfig<D>,
request?: any,
response?: axios.AxiosResponse<T, D>
);
config?: axios.InternalAxiosRequestConfig<D>;
code?: string;
request?: any;
response?: axios.AxiosResponse<T, D>;
isAxiosError: boolean;
status?: number;
toJSON: () => object;
cause?: unknown;
event?: BrowserProgressEvent;
static from<T = unknown, D = any>(
error: Error | unknown,
code?: string,
config?: axios.InternalAxiosRequestConfig<D>,
request?: any,
response?: axios.AxiosResponse<T, D>,
customProps?: object,
): AxiosError<T, D>;
static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION";
static readonly ERR_NETWORK = "ERR_NETWORK";
static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
static readonly ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
static readonly ERR_INVALID_URL = "ERR_INVALID_URL";
static readonly ERR_CANCELED = "ERR_CANCELED";
static readonly ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT";
}
declare class CanceledError<T> extends AxiosError<T> {
}
declare class Axios {
constructor(config?: axios.AxiosRequestConfig);
defaults: axios.AxiosDefaults;
interceptors: {
request: axios.AxiosInterceptorManager<axios.InternalAxiosRequestConfig>;
response: axios.AxiosInterceptorManager<axios.AxiosResponse>;
};
getUri(config?: axios.AxiosRequestConfig): string;
request<T = any, R = axios.AxiosResponse<T>, D = any>(config: axios.AxiosRequestConfig<D>): Promise<R>;
get<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, config?: axios.AxiosRequestConfig<D>): Promise<R>;
delete<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, config?: axios.AxiosRequestConfig<D>): Promise<R>;
head<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, config?: axios.AxiosRequestConfig<D>): Promise<R>;
options<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, config?: axios.AxiosRequestConfig<D>): Promise<R>;
post<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
put<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
patch<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
postForm<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
putForm<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
patchForm<T = any, R = axios.AxiosResponse<T>, D = any>(url: string, data?: D, config?: axios.AxiosRequestConfig<D>): Promise<R>;
}
declare enum HttpStatusCode {
Continue = 100,
SwitchingProtocols = 101,
Processing = 102,
EarlyHints = 103,
Ok = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultiStatus = 207,
AlreadyReported = 208,
ImUsed = 226,
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
Unused = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
PayloadTooLarge = 413,
UriTooLong = 414,
UnsupportedMediaType = 415,
RangeNotSatisfiable = 416,
ExpectationFailed = 417,
ImATeapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
FailedDependency = 424,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
}
type InternalAxiosError<T = unknown, D = any> = AxiosError<T, D>;
declare namespace axios {
type AxiosError<T = unknown, D = any> = InternalAxiosError<T, D>;
type RawAxiosRequestHeaders = Partial<RawAxiosHeaders & {
[Key in CommonRequestHeadersList]: AxiosHeaderValue;
} & {
'Content-Type': ContentType
}>;
type AxiosRequestHeaders = RawAxiosRequestHeaders & AxiosHeaders;
type AxiosHeaderValue = AxiosHeaders | string | string[] | number | boolean | null;
type RawCommonResponseHeaders = {
[Key in CommonResponseHeadersList]: AxiosHeaderValue;
} & {
"set-cookie": string[];
};
type RawAxiosResponseHeaders = Partial<RawAxiosHeaders & RawCommonResponseHeaders>;
type AxiosResponseHeaders = RawAxiosResponseHeaders & AxiosHeaders;
interface AxiosRequestTransformer {
(this: InternalAxiosRequestConfig, data: any, headers: AxiosRequestHeaders): any;
}
interface AxiosResponseTransformer {
(this: InternalAxiosRequestConfig, data: any, headers: AxiosResponseHeaders, status?: number): any;
}
interface AxiosAdapter {
(config: InternalAxiosRequestConfig): AxiosPromise;
}
interface AxiosBasicCredentials {
username: string;
password: string;
}
interface AxiosProxyConfig {
host: string;
port: number;
auth?: AxiosBasicCredentials;
protocol?: string;
}
type Method =
| 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
| 'purge' | 'PURGE'
| 'link' | 'LINK'
| 'unlink' | 'UNLINK';
type ResponseType =
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream'
| 'formdata';
type responseEncoding =
| 'ascii' | 'ASCII'
| 'ansi' | 'ANSI'
| 'binary' | 'BINARY'
| 'base64' | 'BASE64'
| 'base64url' | 'BASE64URL'
| 'hex' | 'HEX'
| 'latin1' | 'LATIN1'
| 'ucs-2' | 'UCS-2'
| 'ucs2' | 'UCS2'
| 'utf-8' | 'UTF-8'
| 'utf8' | 'UTF8'
| 'utf16le' | 'UTF16LE';
interface TransitionalOptions {
silentJSONParsing?: boolean;
forcedJSONParsing?: boolean;
clarifyTimeoutError?: boolean;
}
interface GenericAbortSignal {
readonly aborted: boolean;
onabort?: ((...args: any) => any) | null;
addEventListener?: (...args: any) => any;
removeEventListener?: (...args: any) => any;
}
interface FormDataVisitorHelpers {
defaultVisitor: SerializerVisitor;
convertValue: (value: any) => any;
isVisitable: (value: any) => boolean;
}
interface SerializerVisitor {
(
this: GenericFormData,
value: any,
key: string | number,
path: null | Array<string | number>,
helpers: FormDataVisitorHelpers
): boolean;
}
interface SerializerOptions {
visitor?: SerializerVisitor;
dots?: boolean;
metaTokens?: boolean;
indexes?: boolean | null;
}
// tslint:disable-next-line
interface FormSerializerOptions extends SerializerOptions {
}
interface ParamEncoder {
(value: any, defaultEncoder: (value: any) => any): any;
}
interface CustomParamsSerializer {
(params: Record<string, any>, options?: ParamsSerializerOptions): string;
}
interface ParamsSerializerOptions extends SerializerOptions {
encode?: ParamEncoder;
serialize?: CustomParamsSerializer;
}
type MaxUploadRate = number;
type MaxDownloadRate = number;
interface AxiosProgressEvent {
loaded: number;
total?: number;
progress?: number;
bytes: number;
rate?: number;
estimated?: number;
upload?: boolean;
download?: boolean;
event?: BrowserProgressEvent;
lengthComputable: boolean;
}
type Milliseconds = number;
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | (string & {});
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
type AddressFamily = 4 | 6 | undefined;
interface LookupAddressEntry {
address: string;
family?: AddressFamily;
}
type LookupAddress = string | LookupAddressEntry;
interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
allowAbsoluteUrls?: boolean;
transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
params?: any;
paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
data?: D;
timeout?: Milliseconds;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
responseEncoding?: responseEncoding | string;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
maxRate?: number | [MaxUploadRate, MaxDownloadRate];
beforeRedirect?: (options: Record<string, any>, responseDetails: {headers: Record<string, string>, statusCode: HttpStatusCode}) => void;
socketPath?: string | null;
transport?: any;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions;
signal?: GenericAbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
fetch?: (input: URL | Request | string, init?: RequestInit) => Promise<Response>;
Request?: new (input: URL | Request | string, init?: RequestInit) => Request;
Response?: new (
body?: ArrayBuffer | ArrayBufferView | Blob | FormData | URLSearchParams | string | null,
init?: ResponseInit
) => Response;
};
formSerializer?: FormSerializerOptions;
family?: AddressFamily;
lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) |
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
}
// Alias
type RawAxiosRequestConfig<D = any> = AxiosRequestConfig<D>;
interface InternalAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
headers: AxiosRequestHeaders;
}
interface HeadersDefaults {
common: RawAxiosRequestHeaders;
delete: RawAxiosRequestHeaders;
get: RawAxiosRequestHeaders;
head: RawAxiosRequestHeaders;
post: RawAxiosRequestHeaders;
put: RawAxiosRequestHeaders;
patch: RawAxiosRequestHeaders;
options?: RawAxiosRequestHeaders;
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
}
interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers: HeadersDefaults;
}
interface CreateAxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers?: RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>;
}
interface AxiosResponse<T = any, D = any, H = {}> {
data: T;
status: number;
statusText: string;
headers: H & RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}
type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
interface CancelStatic {
new (message?: string): Cancel;
}
interface Cancel {
message: string | undefined;
}
interface Canceler {
(message?: string, config?: AxiosRequestConfig, request?: any): void;
}
interface CancelTokenStatic {
new (executor: (cancel: Canceler) => void): CancelToken;
source(): CancelTokenSource;
}
interface CancelToken {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
}
interface CancelTokenSource {
token: CancelToken;
cancel: Canceler;
}
interface AxiosInterceptorOptions {
synchronous?: boolean;
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
}
type AxiosRequestInterceptorUse<T> = (onFulfilled?: ((value: T) => T | Promise<T>) | null, onRejected?: ((error: any) => any) | null, options?: AxiosInterceptorOptions) => number;
type AxiosResponseInterceptorUse<T> = (onFulfilled?: ((value: T) => T | Promise<T>) | null, onRejected?: ((error: any) => any) | null) => number;
interface AxiosInterceptorManager<V> {
use: V extends AxiosResponse ? AxiosResponseInterceptorUse<V> : AxiosRequestInterceptorUse<V>;
eject(id: number): void;
clear(): void;
}
interface AxiosInstance extends Axios {
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
create(config?: CreateAxiosDefaults): AxiosInstance;
defaults: Omit<AxiosDefaults, 'headers'> & {
headers: HeadersDefaults & {
[key: string]: AxiosHeaderValue
}
};
}
interface GenericFormData {
append(name: string, value: any, options?: any): any;
}
interface GenericHTMLFormElement {
name: string;
method: string;
submit(): void;
}
interface AxiosStatic extends AxiosInstance {
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
CanceledError: typeof CanceledError;
HttpStatusCode: typeof HttpStatusCode;
readonly VERSION: string;
isCancel(value: any): value is Cancel;
all<T>(values: Array<T | Promise<T>>): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>;
toFormData(sourceObj: object, targetFormData?: GenericFormData, options?: FormSerializerOptions): GenericFormData;
formToJSON(form: GenericFormData|GenericHTMLFormElement): object;
getAdapter(adapters: AxiosAdapterConfig | AxiosAdapterConfig[] | undefined): AxiosAdapter;
AxiosHeaders: typeof AxiosHeaders;
mergeConfig<D = any>(config1: AxiosRequestConfig<D>, config2: AxiosRequestConfig<D>): AxiosRequestConfig<D>;
}
}
declare const axios: axios.AxiosStatic;
export = axios;

581
node_modules/axios/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,581 @@
// TypeScript Version: 4.7
export type AxiosHeaderValue = AxiosHeaders | string | string[] | number | boolean | null;
interface RawAxiosHeaders {
[key: string]: AxiosHeaderValue;
}
type MethodsHeaders = Partial<{
[Key in Method as Lowercase<Key>]: AxiosHeaders;
} & {common: AxiosHeaders}>;
type AxiosHeaderMatcher = string | RegExp | ((this: AxiosHeaders, value: string, name: string) => boolean);
type AxiosHeaderParser = (this: AxiosHeaders, value: AxiosHeaderValue, header: string) => any;
export class AxiosHeaders {
constructor(
headers?: RawAxiosHeaders | AxiosHeaders | string
);
[key: string]: any;
set(headerName?: string, value?: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): AxiosHeaderValue;
has(header: string, matcher?: AxiosHeaderMatcher): boolean;
delete(header: string | string[], matcher?: AxiosHeaderMatcher): boolean;
clear(matcher?: AxiosHeaderMatcher): boolean;
normalize(format: boolean): AxiosHeaders;
concat(...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>): AxiosHeaders;
toJSON(asStrings?: boolean): RawAxiosHeaders;
static from(thing?: AxiosHeaders | RawAxiosHeaders | string): AxiosHeaders;
static accessor(header: string | string[]): AxiosHeaders;
static concat(...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>): AxiosHeaders;
setContentType(value: ContentType, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentType(parser?: RegExp): RegExpExecArray | null;
getContentType(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentType(matcher?: AxiosHeaderMatcher): boolean;
setContentLength(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentLength(parser?: RegExp): RegExpExecArray | null;
getContentLength(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentLength(matcher?: AxiosHeaderMatcher): boolean;
setAccept(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAccept(parser?: RegExp): RegExpExecArray | null;
getAccept(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAccept(matcher?: AxiosHeaderMatcher): boolean;
setUserAgent(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getUserAgent(parser?: RegExp): RegExpExecArray | null;
getUserAgent(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasUserAgent(matcher?: AxiosHeaderMatcher): boolean;
setContentEncoding(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentEncoding(parser?: RegExp): RegExpExecArray | null;
getContentEncoding(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentEncoding(matcher?: AxiosHeaderMatcher): boolean;
setAuthorization(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAuthorization(parser?: RegExp): RegExpExecArray | null;
getAuthorization(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAuthorization(matcher?: AxiosHeaderMatcher): boolean;
getSetCookie(): string[];
[Symbol.iterator](): IterableIterator<[string, AxiosHeaderValue]>;
}
type CommonRequestHeadersList = 'Accept' | 'Content-Length' | 'User-Agent' | 'Content-Encoding' | 'Authorization';
type ContentType = AxiosHeaderValue | 'text/html' | 'text/plain' | 'multipart/form-data' | 'application/json' | 'application/x-www-form-urlencoded' | 'application/octet-stream';
export type RawAxiosRequestHeaders = Partial<RawAxiosHeaders & {
[Key in CommonRequestHeadersList]: AxiosHeaderValue;
} & {
'Content-Type': ContentType
}>;
export type AxiosRequestHeaders = RawAxiosRequestHeaders & AxiosHeaders;
type CommonResponseHeadersList = 'Server' | 'Content-Type' | 'Content-Length' | 'Cache-Control'| 'Content-Encoding';
type RawCommonResponseHeaders = {
[Key in CommonResponseHeadersList]: AxiosHeaderValue;
} & {
"set-cookie": string[];
};
export type RawAxiosResponseHeaders = Partial<RawAxiosHeaders & RawCommonResponseHeaders>;
export type AxiosResponseHeaders = RawAxiosResponseHeaders & AxiosHeaders;
export interface AxiosRequestTransformer {
(this: InternalAxiosRequestConfig, data: any, headers: AxiosRequestHeaders): any;
}
export interface AxiosResponseTransformer {
(this: InternalAxiosRequestConfig, data: any, headers: AxiosResponseHeaders, status?: number): any;
}
export interface AxiosAdapter {
(config: InternalAxiosRequestConfig): AxiosPromise;
}
export interface AxiosBasicCredentials {
username: string;
password: string;
}
export interface AxiosProxyConfig {
host: string;
port: number;
auth?: AxiosBasicCredentials;
protocol?: string;
}
export enum HttpStatusCode {
Continue = 100,
SwitchingProtocols = 101,
Processing = 102,
EarlyHints = 103,
Ok = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultiStatus = 207,
AlreadyReported = 208,
ImUsed = 226,
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
Unused = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthenticationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
PayloadTooLarge = 413,
UriTooLong = 414,
UnsupportedMediaType = 415,
RangeNotSatisfiable = 416,
ExpectationFailed = 417,
ImATeapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
FailedDependency = 424,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
}
export type Method =
| 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
| 'purge' | 'PURGE'
| 'link' | 'LINK'
| 'unlink' | 'UNLINK';
export type ResponseType =
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream'
| 'formdata';
export type responseEncoding =
| 'ascii' | 'ASCII'
| 'ansi' | 'ANSI'
| 'binary' | 'BINARY'
| 'base64' | 'BASE64'
| 'base64url' | 'BASE64URL'
| 'hex' | 'HEX'
| 'latin1' | 'LATIN1'
| 'ucs-2' | 'UCS-2'
| 'ucs2' | 'UCS2'
| 'utf-8' | 'UTF-8'
| 'utf8' | 'UTF8'
| 'utf16le' | 'UTF16LE';
export interface TransitionalOptions {
silentJSONParsing?: boolean;
forcedJSONParsing?: boolean;
clarifyTimeoutError?: boolean;
}
export interface GenericAbortSignal {
readonly aborted: boolean;
onabort?: ((...args: any) => any) | null;
addEventListener?: (...args: any) => any;
removeEventListener?: (...args: any) => any;
}
export interface FormDataVisitorHelpers {
defaultVisitor: SerializerVisitor;
convertValue: (value: any) => any;
isVisitable: (value: any) => boolean;
}
export interface SerializerVisitor {
(
this: GenericFormData,
value: any,
key: string | number,
path: null | Array<string | number>,
helpers: FormDataVisitorHelpers
): boolean;
}
export interface SerializerOptions {
visitor?: SerializerVisitor;
dots?: boolean;
metaTokens?: boolean;
indexes?: boolean | null;
}
// tslint:disable-next-line
export interface FormSerializerOptions extends SerializerOptions {
}
export interface ParamEncoder {
(value: any, defaultEncoder: (value: any) => any): any;
}
export interface CustomParamsSerializer {
(params: Record<string, any>, options?: ParamsSerializerOptions): string;
}
export interface ParamsSerializerOptions extends SerializerOptions {
encode?: ParamEncoder;
serialize?: CustomParamsSerializer;
}
type MaxUploadRate = number;
type MaxDownloadRate = number;
type BrowserProgressEvent = any;
export interface AxiosProgressEvent {
loaded: number;
total?: number;
progress?: number;
bytes: number;
rate?: number;
estimated?: number;
upload?: boolean;
download?: boolean;
event?: BrowserProgressEvent;
lengthComputable: boolean;
}
type Milliseconds = number;
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | (string & {});
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
export type AddressFamily = 4 | 6 | undefined;
export interface LookupAddressEntry {
address: string;
family?: AddressFamily;
}
export type LookupAddress = string | LookupAddressEntry;
export interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
allowAbsoluteUrls?: boolean;
transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
params?: any;
paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer;
data?: D;
timeout?: Milliseconds;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
responseEncoding?: responseEncoding | string;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
maxRate?: number | [MaxUploadRate, MaxDownloadRate];
beforeRedirect?: (options: Record<string, any>, responseDetails: {headers: Record<string, string>, statusCode: HttpStatusCode}) => void;
socketPath?: string | null;
transport?: any;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions;
signal?: GenericAbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
fetch?: (input: URL | Request | string, init?: RequestInit) => Promise<Response>;
Request?: new (input: URL | Request | string, init?: RequestInit) => Request;
Response?: new (
body?: ArrayBuffer | ArrayBufferView | Blob | FormData | URLSearchParams | string | null,
init?: ResponseInit
) => Response;
};
formSerializer?: FormSerializerOptions;
family?: AddressFamily;
lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) |
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
parseReviver?: (this: any, key: string, value: any) => any;
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
}
// Alias
export type RawAxiosRequestConfig<D = any> = AxiosRequestConfig<D>;
export interface InternalAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
headers: AxiosRequestHeaders;
}
export interface HeadersDefaults {
common: RawAxiosRequestHeaders;
delete: RawAxiosRequestHeaders;
get: RawAxiosRequestHeaders;
head: RawAxiosRequestHeaders;
post: RawAxiosRequestHeaders;
put: RawAxiosRequestHeaders;
patch: RawAxiosRequestHeaders;
options?: RawAxiosRequestHeaders;
purge?: RawAxiosRequestHeaders;
link?: RawAxiosRequestHeaders;
unlink?: RawAxiosRequestHeaders;
}
export interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers: HeadersDefaults;
}
export interface CreateAxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers?: RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>;
}
export interface AxiosResponse<T = any, D = any, H = {}> {
data: T;
status: number;
statusText: string;
headers: H & RawAxiosResponseHeaders | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D>;
request?: any;
}
export class AxiosError<T = unknown, D = any> extends Error {
constructor(
message?: string,
code?: string,
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>
);
config?: InternalAxiosRequestConfig<D>;
code?: string;
request?: any;
response?: AxiosResponse<T, D>;
isAxiosError: boolean;
status?: number;
toJSON: () => object;
cause?: unknown;
event?: BrowserProgressEvent;
static from<T = unknown, D = any>(
error: Error | unknown,
code?: string,
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>,
customProps?: object,
): AxiosError<T, D>;
static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION";
static readonly ERR_NETWORK = "ERR_NETWORK";
static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
static readonly ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
static readonly ERR_INVALID_URL = "ERR_INVALID_URL";
static readonly ERR_CANCELED = "ERR_CANCELED";
static readonly ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT";
}
export class CanceledError<T> extends AxiosError<T> {
readonly name: "CanceledError";
}
export type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
export interface CancelStatic {
new (message?: string): Cancel;
}
export interface Cancel {
message: string | undefined;
}
export interface Canceler {
(message?: string, config?: AxiosRequestConfig, request?: any): void;
}
export interface CancelTokenStatic {
new (executor: (cancel: Canceler) => void): CancelToken;
source(): CancelTokenSource;
}
export interface CancelToken {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
}
export interface CancelTokenSource {
token: CancelToken;
cancel: Canceler;
}
export interface AxiosInterceptorOptions {
synchronous?: boolean;
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
}
type AxiosRequestInterceptorUse<T> = (onFulfilled?: ((value: T) => T | Promise<T>) | null, onRejected?: ((error: any) => any) | null, options?: AxiosInterceptorOptions) => number;
type AxiosResponseInterceptorUse<T> = (onFulfilled?: ((value: T) => T | Promise<T>) | null, onRejected?: ((error: any) => any) | null) => number;
export interface AxiosInterceptorManager<V> {
use: V extends AxiosResponse ? AxiosResponseInterceptorUse<V> : AxiosRequestInterceptorUse<V>;
eject(id: number): void;
clear(): void;
}
export class Axios {
constructor(config?: AxiosRequestConfig);
defaults: AxiosDefaults;
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
delete<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
head<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
options<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
}
export interface AxiosInstance extends Axios {
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
create(config?: CreateAxiosDefaults): AxiosInstance;
defaults: Omit<AxiosDefaults, 'headers'> & {
headers: HeadersDefaults & {
[key: string]: AxiosHeaderValue
}
};
}
export interface GenericFormData {
append(name: string, value: any, options?: any): any;
}
export interface GenericHTMLFormElement {
name: string;
method: string;
submit(): void;
}
export function getAdapter(adapters: AxiosAdapterConfig | AxiosAdapterConfig[] | undefined): AxiosAdapter;
export function toFormData(sourceObj: object, targetFormData?: GenericFormData, options?: FormSerializerOptions): GenericFormData;
export function formToJSON(form: GenericFormData|GenericHTMLFormElement): object;
export function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>;
export function spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
export function isCancel<T = any>(value: any): value is CanceledError<T>;
export function all<T>(values: Array<T | Promise<T>>): Promise<T[]>;
export function mergeConfig<D = any>(config1: AxiosRequestConfig<D>, config2: AxiosRequestConfig<D>): AxiosRequestConfig<D>;
export interface AxiosStatic extends AxiosInstance {
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
HttpStatusCode: typeof HttpStatusCode;
readonly VERSION: string;
isCancel: typeof isCancel;
all: typeof all;
spread: typeof spread;
isAxiosError: typeof isAxiosError;
toFormData: typeof toFormData;
formToJSON: typeof formToJSON;
getAdapter: typeof getAdapter;
CanceledError: typeof CanceledError;
AxiosHeaders: typeof AxiosHeaders;
mergeConfig: typeof mergeConfig;
}
declare const axios: AxiosStatic;
export default axios;

43
node_modules/axios/index.js generated vendored Normal file
View File

@@ -0,0 +1,43 @@
import axios from './lib/axios.js';
// This module is intended to unwrap Axios default export as named.
// Keep top-level export same with static properties
// so that it can keep same with es module or cjs
const {
Axios,
AxiosError,
CanceledError,
isCancel,
CancelToken,
VERSION,
all,
Cancel,
isAxiosError,
spread,
toFormData,
AxiosHeaders,
HttpStatusCode,
formToJSON,
getAdapter,
mergeConfig
} = axios;
export {
axios as default,
Axios,
AxiosError,
CanceledError,
isCancel,
CancelToken,
VERSION,
all,
Cancel,
isAxiosError,
spread,
toFormData,
AxiosHeaders,
HttpStatusCode,
formToJSON,
getAdapter,
mergeConfig
}

37
node_modules/axios/lib/adapters/README.md generated vendored Normal file
View File

@@ -0,0 +1,37 @@
# axios // adapters
The modules under `adapters/` are modules that handle dispatching a request and settling a returned `Promise` once a response is received.
## Example
```js
var settle = require('./../core/settle');
module.exports = function myAdapter(config) {
// At this point:
// - config has been merged with defaults
// - request transformers have already run
// - request interceptors have already run
// Make the request using config provided
// Upon response settle the Promise
return new Promise(function(resolve, reject) {
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve, reject, response);
// From here:
// - response transformers will run
// - response interceptors will run
});
}
```

81
node_modules/axios/lib/adapters/adapters.js generated vendored Normal file
View File

@@ -0,0 +1,81 @@
import utils from '../utils.js';
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
import * as fetchAdapter from './fetch.js';
import AxiosError from "../core/AxiosError.js";
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter,
fetch: {
get: fetchAdapter.getFetch,
}
}
utils.forEach(knownAdapters, (fn, value) => {
if (fn) {
try {
Object.defineProperty(fn, 'name', {value});
} catch (e) {
// eslint-disable-next-line no-empty
}
Object.defineProperty(fn, 'adapterName', {value});
}
});
const renderReason = (reason) => `- ${reason}`;
const isResolvedHandle = (adapter) => utils.isFunction(adapter) || adapter === null || adapter === false;
export default {
getAdapter: (adapters, config) => {
adapters = utils.isArray(adapters) ? adapters : [adapters];
const {length} = adapters;
let nameOrAdapter;
let adapter;
const rejectedReasons = {};
for (let i = 0; i < length; i++) {
nameOrAdapter = adapters[i];
let id;
adapter = nameOrAdapter;
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
if (adapter === undefined) {
throw new AxiosError(`Unknown adapter '${id}'`);
}
}
if (adapter && (utils.isFunction(adapter) || (adapter = adapter.get(config)))) {
break;
}
rejectedReasons[id || '#' + i] = adapter;
}
if (!adapter) {
const reasons = Object.entries(rejectedReasons)
.map(([id, state]) => `adapter ${id} ` +
(state === false ? 'is not supported by the environment' : 'is not available in the build')
);
let s = length ?
(reasons.length > 1 ? 'since :\n' + reasons.map(renderReason).join('\n') : ' ' + renderReason(reasons[0])) :
'as no adapter specified';
throw new AxiosError(
`There is no suitable adapter to dispatch the request ` + s,
'ERR_NOT_SUPPORT'
);
}
return adapter;
},
adapters: knownAdapters
}

288
node_modules/axios/lib/adapters/fetch.js generated vendored Normal file
View File

@@ -0,0 +1,288 @@
import platform from "../platform/index.js";
import utils from "../utils.js";
import AxiosError from "../core/AxiosError.js";
import composeSignals from "../helpers/composeSignals.js";
import {trackStream} from "../helpers/trackStream.js";
import AxiosHeaders from "../core/AxiosHeaders.js";
import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.js";
import resolveConfig from "../helpers/resolveConfig.js";
import settle from "../core/settle.js";
const DEFAULT_CHUNK_SIZE = 64 * 1024;
const {isFunction} = utils;
const globalFetchAPI = (({Request, Response}) => ({
Request, Response
}))(utils.global);
const {
ReadableStream, TextEncoder
} = utils.global;
const test = (fn, ...args) => {
try {
return !!fn(...args);
} catch (e) {
return false
}
}
const factory = (env) => {
env = utils.merge.call({
skipUndefined: true
}, globalFetchAPI, env);
const {fetch: envFetch, Request, Response} = env;
const isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function';
const isRequestSupported = isFunction(Request);
const isResponseSupported = isFunction(Response);
if (!isFetchSupported) {
return false;
}
const isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream);
const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ?
((encoder) => (str) => encoder.encode(str))(new TextEncoder()) :
async (str) => new Uint8Array(await new Request(str).arrayBuffer())
);
const supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(() => {
let duplexAccessed = false;
const hasContentType = new Request(platform.origin, {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
});
const supportsResponseStream = isResponseSupported && isReadableStreamSupported &&
test(() => utils.isReadableStream(new Response('').body));
const resolvers = {
stream: supportsResponseStream && ((res) => res.body)
};
isFetchSupported && ((() => {
['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach(type => {
!resolvers[type] && (resolvers[type] = (res, config) => {
let method = res && res[type];
if (method) {
return method.call(res);
}
throw new AxiosError(`Response type '${type}' is not supported`, AxiosError.ERR_NOT_SUPPORT, config);
})
});
})());
const getBodyLength = async (body) => {
if (body == null) {
return 0;
}
if (utils.isBlob(body)) {
return body.size;
}
if (utils.isSpecCompliantForm(body)) {
const _request = new Request(platform.origin, {
method: 'POST',
body,
});
return (await _request.arrayBuffer()).byteLength;
}
if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
return body.byteLength;
}
if (utils.isURLSearchParams(body)) {
body = body + '';
}
if (utils.isString(body)) {
return (await encodeText(body)).byteLength;
}
}
const resolveBodyLength = async (headers, body) => {
const length = utils.toFiniteNumber(headers.getContentLength());
return length == null ? getBodyLength(body) : length;
}
return async (config) => {
let {
url,
method,
data,
signal,
cancelToken,
timeout,
onDownloadProgress,
onUploadProgress,
responseType,
headers,
withCredentials = 'same-origin',
fetchOptions
} = resolveConfig(config);
let _fetch = envFetch || fetch;
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
let composedSignal = composeSignals([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
let request = null;
const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => {
composedSignal.unsubscribe();
});
let requestContentLength;
try {
if (
onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head' &&
(requestContentLength = await resolveBodyLength(headers, data)) !== 0
) {
let _request = new Request(url, {
method: 'POST',
body: data,
duplex: "half"
});
let contentTypeHeader;
if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
headers.setContentType(contentTypeHeader)
}
if (_request.body) {
const [onProgress, flush] = progressEventDecorator(
requestContentLength,
progressEventReducer(asyncDecorator(onUploadProgress))
);
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
}
}
if (!utils.isString(withCredentials)) {
withCredentials = withCredentials ? 'include' : 'omit';
}
// Cloudflare Workers throws when credentials are defined
// see https://github.com/cloudflare/workerd/issues/902
const isCredentialsSupported = isRequestSupported && "credentials" in Request.prototype;
const resolvedOptions = {
...fetchOptions,
signal: composedSignal,
method: method.toUpperCase(),
headers: headers.normalize().toJSON(),
body: data,
duplex: "half",
credentials: isCredentialsSupported ? withCredentials : undefined
};
request = isRequestSupported && new Request(url, resolvedOptions);
let response = await (isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions));
const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
if (supportsResponseStream && (onDownloadProgress || (isStreamResponse && unsubscribe))) {
const options = {};
['status', 'statusText', 'headers'].forEach(prop => {
options[prop] = response[prop];
});
const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));
const [onProgress, flush] = onDownloadProgress && progressEventDecorator(
responseContentLength,
progressEventReducer(asyncDecorator(onDownloadProgress), true)
) || [];
response = new Response(
trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
flush && flush();
unsubscribe && unsubscribe();
}),
options
);
}
responseType = responseType || 'text';
let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](response, config);
!isStreamResponse && unsubscribe && unsubscribe();
return await new Promise((resolve, reject) => {
settle(resolve, reject, {
data: responseData,
headers: AxiosHeaders.from(response.headers),
status: response.status,
statusText: response.statusText,
config,
request
})
})
} catch (err) {
unsubscribe && unsubscribe();
if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
throw Object.assign(
new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request),
{
cause: err.cause || err
}
)
}
throw AxiosError.from(err, err && err.code, config, request);
}
}
}
const seedCache = new Map();
export const getFetch = (config) => {
let env = config ? config.env : {};
const {fetch, Request, Response} = env;
const seeds = [
Request, Response, fetch
];
let len = seeds.length, i = len,
seed, target, map = seedCache;
while (i--) {
seed = seeds[i];
target = map.get(seed);
target === undefined && map.set(seed, target = (i ? new Map() : factory(env)))
map = target;
}
return target;
};
const adapter = getFetch();
export default adapter;

713
node_modules/axios/lib/adapters/http.js generated vendored Executable file
View File

@@ -0,0 +1,713 @@
'use strict';
import utils from './../utils.js';
import settle from './../core/settle.js';
import buildFullPath from '../core/buildFullPath.js';
import buildURL from './../helpers/buildURL.js';
import proxyFromEnv from 'proxy-from-env';
import http from 'http';
import https from 'https';
import util from 'util';
import followRedirects from 'follow-redirects';
import zlib from 'zlib';
import {VERSION} from '../env/data.js';
import transitionalDefaults from '../defaults/transitional.js';
import AxiosError from '../core/AxiosError.js';
import CanceledError from '../cancel/CanceledError.js';
import platform from '../platform/index.js';
import fromDataURI from '../helpers/fromDataURI.js';
import stream from 'stream';
import AxiosHeaders from '../core/AxiosHeaders.js';
import AxiosTransformStream from '../helpers/AxiosTransformStream.js';
import {EventEmitter} from 'events';
import formDataToStream from "../helpers/formDataToStream.js";
import readBlob from "../helpers/readBlob.js";
import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js';
import callbackify from "../helpers/callbackify.js";
import {progressEventReducer, progressEventDecorator, asyncDecorator} from "../helpers/progressEventReducer.js";
import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js';
const zlibOptions = {
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH
};
const brotliOptions = {
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}
const isBrotliSupported = utils.isFunction(zlib.createBrotliDecompress);
const {http: httpFollow, https: httpsFollow} = followRedirects;
const isHttps = /https:?/;
const supportedProtocols = platform.protocols.map(protocol => {
return protocol + ':';
});
const flushOnFinish = (stream, [throttled, flush]) => {
stream
.on('end', flush)
.on('error', flush);
return throttled;
}
/**
* If the proxy or config beforeRedirects functions are defined, call them with the options
* object.
*
* @param {Object<string, any>} options - The options object that was passed to the request.
*
* @returns {Object<string, any>}
*/
function dispatchBeforeRedirect(options, responseDetails) {
if (options.beforeRedirects.proxy) {
options.beforeRedirects.proxy(options);
}
if (options.beforeRedirects.config) {
options.beforeRedirects.config(options, responseDetails);
}
}
/**
* If the proxy or config afterRedirects functions are defined, call them with the options
*
* @param {http.ClientRequestArgs} options
* @param {AxiosProxyConfig} configProxy configuration from Axios options object
* @param {string} location
*
* @returns {http.ClientRequestArgs}
*/
function setProxy(options, configProxy, location) {
let proxy = configProxy;
if (!proxy && proxy !== false) {
const proxyUrl = proxyFromEnv.getProxyForUrl(location);
if (proxyUrl) {
proxy = new URL(proxyUrl);
}
}
if (proxy) {
// Basic proxy authorization
if (proxy.username) {
proxy.auth = (proxy.username || '') + ':' + (proxy.password || '');
}
if (proxy.auth) {
// Support proxy auth object form
if (proxy.auth.username || proxy.auth.password) {
proxy.auth = (proxy.auth.username || '') + ':' + (proxy.auth.password || '');
}
const base64 = Buffer
.from(proxy.auth, 'utf8')
.toString('base64');
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
}
options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
const proxyHost = proxy.hostname || proxy.host;
options.hostname = proxyHost;
// Replace 'host' since options is not a URL object
options.host = proxyHost;
options.port = proxy.port;
options.path = location;
if (proxy.protocol) {
options.protocol = proxy.protocol.includes(':') ? proxy.protocol : `${proxy.protocol}:`;
}
}
options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {
// Configure proxy for redirected request, passing the original config proxy to apply
// the exact same logic as if the redirected request was performed by axios directly.
setProxy(redirectOptions, configProxy, redirectOptions.href);
};
}
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
// temporary hotfix
const wrapAsync = (asyncExecutor) => {
return new Promise((resolve, reject) => {
let onDone;
let isDone;
const done = (value, isRejected) => {
if (isDone) return;
isDone = true;
onDone && onDone(value, isRejected);
}
const _resolve = (value) => {
done(value);
resolve(value);
};
const _reject = (reason) => {
done(reason, true);
reject(reason);
}
asyncExecutor(_resolve, _reject, (onDoneHandler) => (onDone = onDoneHandler)).catch(_reject);
})
};
const resolveFamily = ({address, family}) => {
if (!utils.isString(address)) {
throw TypeError('address must be a string');
}
return ({
address,
family: family || (address.indexOf('.') < 0 ? 6 : 4)
});
}
const buildAddressEntry = (address, family) => resolveFamily(utils.isObject(address) ? address : {address, family});
/*eslint consistent-return:0*/
export default isHttpAdapterSupported && function httpAdapter(config) {
return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
let {data, lookup, family} = config;
const {responseType, responseEncoding} = config;
const method = config.method.toUpperCase();
let isDone;
let rejected = false;
let req;
if (lookup) {
const _lookup = callbackify(lookup, (value) => utils.isArray(value) ? value : [value]);
// hotfix to support opt.all option which is required for node 20.x
lookup = (hostname, opt, cb) => {
_lookup(hostname, opt, (err, arg0, arg1) => {
if (err) {
return cb(err);
}
const addresses = utils.isArray(arg0) ? arg0.map(addr => buildAddressEntry(addr)) : [buildAddressEntry(arg0, arg1)];
opt.all ? cb(err, addresses) : cb(err, addresses[0].address, addresses[0].family);
});
}
}
// temporary internal emitter until the AxiosRequest class will be implemented
const emitter = new EventEmitter();
const onFinished = () => {
if (config.cancelToken) {
config.cancelToken.unsubscribe(abort);
}
if (config.signal) {
config.signal.removeEventListener('abort', abort);
}
emitter.removeAllListeners();
}
onDone((value, isRejected) => {
isDone = true;
if (isRejected) {
rejected = true;
onFinished();
}
});
function abort(reason) {
emitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason);
}
emitter.once('abort', reject);
if (config.cancelToken || config.signal) {
config.cancelToken && config.cancelToken.subscribe(abort);
if (config.signal) {
config.signal.aborted ? abort() : config.signal.addEventListener('abort', abort);
}
}
// Parse url
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined);
const protocol = parsed.protocol || supportedProtocols[0];
if (protocol === 'data:') {
// Apply the same semantics as HTTP: only enforce if a finite, non-negative cap is set.
if (config.maxContentLength > -1) {
// Use the exact string passed to fromDataURI (config.url); fall back to fullPath if needed.
const dataUrl = String(config.url || fullPath || '');
const estimated = estimateDataURLDecodedBytes(dataUrl);
if (estimated > config.maxContentLength) {
return reject(new AxiosError(
'maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE,
config
));
}
}
let convertedData;
if (method !== 'GET') {
return settle(resolve, reject, {
status: 405,
statusText: 'method not allowed',
headers: {},
config
});
}
try {
convertedData = fromDataURI(config.url, responseType === 'blob', {
Blob: config.env && config.env.Blob
});
} catch (err) {
throw AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config);
}
if (responseType === 'text') {
convertedData = convertedData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === 'utf8') {
convertedData = utils.stripBOM(convertedData);
}
} else if (responseType === 'stream') {
convertedData = stream.Readable.from(convertedData);
}
return settle(resolve, reject, {
data: convertedData,
status: 200,
statusText: 'OK',
headers: new AxiosHeaders(),
config
});
}
if (supportedProtocols.indexOf(protocol) === -1) {
return reject(new AxiosError(
'Unsupported protocol ' + protocol,
AxiosError.ERR_BAD_REQUEST,
config
));
}
const headers = AxiosHeaders.from(config.headers).normalize();
// Set User-Agent (required by some servers)
// See https://github.com/axios/axios/issues/69
// User-Agent is specified; handle case where no UA header is desired
// Only set header if it hasn't been set in config
headers.set('User-Agent', 'axios/' + VERSION, false);
const {onUploadProgress, onDownloadProgress} = config;
const maxRate = config.maxRate;
let maxUploadRate = undefined;
let maxDownloadRate = undefined;
// support for spec compliant FormData objects
if (utils.isSpecCompliantForm(data)) {
const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i);
data = formDataToStream(data, (formHeaders) => {
headers.set(formHeaders);
}, {
tag: `axios-${VERSION}-boundary`,
boundary: userBoundary && userBoundary[1] || undefined
});
// support for https://www.npmjs.com/package/form-data api
} else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {
headers.set(data.getHeaders());
if (!headers.hasContentLength()) {
try {
const knownLength = await util.promisify(data.getLength).call(data);
Number.isFinite(knownLength) && knownLength >= 0 && headers.setContentLength(knownLength);
/*eslint no-empty:0*/
} catch (e) {
}
}
} else if (utils.isBlob(data) || utils.isFile(data)) {
data.size && headers.setContentType(data.type || 'application/octet-stream');
headers.setContentLength(data.size || 0);
data = stream.Readable.from(readBlob(data));
} else if (data && !utils.isStream(data)) {
if (Buffer.isBuffer(data)) {
// Nothing to do...
} else if (utils.isArrayBuffer(data)) {
data = Buffer.from(new Uint8Array(data));
} else if (utils.isString(data)) {
data = Buffer.from(data, 'utf-8');
} else {
return reject(new AxiosError(
'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
AxiosError.ERR_BAD_REQUEST,
config
));
}
// Add Content-Length header if data exists
headers.setContentLength(data.length, false);
if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) {
return reject(new AxiosError(
'Request body larger than maxBodyLength limit',
AxiosError.ERR_BAD_REQUEST,
config
));
}
}
const contentLength = utils.toFiniteNumber(headers.getContentLength());
if (utils.isArray(maxRate)) {
maxUploadRate = maxRate[0];
maxDownloadRate = maxRate[1];
} else {
maxUploadRate = maxDownloadRate = maxRate;
}
if (data && (onUploadProgress || maxUploadRate)) {
if (!utils.isStream(data)) {
data = stream.Readable.from(data, {objectMode: false});
}
data = stream.pipeline([data, new AxiosTransformStream({
maxRate: utils.toFiniteNumber(maxUploadRate)
})], utils.noop);
onUploadProgress && data.on('progress', flushOnFinish(
data,
progressEventDecorator(
contentLength,
progressEventReducer(asyncDecorator(onUploadProgress), false, 3)
)
));
}
// HTTP basic authentication
let auth = undefined;
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password || '';
auth = username + ':' + password;
}
if (!auth && parsed.username) {
const urlUsername = parsed.username;
const urlPassword = parsed.password;
auth = urlUsername + ':' + urlPassword;
}
auth && headers.delete('authorization');
let path;
try {
path = buildURL(
parsed.pathname + parsed.search,
config.params,
config.paramsSerializer
).replace(/^\?/, '');
} catch (err) {
const customErr = new Error(err.message);
customErr.config = config;
customErr.url = config.url;
customErr.exists = true;
return reject(customErr);
}
headers.set(
'Accept-Encoding',
'gzip, compress, deflate' + (isBrotliSupported ? ', br' : ''), false
);
const options = {
path,
method: method,
headers: headers.toJSON(),
agents: { http: config.httpAgent, https: config.httpsAgent },
auth,
protocol,
family,
beforeRedirect: dispatchBeforeRedirect,
beforeRedirects: {}
};
// cacheable-lookup integration hotfix
!utils.isUndefined(lookup) && (options.lookup = lookup);
if (config.socketPath) {
options.socketPath = config.socketPath;
} else {
options.hostname = parsed.hostname.startsWith("[") ? parsed.hostname.slice(1, -1) : parsed.hostname;
options.port = parsed.port;
setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
}
let transport;
const isHttpsRequest = isHttps.test(options.protocol);
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
if (config.transport) {
transport = config.transport;
} else if (config.maxRedirects === 0) {
transport = isHttpsRequest ? https : http;
} else {
if (config.maxRedirects) {
options.maxRedirects = config.maxRedirects;
}
if (config.beforeRedirect) {
options.beforeRedirects.config = config.beforeRedirect;
}
transport = isHttpsRequest ? httpsFollow : httpFollow;
}
if (config.maxBodyLength > -1) {
options.maxBodyLength = config.maxBodyLength;
} else {
// follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited
options.maxBodyLength = Infinity;
}
if (config.insecureHTTPParser) {
options.insecureHTTPParser = config.insecureHTTPParser;
}
// Create the request
req = transport.request(options, function handleResponse(res) {
if (req.destroyed) return;
const streams = [res];
const responseLength = +res.headers['content-length'];
if (onDownloadProgress || maxDownloadRate) {
const transformStream = new AxiosTransformStream({
maxRate: utils.toFiniteNumber(maxDownloadRate)
});
onDownloadProgress && transformStream.on('progress', flushOnFinish(
transformStream,
progressEventDecorator(
responseLength,
progressEventReducer(asyncDecorator(onDownloadProgress), true, 3)
)
));
streams.push(transformStream);
}
// decompress the response body transparently if required
let responseStream = res;
// return the last request in case of redirects
const lastRequest = res.req || req;
// if decompress disabled we should not decompress
if (config.decompress !== false && res.headers['content-encoding']) {
// if no content, but headers still say that it is encoded,
// remove the header not confuse downstream operations
if (method === 'HEAD' || res.statusCode === 204) {
delete res.headers['content-encoding'];
}
switch ((res.headers['content-encoding'] || '').toLowerCase()) {
/*eslint default-case:0*/
case 'gzip':
case 'x-gzip':
case 'compress':
case 'x-compress':
// add the unzipper to the body stream processing pipeline
streams.push(zlib.createUnzip(zlibOptions));
// remove the content-encoding in order to not confuse downstream operations
delete res.headers['content-encoding'];
break;
case 'deflate':
streams.push(new ZlibHeaderTransformStream());
// add the unzipper to the body stream processing pipeline
streams.push(zlib.createUnzip(zlibOptions));
// remove the content-encoding in order to not confuse downstream operations
delete res.headers['content-encoding'];
break;
case 'br':
if (isBrotliSupported) {
streams.push(zlib.createBrotliDecompress(brotliOptions));
delete res.headers['content-encoding'];
}
}
}
responseStream = streams.length > 1 ? stream.pipeline(streams, utils.noop) : streams[0];
const offListeners = stream.finished(responseStream, () => {
offListeners();
onFinished();
});
const response = {
status: res.statusCode,
statusText: res.statusMessage,
headers: new AxiosHeaders(res.headers),
config,
request: lastRequest
};
if (responseType === 'stream') {
response.data = responseStream;
settle(resolve, reject, response);
} else {
const responseBuffer = [];
let totalResponseBytes = 0;
responseStream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk);
totalResponseBytes += chunk.length;
// make sure the content length is not over the maxContentLength if specified
if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {
// stream.destroy() emit aborted event before calling reject() on Node.js v16
rejected = true;
responseStream.destroy();
reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
}
});
responseStream.on('aborted', function handlerStreamAborted() {
if (rejected) {
return;
}
const err = new AxiosError(
'stream has been aborted',
AxiosError.ERR_BAD_RESPONSE,
config,
lastRequest
);
responseStream.destroy(err);
reject(err);
});
responseStream.on('error', function handleStreamError(err) {
if (req.destroyed) return;
reject(AxiosError.from(err, null, config, lastRequest));
});
responseStream.on('end', function handleStreamEnd() {
try {
let responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
if (responseType !== 'arraybuffer') {
responseData = responseData.toString(responseEncoding);
if (!responseEncoding || responseEncoding === 'utf8') {
responseData = utils.stripBOM(responseData);
}
}
response.data = responseData;
} catch (err) {
return reject(AxiosError.from(err, null, config, response.request, response));
}
settle(resolve, reject, response);
});
}
emitter.once('abort', err => {
if (!responseStream.destroyed) {
responseStream.emit('error', err);
responseStream.destroy();
}
});
});
emitter.once('abort', err => {
reject(err);
req.destroy(err);
});
// Handle errors
req.on('error', function handleRequestError(err) {
// @todo remove
// if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return;
reject(AxiosError.from(err, null, config, req));
});
// set tcp keep alive to prevent drop connection by peer
req.on('socket', function handleRequestSocket(socket) {
// default interval of sending ack packet is 1 minute
socket.setKeepAlive(true, 1000 * 60);
});
// Handle request timeout
if (config.timeout) {
// This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types.
const timeout = parseInt(config.timeout, 10);
if (Number.isNaN(timeout)) {
reject(new AxiosError(
'error trying to parse `config.timeout` to int',
AxiosError.ERR_BAD_OPTION_VALUE,
config,
req
));
return;
}
// Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
// And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
// At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
// And then these socket which be hang up will devouring CPU little by little.
// ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
req.setTimeout(timeout, function handleRequestTimeout() {
if (isDone) return;
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
const transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
req
));
abort();
});
}
// Send the request
if (utils.isStream(data)) {
let ended = false;
let errored = false;
data.on('end', () => {
ended = true;
});
data.once('error', err => {
errored = true;
req.destroy(err);
});
data.on('close', () => {
if (!ended && !errored) {
abort(new CanceledError('Request stream has been aborted', config, req));
}
});
data.pipe(req);
} else {
req.end(data);
}
});
}
export const __setProxy = setProxy;

200
node_modules/axios/lib/adapters/xhr.js generated vendored Normal file
View File

@@ -0,0 +1,200 @@
import utils from './../utils.js';
import settle from './../core/settle.js';
import transitionalDefaults from '../defaults/transitional.js';
import AxiosError from '../core/AxiosError.js';
import CanceledError from '../cancel/CanceledError.js';
import parseProtocol from '../helpers/parseProtocol.js';
import platform from '../platform/index.js';
import AxiosHeaders from '../core/AxiosHeaders.js';
import {progressEventReducer} from '../helpers/progressEventReducer.js';
import resolveConfig from "../helpers/resolveConfig.js";
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
const _config = resolveConfig(config);
let requestData = _config.data;
const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
let {responseType, onUploadProgress, onDownloadProgress} = _config;
let onCanceled;
let uploadThrottled, downloadThrottled;
let flushUpload, flushDownload;
function done() {
flushUpload && flushUpload(); // flush events
flushDownload && flushDownload(); // flush events
_config.cancelToken && _config.cancelToken.unsubscribe(onCanceled);
_config.signal && _config.signal.removeEventListener('abort', onCanceled);
}
let request = new XMLHttpRequest();
request.open(_config.method.toUpperCase(), _config.url, true);
// Set the request timeout in MS
request.timeout = _config.timeout;
function onloadend() {
if (!request) {
return;
}
// Prepare the response
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// Clean up request
request = null;
};
// Handle low level network errors
request.onerror = function handleError(event) {
// Browsers deliver a ProgressEvent in XHR onerror
// (message may be empty; when present, surface it)
// See https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/error_event
const msg = event && event.message ? event.message : 'Network Error';
const err = new AxiosError(msg, AxiosError.ERR_NETWORK, config, request);
// attach the underlying event for consumers who want details
err.event = event || null;
reject(err);
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
let timeoutErrorMessage = _config.timeout ? 'timeout of ' + _config.timeout + 'ms exceeded' : 'timeout exceeded';
const transitional = _config.transitional || transitionalDefaults;
if (_config.timeoutErrorMessage) {
timeoutErrorMessage = _config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request));
// Clean up request
request = null;
};
// Remove Content-Type if data is undefined
requestData === undefined && requestHeaders.setContentType(null);
// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
});
}
// Add withCredentials to request if needed
if (!utils.isUndefined(_config.withCredentials)) {
request.withCredentials = !!_config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = _config.responseType;
}
// Handle progress if needed
if (onDownloadProgress) {
([downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true));
request.addEventListener('progress', downloadThrottled);
}
// Not all browsers support upload events
if (onUploadProgress && request.upload) {
([uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress));
request.upload.addEventListener('progress', uploadThrottled);
request.upload.addEventListener('loadend', flushUpload);
}
if (_config.cancelToken || _config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
if (_config.signal) {
_config.signal.aborted ? onCanceled() : _config.signal.addEventListener('abort', onCanceled);
}
}
const protocol = parseProtocol(_config.url);
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return;
}
// Send the request
request.send(requestData || null);
});
}

89
node_modules/axios/lib/axios.js generated vendored Normal file
View File

@@ -0,0 +1,89 @@
'use strict';
import utils from './utils.js';
import bind from './helpers/bind.js';
import Axios from './core/Axios.js';
import mergeConfig from './core/mergeConfig.js';
import defaults from './defaults/index.js';
import formDataToJSON from './helpers/formDataToJSON.js';
import CanceledError from './cancel/CanceledError.js';
import CancelToken from './cancel/CancelToken.js';
import isCancel from './cancel/isCancel.js';
import {VERSION} from './env/data.js';
import toFormData from './helpers/toFormData.js';
import AxiosError from './core/AxiosError.js';
import spread from './helpers/spread.js';
import isAxiosError from './helpers/isAxiosError.js';
import AxiosHeaders from "./core/AxiosHeaders.js";
import adapters from './adapters/adapters.js';
import HttpStatusCode from './helpers/HttpStatusCode.js';
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
*
* @returns {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// Copy context to instance
utils.extend(instance, context, null, {allOwnKeys: true});
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// Create the default instance to be exported
const axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Expose Cancel & CancelToken
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
axios.toFormData = toFormData;
// Expose AxiosError class
axios.AxiosError = AxiosError;
// alias for CanceledError for backward compatibility
axios.Cancel = axios.CanceledError;
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = spread;
// Expose isAxiosError
axios.isAxiosError = isAxiosError;
// Expose mergeConfig
axios.mergeConfig = mergeConfig;
axios.AxiosHeaders = AxiosHeaders;
axios.formToJSON = thing => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
axios.getAdapter = adapters.getAdapter;
axios.HttpStatusCode = HttpStatusCode;
axios.default = axios;
// this module should only have a default export
export default axios

135
node_modules/axios/lib/cancel/CancelToken.js generated vendored Normal file
View File

@@ -0,0 +1,135 @@
'use strict';
import CanceledError from './CanceledError.js';
/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @param {Function} executor The executor function.
*
* @returns {CancelToken}
*/
class CancelToken {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
let resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
// eslint-disable-next-line func-names
this.promise.then(cancel => {
if (!token._listeners) return;
let i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// eslint-disable-next-line func-names
this.promise.then = onfulfilled => {
let _resolve;
// eslint-disable-next-line func-names
const promise = new Promise(resolve => {
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
return promise;
};
executor(function cancel(message, config, request) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new CanceledError(message, config, request);
resolvePromise(token.reason);
});
}
/**
* Throws a `CanceledError` if cancellation has been requested.
*/
throwIfRequested() {
if (this.reason) {
throw this.reason;
}
}
/**
* Subscribe to the cancel signal
*/
subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
/**
* Unsubscribe from the cancel signal
*/
unsubscribe(listener) {
if (!this._listeners) {
return;
}
const index = this._listeners.indexOf(listener);
if (index !== -1) {
this._listeners.splice(index, 1);
}
}
toAbortSignal() {
const controller = new AbortController();
const abort = (err) => {
controller.abort(err);
};
this.subscribe(abort);
controller.signal.unsubscribe = () => this.unsubscribe(abort);
return controller.signal;
}
/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel
};
}
}
export default CancelToken;

25
node_modules/axios/lib/cancel/CanceledError.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
import AxiosError from '../core/AxiosError.js';
import utils from '../utils.js';
/**
* A `CanceledError` is an object that is thrown when an operation is canceled.
*
* @param {string=} message The message.
* @param {Object=} config The config.
* @param {Object=} request The request.
*
* @returns {CanceledError} The created error.
*/
function CanceledError(message, config, request) {
// eslint-disable-next-line no-eq-null,eqeqeq
AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request);
this.name = 'CanceledError';
}
utils.inherits(CanceledError, AxiosError, {
__CANCEL__: true
});
export default CanceledError;

5
node_modules/axios/lib/cancel/isCancel.js generated vendored Normal file
View File

@@ -0,0 +1,5 @@
'use strict';
export default function isCancel(value) {
return !!(value && value.__CANCEL__);
}

240
node_modules/axios/lib/core/Axios.js generated vendored Normal file
View File

@@ -0,0 +1,240 @@
'use strict';
import utils from './../utils.js';
import buildURL from '../helpers/buildURL.js';
import InterceptorManager from './InterceptorManager.js';
import dispatchRequest from './dispatchRequest.js';
import mergeConfig from './mergeConfig.js';
import buildFullPath from './buildFullPath.js';
import validator from '../helpers/validator.js';
import AxiosHeaders from './AxiosHeaders.js';
const validators = validator.validators;
/**
* Create a new instance of Axios
*
* @param {Object} instanceConfig The default config for the instance
*
* @return {Axios} A new instance of Axios
*/
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig || {};
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* Dispatch a request
*
* @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
* @param {?Object} config
*
* @returns {Promise} The Promise to be fulfilled
*/
async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {
if (err instanceof Error) {
let dummy = {};
Error.captureStackTrace ? Error.captureStackTrace(dummy) : (dummy = new Error());
// slice off the Error: ... line
const stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
try {
if (!err.stack) {
err.stack = stack;
// match without the 2 top stack lines
} else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) {
err.stack += '\n' + stack
}
} catch (e) {
// ignore the case where "stack" is an un-writable property
}
}
throw err;
}
}
_request(configOrUrl, config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
config = mergeConfig(this.defaults, config);
const {transitional, paramsSerializer, headers} = config;
if (transitional !== undefined) {
validator.assertOptions(transitional, {
silentJSONParsing: validators.transitional(validators.boolean),
forcedJSONParsing: validators.transitional(validators.boolean),
clarifyTimeoutError: validators.transitional(validators.boolean)
}, false);
}
if (paramsSerializer != null) {
if (utils.isFunction(paramsSerializer)) {
config.paramsSerializer = {
serialize: paramsSerializer
}
} else {
validator.assertOptions(paramsSerializer, {
encode: validators.function,
serialize: validators.function
}, true);
}
}
// Set config.allowAbsoluteUrls
if (config.allowAbsoluteUrls !== undefined) {
// do nothing
} else if (this.defaults.allowAbsoluteUrls !== undefined) {
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
} else {
config.allowAbsoluteUrls = true;
}
validator.assertOptions(config, {
baseUrl: validators.spelling('baseURL'),
withXsrfToken: validators.spelling('withXSRFToken')
}, true);
// Set config.method
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
// Flatten headers
let contextHeaders = headers && utils.merge(
headers.common,
headers[config.method]
);
headers && utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
(method) => {
delete headers[method];
}
);
config.headers = AxiosHeaders.concat(contextHeaders, headers);
// filter out skipped interceptors
const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
let promise;
let i = 0;
let len;
if (!synchronousRequestInterceptors) {
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift(...requestInterceptorChain);
chain.push(...responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
len = requestInterceptorChain.length;
let newConfig = config;
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected.call(this, error);
break;
}
}
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
i = 0;
len = responseInterceptorChain.length;
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
return promise;
}
getUri(config) {
config = mergeConfig(this.defaults, config);
const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
return buildURL(fullPath, config.params, config.paramsSerializer);
}
}
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url,
data
}));
};
}
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
export default Axios;

110
node_modules/axios/lib/core/AxiosError.js generated vendored Normal file
View File

@@ -0,0 +1,110 @@
'use strict';
import utils from '../utils.js';
/**
* Create an Error with the specified message, config, error code, request and response.
*
* @param {string} message The error message.
* @param {string} [code] The error code (for example, 'ECONNABORTED').
* @param {Object} [config] The config.
* @param {Object} [request] The request.
* @param {Object} [response] The response.
*
* @returns {Error} The created error.
*/
function AxiosError(message, code, config, request, response) {
Error.call(this);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error()).stack;
}
this.message = message;
this.name = 'AxiosError';
code && (this.code = code);
config && (this.config = config);
request && (this.request = request);
if (response) {
this.response = response;
this.status = response.status ? response.status : null;
}
}
utils.inherits(AxiosError, Error, {
toJSON: function toJSON() {
return {
// Standard
message: this.message,
name: this.name,
// Microsoft
description: this.description,
number: this.number,
// Mozilla
fileName: this.fileName,
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
stack: this.stack,
// Axios
config: utils.toJSONObject(this.config),
code: this.code,
status: this.status
};
}
});
const prototype = AxiosError.prototype;
const descriptors = {};
[
'ERR_BAD_OPTION_VALUE',
'ERR_BAD_OPTION',
'ECONNABORTED',
'ETIMEDOUT',
'ERR_NETWORK',
'ERR_FR_TOO_MANY_REDIRECTS',
'ERR_DEPRECATED',
'ERR_BAD_RESPONSE',
'ERR_BAD_REQUEST',
'ERR_CANCELED',
'ERR_NOT_SUPPORT',
'ERR_INVALID_URL'
// eslint-disable-next-line func-names
].forEach(code => {
descriptors[code] = {value: code};
});
Object.defineProperties(AxiosError, descriptors);
Object.defineProperty(prototype, 'isAxiosError', {value: true});
// eslint-disable-next-line func-names
AxiosError.from = (error, code, config, request, response, customProps) => {
const axiosError = Object.create(prototype);
utils.toFlatObject(error, axiosError, function filter(obj) {
return obj !== Error.prototype;
}, prop => {
return prop !== 'isAxiosError';
});
const msg = error && error.message ? error.message : 'Error';
// Prefer explicit code; otherwise copy the low-level error's code (e.g. ECONNREFUSED)
const errCode = code == null && error ? error.code : code;
AxiosError.call(axiosError, msg, errCode, config, request, response);
// Chain the original error on the standard field; non-enumerable to avoid JSON noise
if (error && axiosError.cause == null) {
Object.defineProperty(axiosError, 'cause', { value: error, configurable: true });
}
axiosError.name = (error && error.name) || 'Error';
customProps && Object.assign(axiosError, customProps);
return axiosError;
};
export default AxiosError;

314
node_modules/axios/lib/core/AxiosHeaders.js generated vendored Normal file
View File

@@ -0,0 +1,314 @@
'use strict';
import utils from '../utils.js';
import parseHeaders from '../helpers/parseHeaders.js';
const $internals = Symbol('internals');
function normalizeHeader(header) {
return header && String(header).trim().toLowerCase();
}
function normalizeValue(value) {
if (value === false || value == null) {
return value;
}
return utils.isArray(value) ? value.map(normalizeValue) : String(value);
}
function parseTokens(str) {
const tokens = Object.create(null);
const tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
let match;
while ((match = tokensRE.exec(str))) {
tokens[match[1]] = match[2];
}
return tokens;
}
const isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim());
function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) {
if (utils.isFunction(filter)) {
return filter.call(this, value, header);
}
if (isHeaderNameFilter) {
value = header;
}
if (!utils.isString(value)) return;
if (utils.isString(filter)) {
return value.indexOf(filter) !== -1;
}
if (utils.isRegExp(filter)) {
return filter.test(value);
}
}
function formatHeader(header) {
return header.trim()
.toLowerCase().replace(/([a-z\d])(\w*)/g, (w, char, str) => {
return char.toUpperCase() + str;
});
}
function buildAccessors(obj, header) {
const accessorName = utils.toCamelCase(' ' + header);
['get', 'set', 'has'].forEach(methodName => {
Object.defineProperty(obj, methodName + accessorName, {
value: function(arg1, arg2, arg3) {
return this[methodName].call(this, header, arg1, arg2, arg3);
},
configurable: true
});
});
}
class AxiosHeaders {
constructor(headers) {
headers && this.set(headers);
}
set(header, valueOrRewrite, rewrite) {
const self = this;
function setHeader(_value, _header, _rewrite) {
const lHeader = normalizeHeader(_header);
if (!lHeader) {
throw new Error('header name must be a non-empty string');
}
const key = utils.findKey(self, lHeader);
if(!key || self[key] === undefined || _rewrite === true || (_rewrite === undefined && self[key] !== false)) {
self[key || _header] = normalizeValue(_value);
}
}
const setHeaders = (headers, _rewrite) =>
utils.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));
if (utils.isPlainObject(header) || header instanceof this.constructor) {
setHeaders(header, valueOrRewrite)
} else if(utils.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
setHeaders(parseHeaders(header), valueOrRewrite);
} else if (utils.isObject(header) && utils.isIterable(header)) {
let obj = {}, dest, key;
for (const entry of header) {
if (!utils.isArray(entry)) {
throw TypeError('Object iterator must return a key-value pair');
}
obj[key = entry[0]] = (dest = obj[key]) ?
(utils.isArray(dest) ? [...dest, entry[1]] : [dest, entry[1]]) : entry[1];
}
setHeaders(obj, valueOrRewrite)
} else {
header != null && setHeader(valueOrRewrite, header, rewrite);
}
return this;
}
get(header, parser) {
header = normalizeHeader(header);
if (header) {
const key = utils.findKey(this, header);
if (key) {
const value = this[key];
if (!parser) {
return value;
}
if (parser === true) {
return parseTokens(value);
}
if (utils.isFunction(parser)) {
return parser.call(this, value, key);
}
if (utils.isRegExp(parser)) {
return parser.exec(value);
}
throw new TypeError('parser must be boolean|regexp|function');
}
}
}
has(header, matcher) {
header = normalizeHeader(header);
if (header) {
const key = utils.findKey(this, header);
return !!(key && this[key] !== undefined && (!matcher || matchHeaderValue(this, this[key], key, matcher)));
}
return false;
}
delete(header, matcher) {
const self = this;
let deleted = false;
function deleteHeader(_header) {
_header = normalizeHeader(_header);
if (_header) {
const key = utils.findKey(self, _header);
if (key && (!matcher || matchHeaderValue(self, self[key], key, matcher))) {
delete self[key];
deleted = true;
}
}
}
if (utils.isArray(header)) {
header.forEach(deleteHeader);
} else {
deleteHeader(header);
}
return deleted;
}
clear(matcher) {
const keys = Object.keys(this);
let i = keys.length;
let deleted = false;
while (i--) {
const key = keys[i];
if(!matcher || matchHeaderValue(this, this[key], key, matcher, true)) {
delete this[key];
deleted = true;
}
}
return deleted;
}
normalize(format) {
const self = this;
const headers = {};
utils.forEach(this, (value, header) => {
const key = utils.findKey(headers, header);
if (key) {
self[key] = normalizeValue(value);
delete self[header];
return;
}
const normalized = format ? formatHeader(header) : String(header).trim();
if (normalized !== header) {
delete self[header];
}
self[normalized] = normalizeValue(value);
headers[normalized] = true;
});
return this;
}
concat(...targets) {
return this.constructor.concat(this, ...targets);
}
toJSON(asStrings) {
const obj = Object.create(null);
utils.forEach(this, (value, header) => {
value != null && value !== false && (obj[header] = asStrings && utils.isArray(value) ? value.join(', ') : value);
});
return obj;
}
[Symbol.iterator]() {
return Object.entries(this.toJSON())[Symbol.iterator]();
}
toString() {
return Object.entries(this.toJSON()).map(([header, value]) => header + ': ' + value).join('\n');
}
getSetCookie() {
return this.get("set-cookie") || [];
}
get [Symbol.toStringTag]() {
return 'AxiosHeaders';
}
static from(thing) {
return thing instanceof this ? thing : new this(thing);
}
static concat(first, ...targets) {
const computed = new this(first);
targets.forEach((target) => computed.set(target));
return computed;
}
static accessor(header) {
const internals = this[$internals] = (this[$internals] = {
accessors: {}
});
const accessors = internals.accessors;
const prototype = this.prototype;
function defineAccessor(_header) {
const lHeader = normalizeHeader(_header);
if (!accessors[lHeader]) {
buildAccessors(prototype, _header);
accessors[lHeader] = true;
}
}
utils.isArray(header) ? header.forEach(defineAccessor) : defineAccessor(header);
return this;
}
}
AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent', 'Authorization']);
// reserved names hotfix
utils.reduceDescriptors(AxiosHeaders.prototype, ({value}, key) => {
let mapped = key[0].toUpperCase() + key.slice(1); // map `set` => `Set`
return {
get: () => value,
set(headerValue) {
this[mapped] = headerValue;
}
}
});
utils.freezeMethods(AxiosHeaders);
export default AxiosHeaders;

71
node_modules/axios/lib/core/InterceptorManager.js generated vendored Normal file
View File

@@ -0,0 +1,71 @@
'use strict';
import utils from './../utils.js';
class InterceptorManager {
constructor() {
this.handlers = [];
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
}
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*
* @returns {Boolean} `true` if the interceptor was removed, `false` otherwise
*/
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
/**
* Clear all interceptors from the stack
*
* @returns {void}
*/
clear() {
if (this.handlers) {
this.handlers = [];
}
}
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*
* @returns {void}
*/
forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}
}
export default InterceptorManager;

8
node_modules/axios/lib/core/README.md generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# axios // core
The modules found in `core/` should be modules that are specific to the domain logic of axios. These modules would most likely not make sense to be consumed outside of the axios module, as their logic is too specific. Some examples of core modules are:
- Dispatching requests
- Requests sent via `adapters/` (see lib/adapters/README.md)
- Managing interceptors
- Handling config

22
node_modules/axios/lib/core/buildFullPath.js generated vendored Normal file
View File

@@ -0,0 +1,22 @@
'use strict';
import isAbsoluteURL from '../helpers/isAbsoluteURL.js';
import combineURLs from '../helpers/combineURLs.js';
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
*
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
let isRelativeUrl = !isAbsoluteURL(requestedURL);
if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) {
return combineURLs(baseURL, requestedURL);
}
return requestedURL;
}

81
node_modules/axios/lib/core/dispatchRequest.js generated vendored Normal file
View File

@@ -0,0 +1,81 @@
'use strict';
import transformData from './transformData.js';
import isCancel from '../cancel/isCancel.js';
import defaults from '../defaults/index.js';
import CanceledError from '../cancel/CanceledError.js';
import AxiosHeaders from '../core/AxiosHeaders.js';
import adapters from "../adapters/adapters.js";
/**
* Throws a `CanceledError` if cancellation has been requested.
*
* @param {Object} config The config that is to be used for the request
*
* @returns {void}
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new CanceledError(null, config);
}
}
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
*
* @returns {Promise} The Promise to be fulfilled
*/
export default function dispatchRequest(config) {
throwIfCancellationRequested(config);
config.headers = AxiosHeaders.from(config.headers);
// Transform request data
config.data = transformData.call(
config,
config.transformRequest
);
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded', false);
}
const adapter = adapters.getAdapter(config.adapter || defaults.adapter, config);
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData.call(
config,
config.transformResponse,
response
);
response.headers = AxiosHeaders.from(response.headers);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
config.transformResponse,
reason.response
);
reason.response.headers = AxiosHeaders.from(reason.response.headers);
}
}
return Promise.reject(reason);
});
}

106
node_modules/axios/lib/core/mergeConfig.js generated vendored Normal file
View File

@@ -0,0 +1,106 @@
'use strict';
import utils from '../utils.js';
import AxiosHeaders from "./AxiosHeaders.js";
const headersToObject = (thing) => thing instanceof AxiosHeaders ? { ...thing } : thing;
/**
* Config-specific merge-function which creates a new config-object
* by merging two configuration objects together.
*
* @param {Object} config1
* @param {Object} config2
*
* @returns {Object} New object resulting from merging config2 to config1
*/
export default function mergeConfig(config1, config2) {
// eslint-disable-next-line no-param-reassign
config2 = config2 || {};
const config = {};
function getMergedValue(target, source, prop, caseless) {
if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
return utils.merge.call({caseless}, target, source);
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
} else if (utils.isArray(source)) {
return source.slice();
}
return source;
}
// eslint-disable-next-line consistent-return
function mergeDeepProperties(a, b, prop , caseless) {
if (!utils.isUndefined(b)) {
return getMergedValue(a, b, prop , caseless);
} else if (!utils.isUndefined(a)) {
return getMergedValue(undefined, a, prop , caseless);
}
}
// eslint-disable-next-line consistent-return
function valueFromConfig2(a, b) {
if (!utils.isUndefined(b)) {
return getMergedValue(undefined, b);
}
}
// eslint-disable-next-line consistent-return
function defaultToConfig2(a, b) {
if (!utils.isUndefined(b)) {
return getMergedValue(undefined, b);
} else if (!utils.isUndefined(a)) {
return getMergedValue(undefined, a);
}
}
// eslint-disable-next-line consistent-return
function mergeDirectKeys(a, b, prop) {
if (prop in config2) {
return getMergedValue(a, b);
} else if (prop in config1) {
return getMergedValue(undefined, a);
}
}
const mergeMap = {
url: valueFromConfig2,
method: valueFromConfig2,
data: valueFromConfig2,
baseURL: defaultToConfig2,
transformRequest: defaultToConfig2,
transformResponse: defaultToConfig2,
paramsSerializer: defaultToConfig2,
timeout: defaultToConfig2,
timeoutMessage: defaultToConfig2,
withCredentials: defaultToConfig2,
withXSRFToken: defaultToConfig2,
adapter: defaultToConfig2,
responseType: defaultToConfig2,
xsrfCookieName: defaultToConfig2,
xsrfHeaderName: defaultToConfig2,
onUploadProgress: defaultToConfig2,
onDownloadProgress: defaultToConfig2,
decompress: defaultToConfig2,
maxContentLength: defaultToConfig2,
maxBodyLength: defaultToConfig2,
beforeRedirect: defaultToConfig2,
transport: defaultToConfig2,
httpAgent: defaultToConfig2,
httpsAgent: defaultToConfig2,
cancelToken: defaultToConfig2,
socketPath: defaultToConfig2,
responseEncoding: defaultToConfig2,
validateStatus: mergeDirectKeys,
headers: (a, b , prop) => mergeDeepProperties(headersToObject(a), headersToObject(b),prop, true)
};
utils.forEach(Object.keys({...config1, ...config2}), function computeConfigValue(prop) {
const merge = mergeMap[prop] || mergeDeepProperties;
const configValue = merge(config1[prop], config2[prop], prop);
(utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
});
return config;
}

27
node_modules/axios/lib/core/settle.js generated vendored Normal file
View File

@@ -0,0 +1,27 @@
'use strict';
import AxiosError from './AxiosError.js';
/**
* Resolve or reject a Promise based on response status.
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*
* @returns {object} The response.
*/
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(new AxiosError(
'Request failed with status code ' + response.status,
[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
response.config,
response.request,
response
));
}
}

28
node_modules/axios/lib/core/transformData.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
'use strict';
import utils from './../utils.js';
import defaults from '../defaults/index.js';
import AxiosHeaders from '../core/AxiosHeaders.js';
/**
* Transform the data for a request or a response
*
* @param {Array|Function} fns A single function or Array of functions
* @param {?Object} response The response object
*
* @returns {*} The resulting transformed data
*/
export default function transformData(fns, response) {
const config = this || defaults;
const context = response || config;
const headers = AxiosHeaders.from(context.headers);
let data = context.data;
utils.forEach(fns, function transform(fn) {
data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
});
headers.normalize();
return data;
}

161
node_modules/axios/lib/defaults/index.js generated vendored Normal file
View File

@@ -0,0 +1,161 @@
'use strict';
import utils from '../utils.js';
import AxiosError from '../core/AxiosError.js';
import transitionalDefaults from './transitional.js';
import toFormData from '../helpers/toFormData.js';
import toURLEncodedForm from '../helpers/toURLEncodedForm.js';
import platform from '../platform/index.js';
import formDataToJSON from '../helpers/formDataToJSON.js';
/**
* It takes a string, tries to parse it, and if it fails, it returns the stringified version
* of the input
*
* @param {any} rawValue - The value to be stringified.
* @param {Function} parser - A function that parses a string into a JavaScript object.
* @param {Function} encoder - A function that takes a value and returns a string.
*
* @returns {string} A stringified version of the rawValue.
*/
function stringifySafely(rawValue, parser, encoder) {
if (utils.isString(rawValue)) {
try {
(parser || JSON.parse)(rawValue);
return utils.trim(rawValue);
} catch (e) {
if (e.name !== 'SyntaxError') {
throw e;
}
}
}
return (encoder || JSON.stringify)(rawValue);
}
const defaults = {
transitional: transitionalDefaults,
adapter: ['xhr', 'http', 'fetch'],
transformRequest: [function transformRequest(data, headers) {
const contentType = headers.getContentType() || '';
const hasJSONContentType = contentType.indexOf('application/json') > -1;
const isObjectPayload = utils.isObject(data);
if (isObjectPayload && utils.isHTMLForm(data)) {
data = new FormData(data);
}
const isFormData = utils.isFormData(data);
if (isFormData) {
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}
if (utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data) ||
utils.isReadableStream(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
headers.setContentType('application/x-www-form-urlencoded;charset=utf-8', false);
return data.toString();
}
let isFileList;
if (isObjectPayload) {
if (contentType.indexOf('application/x-www-form-urlencoded') > -1) {
return toURLEncodedForm(data, this.formSerializer).toString();
}
if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
const _FormData = this.env && this.env.FormData;
return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
}
}
if (isObjectPayload || hasJSONContentType ) {
headers.setContentType('application/json', false);
return stringifySafely(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
const transitional = this.transitional || defaults.transitional;
const forcedJSONParsing = transitional && transitional.forcedJSONParsing;
const JSONRequested = this.responseType === 'json';
if (utils.isResponse(data) || utils.isReadableStream(data)) {
return data;
}
if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
const silentJSONParsing = transitional && transitional.silentJSONParsing;
const strictJSONParsing = !silentJSONParsing && JSONRequested;
try {
return JSON.parse(data, this.parseReviver);
} catch (e) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response);
}
throw e;
}
}
}
return data;
}],
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
env: {
FormData: platform.classes.FormData,
Blob: platform.classes.Blob
},
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
headers: {
common: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': undefined
}
}
};
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch'], (method) => {
defaults.headers[method] = {};
});
export default defaults;

7
node_modules/axios/lib/defaults/transitional.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
export default {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
};

3
node_modules/axios/lib/env/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# axios // env
The `data.js` file is updated automatically when the package version is upgrading. Please do not edit it manually.

2
node_modules/axios/lib/env/classes/FormData.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
import _FormData from 'form-data';
export default typeof FormData !== 'undefined' ? FormData : _FormData;

1
node_modules/axios/lib/env/data.js generated vendored Normal file
View File

@@ -0,0 +1 @@
export const VERSION = "1.12.2";

143
node_modules/axios/lib/helpers/AxiosTransformStream.js generated vendored Normal file
View File

@@ -0,0 +1,143 @@
'use strict';
import stream from 'stream';
import utils from '../utils.js';
const kInternals = Symbol('internals');
class AxiosTransformStream extends stream.Transform{
constructor(options) {
options = utils.toFlatObject(options, {
maxRate: 0,
chunkSize: 64 * 1024,
minChunkSize: 100,
timeWindow: 500,
ticksRate: 2,
samplesCount: 15
}, null, (prop, source) => {
return !utils.isUndefined(source[prop]);
});
super({
readableHighWaterMark: options.chunkSize
});
const internals = this[kInternals] = {
timeWindow: options.timeWindow,
chunkSize: options.chunkSize,
maxRate: options.maxRate,
minChunkSize: options.minChunkSize,
bytesSeen: 0,
isCaptured: false,
notifiedBytesLoaded: 0,
ts: Date.now(),
bytes: 0,
onReadCallback: null
};
this.on('newListener', event => {
if (event === 'progress') {
if (!internals.isCaptured) {
internals.isCaptured = true;
}
}
});
}
_read(size) {
const internals = this[kInternals];
if (internals.onReadCallback) {
internals.onReadCallback();
}
return super._read(size);
}
_transform(chunk, encoding, callback) {
const internals = this[kInternals];
const maxRate = internals.maxRate;
const readableHighWaterMark = this.readableHighWaterMark;
const timeWindow = internals.timeWindow;
const divider = 1000 / timeWindow;
const bytesThreshold = (maxRate / divider);
const minChunkSize = internals.minChunkSize !== false ? Math.max(internals.minChunkSize, bytesThreshold * 0.01) : 0;
const pushChunk = (_chunk, _callback) => {
const bytes = Buffer.byteLength(_chunk);
internals.bytesSeen += bytes;
internals.bytes += bytes;
internals.isCaptured && this.emit('progress', internals.bytesSeen);
if (this.push(_chunk)) {
process.nextTick(_callback);
} else {
internals.onReadCallback = () => {
internals.onReadCallback = null;
process.nextTick(_callback);
};
}
}
const transformChunk = (_chunk, _callback) => {
const chunkSize = Buffer.byteLength(_chunk);
let chunkRemainder = null;
let maxChunkSize = readableHighWaterMark;
let bytesLeft;
let passed = 0;
if (maxRate) {
const now = Date.now();
if (!internals.ts || (passed = (now - internals.ts)) >= timeWindow) {
internals.ts = now;
bytesLeft = bytesThreshold - internals.bytes;
internals.bytes = bytesLeft < 0 ? -bytesLeft : 0;
passed = 0;
}
bytesLeft = bytesThreshold - internals.bytes;
}
if (maxRate) {
if (bytesLeft <= 0) {
// next time window
return setTimeout(() => {
_callback(null, _chunk);
}, timeWindow - passed);
}
if (bytesLeft < maxChunkSize) {
maxChunkSize = bytesLeft;
}
}
if (maxChunkSize && chunkSize > maxChunkSize && (chunkSize - maxChunkSize) > minChunkSize) {
chunkRemainder = _chunk.subarray(maxChunkSize);
_chunk = _chunk.subarray(0, maxChunkSize);
}
pushChunk(_chunk, chunkRemainder ? () => {
process.nextTick(_callback, null, chunkRemainder);
} : _callback);
};
transformChunk(chunk, function transformNextChunk(err, _chunk) {
if (err) {
return callback(err);
}
if (_chunk) {
transformChunk(_chunk, transformNextChunk);
} else {
callback(null);
}
});
}
}
export default AxiosTransformStream;

58
node_modules/axios/lib/helpers/AxiosURLSearchParams.js generated vendored Normal file
View File

@@ -0,0 +1,58 @@
'use strict';
import toFormData from './toFormData.js';
/**
* It encodes a string by replacing all characters that are not in the unreserved set with
* their percent-encoded equivalents
*
* @param {string} str - The string to encode.
*
* @returns {string} The encoded string.
*/
function encode(str) {
const charMap = {
'!': '%21',
"'": '%27',
'(': '%28',
')': '%29',
'~': '%7E',
'%20': '+',
'%00': '\x00'
};
return encodeURIComponent(str).replace(/[!'()~]|%20|%00/g, function replacer(match) {
return charMap[match];
});
}
/**
* It takes a params object and converts it to a FormData object
*
* @param {Object<string, any>} params - The parameters to be converted to a FormData object.
* @param {Object<string, any>} options - The options object passed to the Axios constructor.
*
* @returns {void}
*/
function AxiosURLSearchParams(params, options) {
this._pairs = [];
params && toFormData(params, this, options);
}
const prototype = AxiosURLSearchParams.prototype;
prototype.append = function append(name, value) {
this._pairs.push([name, value]);
};
prototype.toString = function toString(encoder) {
const _encode = encoder ? function(value) {
return encoder.call(this, value, encode);
} : encode;
return this._pairs.map(function each(pair) {
return _encode(pair[0]) + '=' + _encode(pair[1]);
}, '').join('&');
};
export default AxiosURLSearchParams;

Some files were not shown because too many files have changed in this diff Show More