Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 55 additions & 23 deletions sources/realtime/RealtimeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import { t } from '@/text';
import { config } from '@/config';
import { requestMicrophonePermission, showMicrophonePermissionDeniedAlert } from '@/utils/microphonePermissions';

// Timeout for session operations to prevent hanging on poor networks
const SESSION_START_TIMEOUT_MS = 15000;

/**
* Wraps a promise with a timeout to prevent hanging on poor network conditions.
* This is critical for mobile users on cellular networks.
*/
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs)
)
]);
}

let voiceSession: VoiceSession | null = null;
let voiceSessionStarted: boolean = false;
let currentSessionId: string | null = null;
Expand All @@ -28,33 +44,41 @@ export async function startRealtimeSession(sessionId: string, initialContext?: s

const experimentsEnabled = storage.getState().settings.experiments;
const agentId = __DEV__ ? config.elevenLabsAgentIdDev : config.elevenLabsAgentIdProd;

if (!agentId) {
console.error('Agent ID not configured');
return;
}

try {
// Simple path: No experiments = no auth needed
if (!experimentsEnabled) {
currentSessionId = sessionId;
voiceSessionStarted = true;
await voiceSession.startSession({
sessionId,
initialContext,
agentId // Use agentId directly, no token
});
await withTimeout(
voiceSession.startSession({
sessionId,
initialContext,
agentId // Use agentId directly, no token
}),
SESSION_START_TIMEOUT_MS,
'Voice session start'
);
return;
}

// Experiments enabled = full auth flow
const credentials = await TokenStorage.getCredentials();
if (!credentials) {
Modal.alert(t('common.error'), t('errors.authenticationFailed'));
return;
}

const response = await fetchVoiceToken(credentials, sessionId);

const response = await withTimeout(
fetchVoiceToken(credentials, sessionId),
SESSION_START_TIMEOUT_MS,
'Voice token fetch'
);
console.log('[Voice] fetchVoiceToken response:', response);

if (!response.allowed) {
Expand All @@ -72,19 +96,27 @@ export async function startRealtimeSession(sessionId: string, initialContext?: s

if (response.token) {
// Use token from backend
await voiceSession.startSession({
sessionId,
initialContext,
token: response.token,
agentId: response.agentId
});
await withTimeout(
voiceSession.startSession({
sessionId,
initialContext,
token: response.token,
agentId: response.agentId
}),
SESSION_START_TIMEOUT_MS,
'Voice session start'
);
} else {
// No token (e.g. server not deployed yet) - use agentId directly
await voiceSession.startSession({
sessionId,
initialContext,
agentId
});
await withTimeout(
voiceSession.startSession({
sessionId,
initialContext,
agentId
}),
SESSION_START_TIMEOUT_MS,
'Voice session start'
);
}
} catch (error) {
console.error('Failed to start realtime session:', error);
Expand All @@ -98,7 +130,7 @@ export async function stopRealtimeSession() {
if (!voiceSession) {
return;
}

try {
await voiceSession.endSession();
currentSessionId = null;
Expand All @@ -125,4 +157,4 @@ export function getVoiceSession(): VoiceSession | null {

export function getCurrentRealtimeSessionId(): string | null {
return currentSessionId;
}
}
6 changes: 3 additions & 3 deletions sources/sync/apiSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ class ApiSocket {
},
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity
reconnectionDelay: 500, // Reduced from 1000ms for faster recovery
reconnectionDelayMax: 3000, // Reduced from 5000ms for faster recovery
reconnectionAttempts: Infinity,
});

this.setupEventHandlers();
Expand Down
2 changes: 1 addition & 1 deletion sources/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class Sync {
await this.registerPushToken();
}
this.pushTokenSync = new InvalidateSync(registerPushToken);
this.activityAccumulator = new ActivityUpdateAccumulator(this.flushActivityUpdates.bind(this), 2000);
this.activityAccumulator = new ActivityUpdateAccumulator(this.flushActivityUpdates.bind(this), 500); // Reduced from 2000ms for faster terminal feedback on mobile

// Listen for app state changes to refresh purchases
AppState.addEventListener('change', (nextAppState) => {
Expand Down