main commit
This commit is contained in:
@@ -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)
|
||||
|
||||
4016
backend/god-eye.log
4016
backend/god-eye.log
File diff suppressed because it is too large
Load Diff
@@ -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
815
backend/public/mobile.html
Normal 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>
|
||||
486
backend/src/managers/ConnectionManager.js
Normal file
486
backend/src/managers/ConnectionManager.js
Normal 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 };
|
||||
515
backend/src/managers/ConnectionManager.js.backup
Normal file
515
backend/src/managers/ConnectionManager.js.backup
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление последнего времени активности
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
@@ -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
539
backend/test_chunk.js
Normal 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 {
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
69
docs/CACHE_CLEAR_INSTRUCTIONS.md
Normal file
69
docs/CACHE_CLEAR_INSTRUCTIONS.md
Normal 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
|
||||
- ✅ **Видно в операторе**: Смена видео при переключении камеры
|
||||
|
||||
---
|
||||
|
||||
**🚨 КРИТИЧЕСКИ ВАЖНО**: Обязательно очистите кэш браузера на телефоне, иначе исправления не будут работать!
|
||||
162
docs/COMPLETE_UI_TESTING_GUIDE.md
Normal file
162
docs/COMPLETE_UI_TESTING_GUIDE.md
Normal 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
316
docs/CONNECTION_MANAGER.md
Normal 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` (клиентские методы)
|
||||
72
docs/DESKTOP_APP_TESTING_GUIDE.md
Normal file
72
docs/DESKTOP_APP_TESTING_GUIDE.md
Normal 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**: выйти из полноэкранного режима
|
||||
112
docs/FIXED_SESSION_TESTING_GUIDE.md
Normal file
112
docs/FIXED_SESSION_TESTING_GUIDE.md
Normal 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. **Правильная синхронизация** - состояние между сервером и клиентом всегда синхронизировано
|
||||
|
||||
Теперь система сессий работает полноценно и позволяет эффективно управлять множественными подключениями!
|
||||
116
docs/IMPROVED_UI_TESTING_GUIDE.md
Normal file
116
docs/IMPROVED_UI_TESTING_GUIDE.md
Normal 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
107
docs/MOBILE_TEST_GUIDE.md
Normal 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 соединения
|
||||
288
docs/OPERATOR_TO_ANDROID_CONNECTION.md
Normal file
288
docs/OPERATOR_TO_ANDROID_CONNECTION.md
Normal 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 видеопотоком!
|
||||
484
docs/SERVER_TO_ANDROID_PROTOCOL.md
Normal file
484
docs/SERVER_TO_ANDROID_PROTOCOL.md
Normal 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
134
docs/WEBRTC_FIXES_GUIDE.md
Normal 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
52
god-eye.log
Normal 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
363
node_modules/.package-lock.json
generated
vendored
Normal 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
24
node_modules/@socket.io/component-emitter/LICENSE
generated
vendored
Normal 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
79
node_modules/@socket.io/component-emitter/Readme.md
generated
vendored
Normal 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
|
||||
179
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
Normal file
179
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
176
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
Normal file
176
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
Normal 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;
|
||||
};
|
||||
4
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
Normal file
4
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@socket.io/component-emitter",
|
||||
"type": "commonjs"
|
||||
}
|
||||
179
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
Normal file
179
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
169
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
Normal file
169
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
Normal 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;
|
||||
};
|
||||
4
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
Normal file
4
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
Normal 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
28
node_modules/@socket.io/component-emitter/package.json
generated
vendored
Normal 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
21
node_modules/asynckit/LICENSE
generated
vendored
Normal 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
233
node_modules/asynckit/README.md
generated
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
# asynckit [](https://www.npmjs.com/package/asynckit)
|
||||
|
||||
Minimal async jobs utility library, with streams support.
|
||||
|
||||
[](https://travis-ci.org/alexindigo/asynckit)
|
||||
[](https://travis-ci.org/alexindigo/asynckit)
|
||||
[](https://ci.appveyor.com/project/alexindigo/asynckit)
|
||||
|
||||
[](https://coveralls.io/github/alexindigo/asynckit?branch=master)
|
||||
[](https://david-dm.org/alexindigo/asynckit)
|
||||
[](https://www.bithound.io/github/alexindigo/asynckit)
|
||||
|
||||
<!-- [](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
76
node_modules/asynckit/bench.js
generated
vendored
Normal 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
6
node_modules/asynckit/index.js
generated
vendored
Normal 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
29
node_modules/asynckit/lib/abort.js
generated
vendored
Normal 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
34
node_modules/asynckit/lib/async.js
generated
vendored
Normal 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
26
node_modules/asynckit/lib/defer.js
generated
vendored
Normal 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
75
node_modules/asynckit/lib/iterate.js
generated
vendored
Normal 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
91
node_modules/asynckit/lib/readable_asynckit.js
generated
vendored
Normal 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
25
node_modules/asynckit/lib/readable_parallel.js
generated
vendored
Normal 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
25
node_modules/asynckit/lib/readable_serial.js
generated
vendored
Normal 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
29
node_modules/asynckit/lib/readable_serial_ordered.js
generated
vendored
Normal 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
37
node_modules/asynckit/lib/state.js
generated
vendored
Normal 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
141
node_modules/asynckit/lib/streamify.js
generated
vendored
Normal 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
29
node_modules/asynckit/lib/terminator.js
generated
vendored
Normal 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
63
node_modules/asynckit/package.json
generated
vendored
Normal 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
43
node_modules/asynckit/parallel.js
generated
vendored
Normal 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
17
node_modules/asynckit/serial.js
generated
vendored
Normal 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
75
node_modules/asynckit/serialOrdered.js
generated
vendored
Normal 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
21
node_modules/asynckit/stream.js
generated
vendored
Normal 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
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
7
node_modules/axios/LICENSE
generated
vendored
Normal 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
3
node_modules/axios/MIGRATION_GUIDE.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Migration Guide
|
||||
|
||||
## 0.x.x -> 1.1.0
|
||||
1756
node_modules/axios/README.md
generated
vendored
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
568
node_modules/axios/index.d.cts
generated
vendored
Normal 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
581
node_modules/axios/index.d.ts
generated
vendored
Normal 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
43
node_modules/axios/index.js
generated
vendored
Normal 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
37
node_modules/axios/lib/adapters/README.md
generated
vendored
Normal 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
81
node_modules/axios/lib/adapters/adapters.js
generated
vendored
Normal 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
288
node_modules/axios/lib/adapters/fetch.js
generated
vendored
Normal 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
713
node_modules/axios/lib/adapters/http.js
generated
vendored
Executable 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
200
node_modules/axios/lib/adapters/xhr.js
generated
vendored
Normal 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
89
node_modules/axios/lib/axios.js
generated
vendored
Normal 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
135
node_modules/axios/lib/cancel/CancelToken.js
generated
vendored
Normal 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
25
node_modules/axios/lib/cancel/CanceledError.js
generated
vendored
Normal 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
5
node_modules/axios/lib/cancel/isCancel.js
generated
vendored
Normal 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
240
node_modules/axios/lib/core/Axios.js
generated
vendored
Normal 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
110
node_modules/axios/lib/core/AxiosError.js
generated
vendored
Normal 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
314
node_modules/axios/lib/core/AxiosHeaders.js
generated
vendored
Normal 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
71
node_modules/axios/lib/core/InterceptorManager.js
generated
vendored
Normal 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
8
node_modules/axios/lib/core/README.md
generated
vendored
Normal 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
22
node_modules/axios/lib/core/buildFullPath.js
generated
vendored
Normal 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
81
node_modules/axios/lib/core/dispatchRequest.js
generated
vendored
Normal 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
106
node_modules/axios/lib/core/mergeConfig.js
generated
vendored
Normal 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
27
node_modules/axios/lib/core/settle.js
generated
vendored
Normal 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
28
node_modules/axios/lib/core/transformData.js
generated
vendored
Normal 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
161
node_modules/axios/lib/defaults/index.js
generated
vendored
Normal 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
7
node_modules/axios/lib/defaults/transitional.js
generated
vendored
Normal 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
3
node_modules/axios/lib/env/README.md
generated
vendored
Normal 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
2
node_modules/axios/lib/env/classes/FormData.js
generated
vendored
Normal 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
1
node_modules/axios/lib/env/data.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const VERSION = "1.12.2";
|
||||
143
node_modules/axios/lib/helpers/AxiosTransformStream.js
generated
vendored
Normal file
143
node_modules/axios/lib/helpers/AxiosTransformStream.js
generated
vendored
Normal 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
58
node_modules/axios/lib/helpers/AxiosURLSearchParams.js
generated
vendored
Normal 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
Reference in New Issue
Block a user