import { PROTOCOL_VERSION } from './contract.js'; import type { IncomingMessage, OutgoingMessage } from './messages.js'; const INCOMING_TYPES = new Set([ 'hello_ack', 'trainer_meta', 'trainer_values', 'game_status', 'installed_apps', 'value_changed', 'trainer_changed', 'set_value_result', 'remote_command_result', 'error', ]); const OUTGOING_TYPES = new Set(['hello', 'set_value', 'remote_command']); export function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } export function isIncomingMessage(value: unknown): value is IncomingMessage { if (!isEnvelope(value) || !INCOMING_TYPES.has(value.type as IncomingMessage['type'])) { return false; } const payload = value.payload; switch (value.type) { case 'hello_ack': return ( hasString(payload, 'sessionId') && hasBoolean(payload, 'accepted') && hasString(payload, 'serverVersion') && hasNumber(payload, 'protocolVersion') ); case 'trainer_meta': return ( isRecord(payload.session) && hasString(payload.session, 'instanceId') && isRecord(payload.trainer) && hasString(payload.trainer, 'trainerId') && isRecord(payload.schema) && Array.isArray(payload.schema.categories) && Array.isArray(payload.schema.cheats) ); case 'trainer_values': return hasString(payload, 'trainerId') && isRecord(payload.values); case 'installed_apps': return ( hasString(payload, 'instanceId') && hasString(payload, 'updatedAt') && Array.isArray(payload.apps) ); case 'game_status': return ( hasString(payload, 'instanceId') && hasString(payload, 'updatedAt') && isRecord(payload.session) && isRecord(payload.trainer) ); case 'value_changed': return ( hasString(payload, 'trainerId') && hasString(payload, 'target') && 'value' in payload ); case 'trainer_changed': return hasString(payload, 'trainerId'); case 'set_value_result': return ( hasBoolean(payload, 'ok') && hasString(payload, 'trainerId') && hasString(payload, 'target') ); case 'remote_command_result': return ( hasBoolean(payload, 'ok') && (payload.action === 'launch' || payload.action === 'stop') ); case 'error': return hasString(payload, 'code') && hasString(payload, 'message'); default: return false; } } export function isOutgoingMessage(value: unknown): value is OutgoingMessage { if (!isEnvelope(value) || !OUTGOING_TYPES.has(value.type as OutgoingMessage['type'])) { return false; } const payload = value.payload; if (value.type === 'hello') { return ( payload.client === 'mobile-web' && hasString(payload, 'clientVersion') && isRecord(payload.capabilities) ); } if (value.type === 'set_value') { return ( hasString(payload, 'trainerId') && hasString(payload, 'target') && 'value' in payload ); } return payload.action === 'launch' || payload.action === 'stop'; } function isEnvelope(value: unknown): value is Record & { type: string; version: number; requestId: string | null; payload: Record; } { return ( isRecord(value) && typeof value.type === 'string' && value.version === PROTOCOL_VERSION && (value.requestId === null || typeof value.requestId === 'string') && isRecord(value.payload) ); } function hasString(value: Record, key: string): boolean { return typeof value[key] === 'string'; } function hasNumber(value: Record, key: string): boolean { return typeof value[key] === 'number'; } function hasBoolean(value: Record, key: string): boolean { return typeof value[key] === 'boolean'; }