Звонки: preflight сессии перед вызовом и retry; таймаут вынесен в настройки
This commit is contained in:
parent
ef0cd2cb7d
commit
6774c26ea1
17
AGENTS.md
17
AGENTS.md
@ -23,3 +23,20 @@
|
|||||||
- Если из запроса неясно, куда деплоить, обязательно сначала спросить пользователя, какой именно целевой контур нужен.
|
- Если из запроса неясно, куда деплоить, обязательно сначала спросить пользователя, какой именно целевой контур нужен.
|
||||||
- По умолчанию сначала деплой и проверка на тестовом контуре; на основной (`prod`) деплоить только после явного подтверждения пользователя, что версия проверена и готова.
|
- По умолчанию сначала деплой и проверка на тестовом контуре; на основной (`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
|
client.version=1.2.40
|
||||||
server.version=1.2.33
|
server.version=1.2.34
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export function render({ navigate }) {
|
|||||||
solanaServer: state.entrySettings.solanaServer,
|
solanaServer: state.entrySettings.solanaServer,
|
||||||
shineServer: state.entrySettings.shineServer,
|
shineServer: state.entrySettings.shineServer,
|
||||||
arweaveServer: state.entrySettings.arweaveServer,
|
arweaveServer: state.entrySettings.arweaveServer,
|
||||||
|
callPreflightTimeoutMs: Number(state.entrySettings.callPreflightTimeoutMs || 6000),
|
||||||
statuses: { ...state.entrySettings.statuses },
|
statuses: { ...state.entrySettings.statuses },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,6 +109,33 @@ export function render({ navigate }) {
|
|||||||
body.append(block);
|
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');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'auth-footer-actions';
|
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 = {
|
const TYPES = {
|
||||||
INVITE: 100,
|
INVITE: 100,
|
||||||
@ -31,6 +31,47 @@ function nowMs() {
|
|||||||
return Date.now();
|
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() {
|
function makeCallId() {
|
||||||
return `${Date.now()}${Math.floor(Math.random() * 1_000_000_000)}`;
|
return `${Date.now()}${Math.floor(Math.random() * 1_000_000_000)}`;
|
||||||
}
|
}
|
||||||
@ -1314,6 +1355,12 @@ export async function startOutgoingCall(peerLogin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callId = makeCallId();
|
const callId = makeCallId();
|
||||||
|
const preflightTimeoutMs = resolveCallPreflightTimeoutMs();
|
||||||
|
const preflightOk = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: false });
|
||||||
|
if (!preflightOk) {
|
||||||
|
throw new Error('Сервер временно недоступен');
|
||||||
|
}
|
||||||
|
|
||||||
const call = {
|
const call = {
|
||||||
callId,
|
callId,
|
||||||
peerLogin: cleanPeer,
|
peerLogin: cleanPeer,
|
||||||
@ -1359,6 +1406,19 @@ export async function startOutgoingCall(peerLogin) {
|
|||||||
try {
|
try {
|
||||||
await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
|
await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
|
||||||
} catch (error) {
|
} 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)}` });
|
await finalizeCall(call, { localReasonCode: 'error', debugReason: `invite_failed:${toErrorText(error)}` });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -1587,6 +1647,11 @@ export async function handleCallPushAction(action, payload = {}) {
|
|||||||
const normalized = String(action || '').trim().toLowerCase();
|
const normalized = String(action || '').trim().toLowerCase();
|
||||||
if (normalized !== 'accept' && normalized !== 'decline') return;
|
if (normalized !== 'accept' && normalized !== 'decline') return;
|
||||||
if (!isIncomingCallPushFresh(payload)) return;
|
if (!isIncomingCallPushFresh(payload)) return;
|
||||||
|
const timeoutMs = resolveCallPreflightTimeoutMs();
|
||||||
|
const ok = await ensureSessionForCall({ timeoutMs, force: false });
|
||||||
|
if (!ok) {
|
||||||
|
throw new Error('Не удалось подключиться, вызов завершён');
|
||||||
|
}
|
||||||
await handleIncomingCallPush(payload);
|
await handleIncomingCallPush(payload);
|
||||||
if (normalized === 'accept') {
|
if (normalized === 'accept') {
|
||||||
await acceptIncomingCall();
|
await acceptIncomingCall();
|
||||||
|
|||||||
@ -80,6 +80,7 @@ const LOCAL_WS_OVERRIDE_URL = readLocalWsOverrideUrl() || inferTunnelWsUrl();
|
|||||||
const DEFAULT_SOLANA_SERVER = 'https://api.devnet.solana.com';
|
const DEFAULT_SOLANA_SERVER = 'https://api.devnet.solana.com';
|
||||||
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
|
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
|
||||||
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
|
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
|
||||||
|
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
|
||||||
|
|
||||||
function loadStoredSession() {
|
function loadStoredSession() {
|
||||||
try {
|
try {
|
||||||
@ -146,6 +147,7 @@ function persistEntrySettings(settings) {
|
|||||||
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||||
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
|
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
|
||||||
arweaveServer: String(settings?.arweaveServer || DEFAULT_ARWEAVE_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: {
|
statuses: {
|
||||||
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
|
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
|
||||||
shineServer: String(settings?.statuses?.shineServer || 'idle'),
|
shineServer: String(settings?.statuses?.shineServer || 'idle'),
|
||||||
@ -208,6 +210,7 @@ function createInitialState({ withStoredSession = true } = {}) {
|
|||||||
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
|
||||||
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
|
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
|
||||||
arweaveServer: String(storedEntrySettings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
|
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: {
|
statuses: {
|
||||||
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
|
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
|
||||||
shineServer: String(storedEntrySettings?.statuses?.shineServer || 'idle'),
|
shineServer: String(storedEntrySettings?.statuses?.shineServer || 'idle'),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user