Звонки: preflight сессии перед вызовом и retry; таймаут вынесен в настройки
This commit is contained in:
parent
ef0cd2cb7d
commit
6774c26ea1
17
AGENTS.md
17
AGENTS.md
@ -23,3 +23,20 @@
|
||||
- Если из запроса неясно, куда деплоить, обязательно сначала спросить пользователя, какой именно целевой контур нужен.
|
||||
- По умолчанию сначала деплой и проверка на тестовом контуре; на основной (`prod`) деплоить только после явного подтверждения пользователя, что версия проверена и готова.
|
||||
- При уточняющем вопросе отдельно предупреждать: деплой на основной выполнять только если точно подтверждена корректная работа.
|
||||
|
||||
## Логи звонков (установка соединения)
|
||||
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
|
||||
- На проде специальный файл для звонков:
|
||||
- `/home/player/SHiNE/shine-server/logs/call-delivery-events.log`
|
||||
- Общий серверный лог (и ротации) на проде:
|
||||
- `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
- `/home/player/SHiNE/shine-server/logs/app.YYYY-MM-DD.log`
|
||||
- Для анализа причин недозвона в первую очередь фильтровать записи по ключам:
|
||||
- `CallDeliveryReport`
|
||||
- `call_connected`
|
||||
- `outgoing_failed`
|
||||
- `incoming_failed`
|
||||
- `call_busy`
|
||||
- `call_declined`
|
||||
- `unknown_error`
|
||||
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.39
|
||||
server.version=1.2.33
|
||||
client.version=1.2.40
|
||||
server.version=1.2.34
|
||||
|
||||
@ -19,6 +19,7 @@ export function render({ navigate }) {
|
||||
solanaServer: state.entrySettings.solanaServer,
|
||||
shineServer: state.entrySettings.shineServer,
|
||||
arweaveServer: state.entrySettings.arweaveServer,
|
||||
callPreflightTimeoutMs: Number(state.entrySettings.callPreflightTimeoutMs || 6000),
|
||||
statuses: { ...state.entrySettings.statuses },
|
||||
};
|
||||
|
||||
@ -108,6 +109,33 @@ export function render({ navigate }) {
|
||||
body.append(block);
|
||||
});
|
||||
|
||||
const callSettings = document.createElement('div');
|
||||
callSettings.className = 'stack';
|
||||
|
||||
const callTimeoutLabel = document.createElement('label');
|
||||
callTimeoutLabel.className = 'field-label';
|
||||
callTimeoutLabel.textContent = 'Таймаут пред-подключения перед звонком (мс)';
|
||||
|
||||
const callTimeoutInput = document.createElement('input');
|
||||
callTimeoutInput.className = 'input';
|
||||
callTimeoutInput.type = 'number';
|
||||
callTimeoutInput.min = '1000';
|
||||
callTimeoutInput.max = '20000';
|
||||
callTimeoutInput.step = '500';
|
||||
callTimeoutInput.value = String(Math.max(1000, Math.min(20000, Number(draft.callPreflightTimeoutMs) || 6000)));
|
||||
callTimeoutInput.addEventListener('input', () => {
|
||||
const n = Number(callTimeoutInput.value);
|
||||
if (!Number.isFinite(n)) return;
|
||||
draft.callPreflightTimeoutMs = Math.max(1000, Math.min(20000, Math.round(n)));
|
||||
});
|
||||
|
||||
const callTimeoutHint = document.createElement('p');
|
||||
callTimeoutHint.className = 'meta-muted';
|
||||
callTimeoutHint.textContent = 'Перед исходящим звонком клиент проверяет и восстанавливает WS-сессию. Это время ожидания такой проверки перед ошибкой «Сервер временно недоступен».';
|
||||
|
||||
callSettings.append(callTimeoutLabel, callTimeoutInput, callTimeoutHint);
|
||||
body.append(callSettings);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { addSystemChatMessage, authService } from '../state.js';
|
||||
import { addSystemChatMessage, authService, authorizeSession, state } from '../state.js';
|
||||
|
||||
const TYPES = {
|
||||
INVITE: 100,
|
||||
@ -31,6 +31,47 @@ function nowMs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function resolveCallPreflightTimeoutMs() {
|
||||
const configured = Number(state?.entrySettings?.callPreflightTimeoutMs || 6000);
|
||||
return Math.max(1000, Math.min(20000, Number.isFinite(configured) ? configured : 6000));
|
||||
}
|
||||
|
||||
function isSessionReadyForCall() {
|
||||
const wsOpen = Boolean(authService?.ws?.ws && authService.ws.ws.readyState === WebSocket.OPEN);
|
||||
const hasSession = Boolean(state?.session?.isAuthorized && state?.session?.login && state?.session?.sessionId);
|
||||
return wsOpen && hasSession;
|
||||
}
|
||||
|
||||
async function withTimeout(promise, timeoutMs, timeoutMessage = 'timeout') {
|
||||
let timerId = 0;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => {
|
||||
timerId = window.setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timerId) window.clearTimeout(timerId);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSessionForCall({ timeoutMs, force = false } = {}) {
|
||||
if (!force && isSessionReadyForCall()) return true;
|
||||
const login = String(state?.session?.login || '').trim();
|
||||
const sessionId = String(state?.session?.sessionId || '').trim();
|
||||
if (!login || !sessionId) return false;
|
||||
|
||||
try {
|
||||
await withTimeout(authService.ws.open(), timeoutMs, 'call_preflight_ws_timeout');
|
||||
const resumed = await withTimeout(authService.resumeSession(login, sessionId), timeoutMs, 'call_preflight_resume_timeout');
|
||||
authorizeSession(resumed);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function makeCallId() {
|
||||
return `${Date.now()}${Math.floor(Math.random() * 1_000_000_000)}`;
|
||||
}
|
||||
@ -1314,6 +1355,12 @@ export async function startOutgoingCall(peerLogin) {
|
||||
}
|
||||
|
||||
const callId = makeCallId();
|
||||
const preflightTimeoutMs = resolveCallPreflightTimeoutMs();
|
||||
const preflightOk = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: false });
|
||||
if (!preflightOk) {
|
||||
throw new Error('Сервер временно недоступен');
|
||||
}
|
||||
|
||||
const call = {
|
||||
callId,
|
||||
peerLogin: cleanPeer,
|
||||
@ -1359,6 +1406,19 @@ export async function startOutgoingCall(peerLogin) {
|
||||
try {
|
||||
await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
|
||||
} catch (error) {
|
||||
const text = String(error?.message || '').toUpperCase();
|
||||
const isNotAuth = text.includes('NOT_AUTHENTICATED');
|
||||
if (isNotAuth) {
|
||||
const recovered = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: true });
|
||||
if (recovered) {
|
||||
try {
|
||||
await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
await finalizeCall(call, { localReasonCode: 'error', debugReason: 'invite_failed:not_authenticated_after_retry' });
|
||||
throw new Error('Сервер временно недоступен');
|
||||
}
|
||||
await finalizeCall(call, { localReasonCode: 'error', debugReason: `invite_failed:${toErrorText(error)}` });
|
||||
throw error;
|
||||
}
|
||||
@ -1587,6 +1647,11 @@ export async function handleCallPushAction(action, payload = {}) {
|
||||
const normalized = String(action || '').trim().toLowerCase();
|
||||
if (normalized !== 'accept' && normalized !== 'decline') return;
|
||||
if (!isIncomingCallPushFresh(payload)) return;
|
||||
const timeoutMs = resolveCallPreflightTimeoutMs();
|
||||
const ok = await ensureSessionForCall({ timeoutMs, force: false });
|
||||
if (!ok) {
|
||||
throw new Error('Не удалось подключиться, вызов завершён');
|
||||
}
|
||||
await handleIncomingCallPush(payload);
|
||||
if (normalized === 'accept') {
|
||||
await acceptIncomingCall();
|
||||
|
||||
@ -80,6 +80,7 @@ const LOCAL_WS_OVERRIDE_URL = readLocalWsOverrideUrl() || inferTunnelWsUrl();
|
||||
const DEFAULT_SOLANA_SERVER = 'https://api.devnet.solana.com';
|
||||
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
|
||||
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
|
||||
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
|
||||
|
||||
function loadStoredSession() {
|
||||
try {
|
||||
@ -146,6 +147,7 @@ function persistEntrySettings(settings) {
|
||||
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
|
||||
arweaveServer: String(settings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
|
||||
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(settings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
|
||||
statuses: {
|
||||
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
|
||||
shineServer: String(settings?.statuses?.shineServer || 'idle'),
|
||||
@ -208,6 +210,7 @@ function createInitialState({ withStoredSession = true } = {}) {
|
||||
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
|
||||
arweaveServer: String(storedEntrySettings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
|
||||
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(storedEntrySettings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
|
||||
statuses: {
|
||||
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
|
||||
shineServer: String(storedEntrySettings?.statuses?.shineServer || 'idle'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user