import {
createSession,
deleteSession,
getSession,
getStatus,
type WdaClientOptions,
} from '../executor/wda.js';
import type { WdaSession } from '../types/wda.js';
import { WdaConnectionError } from '../types/wda.js';
interface SessionState {
session: WdaSession | null;
bundleId?: string;
port: number;
}
// Sessions keyed by port to support multiple simulators
const sessions = new Map<number, SessionState>();
function getSessionState(port: number): SessionState {
let state = sessions.get(port);
if (!state) {
state = { session: null, bundleId: undefined, port };
sessions.set(port, state);
}
return state;
}
/**
* Get or create a WDA session.
* Sessions are reused across tool calls for efficiency.
* Sessions are validated before reuse to handle stale sessions.
*
* @param bundleId - Optional bundle ID to activate an app
* @param options - WDA client options including port
* @returns Session ID for use in WDA requests
*/
export async function getOrCreateSession(
bundleId?: string,
options?: WdaClientOptions
): Promise<string> {
const port = options?.port ?? 8100;
const state = getSessionState(port);
// If we have a session and bundle ID matches (or no bundle ID required), try to reuse it
if (state.session && (!bundleId || state.bundleId === bundleId)) {
// Verify session is still valid by checking the session endpoint
try {
const isValid = await getSession(state.session.sessionId, options);
if (isValid) {
return state.session.sessionId;
}
// Session no longer valid, clear it
state.session = null;
state.bundleId = undefined;
} catch (error) {
if (error instanceof WdaConnectionError) {
// WDA not running, propagate error
throw error;
}
// Other errors: clear session and try to create new one
state.session = null;
state.bundleId = undefined;
}
}
// Clean up existing session if bundle ID changed
if (state.session && bundleId && state.bundleId !== bundleId) {
try {
await deleteSession(state.session.sessionId, options);
} catch {
// Ignore cleanup errors
}
state.session = null;
state.bundleId = undefined;
}
// Create new session
const session = await createSession(bundleId, options);
state.session = session;
state.bundleId = bundleId;
return session.sessionId;
}
/**
* Clear the current session for a specific port.
* Useful for cleanup or switching apps.
*/
export async function clearSession(options?: WdaClientOptions): Promise<void> {
const port = options?.port ?? 8100;
const state = sessions.get(port);
if (state?.session) {
try {
await deleteSession(state.session.sessionId, options);
} catch {
// Ignore cleanup errors
}
}
// Remove from map entirely to avoid memory leak
sessions.delete(port);
}
/**
* Clear all cached sessions.
*/
export function clearAllSessions(): void {
sessions.clear();
}
/**
* Check if WDA is available and running on the specified port.
*/
export async function isWdaAvailable(options?: WdaClientOptions): Promise<boolean> {
try {
await getStatus(options);
return true;
} catch (error) {
if (error instanceof WdaConnectionError) {
return false;
}
throw error;
}
}
/**
* Get the current session ID without creating a new one.
* Returns null if no session exists.
*/
export function getCurrentSessionId(options?: WdaClientOptions): string | null {
const port = options?.port ?? 8100;
const state = sessions.get(port);
return state?.session?.sessionId ?? null;
}