Files
Wand-Enhancer-luanslimadev/web-panel/src/remote-session/remote-session.client.ts
T
kitbyte a0b3968d33 refactor(web-panel): update architecture + cheat i18n
Reorganize the panel around product capabilities and integrate the
remote i18n feature from origin/master into the new structure.

- Restructure src into capability features (app, trainer, library,
  remote-session, appearance, shared); move the bridge to bridge/src.
- Add lingui-based UI localization and wrap remaining user-facing
  strings (Trans / msg macros); fix the 'END В·' mojibake.
- Port WeMod cheat-metadata i18n: capture the access token from the
  snapshot's renderer in the bridge and fetch localized trainer_meta
  in remote-session.i18n; guarantee snapshot sync on token failure.
- Wire vitest to the app's lingui/preact pipeline (mergeConfig).
2026-06-15 00:10:49 +03:00

135 lines
3.8 KiB
TypeScript

import { WEB_CONTRACT } from '../../protocol/contract';
import {
type HelloMessage,
type IncomingMessage,
type OutgoingMessage,
PROTOCOL_VERSION,
type RemoteCommandMessage,
type SetValueMessage,
} from '../../protocol/messages';
import { isIncomingMessage } from '../../protocol/validation';
type SocketHandlers = {
onConnecting: () => void;
onTransportOpen: () => void;
onMessage: (message: IncomingMessage) => void;
onClose: () => void;
onError: (message: string) => void;
};
let requestSequence = 0;
export class RemoteSessionClient {
private socket: WebSocket | null = null;
private intentionalDisconnect = false;
constructor(
private readonly url: string,
private readonly handlers: SocketHandlers,
) {}
connect(pairingToken?: string): void {
this.disconnect();
this.intentionalDisconnect = false;
this.handlers.onConnecting();
const socket = new WebSocket(this.url);
this.socket = socket;
socket.addEventListener('open', () => {
this.handlers.onTransportOpen();
this.send(this.createHelloMessage(pairingToken));
});
socket.addEventListener('message', (event) => this.handleMessage(event));
socket.addEventListener('close', () => {
if (this.socket === socket) {
this.socket = null;
}
if (!this.intentionalDisconnect) {
this.handlers.onClose();
}
});
socket.addEventListener('error', () => this.handlers.onError('WebSocket connection failed.'));
}
disconnect(): void {
this.intentionalDisconnect = true;
this.socket?.close();
this.socket = null;
}
isOpen(): boolean {
return Boolean(this.socket && this.socket.readyState === WebSocket.OPEN);
}
setValue(trainerId: string, target: string, value: unknown, cheatId?: string): string | null {
const requestId = createRequestId(`set_${target}`);
const message: SetValueMessage = {
type: 'set_value',
version: PROTOCOL_VERSION,
requestId,
payload: { trainerId, target, value, cheatId },
};
return this.send(message) ? requestId : null;
}
launchGame(gameId: string, titleId?: string): boolean {
return this.sendCommand('launch', gameId, titleId);
}
stopPlaying(gameId?: string, titleId?: string): boolean {
return this.sendCommand('stop', gameId, titleId);
}
private send(message: OutgoingMessage): boolean {
const socket = this.socket;
if (!socket || socket.readyState !== WebSocket.OPEN) {
return false;
}
socket.send(JSON.stringify(message));
return true;
}
private sendCommand(action: 'launch' | 'stop', gameId?: string, titleId?: string): boolean {
const message: RemoteCommandMessage = {
type: 'remote_command',
version: PROTOCOL_VERSION,
requestId: createRequestId(`command_${action}`),
payload: { action, gameId, titleId },
};
return this.send(message);
}
private createHelloMessage(pairingToken?: string): HelloMessage {
return {
type: 'hello',
version: PROTOCOL_VERSION,
requestId: createRequestId('hello'),
payload: {
client: 'mobile-web',
clientVersion: WEB_CONTRACT.clientVersion,
pairingToken,
capabilities: { supportsDeltaValues: true, supportsTrainerSwitch: true },
},
};
}
private handleMessage(event: MessageEvent): void {
try {
const parsed = JSON.parse(String(event.data)) as unknown;
if (!isIncomingMessage(parsed)) {
this.handlers.onError('Received an invalid protocol message.');
return;
}
this.handlers.onMessage(parsed);
} catch (error) {
this.handlers.onError(error instanceof Error ? error.message : 'Failed to parse websocket message.');
}
}
}
function createRequestId(prefix: string): string {
requestSequence += 1;
return `${prefix}_${Date.now()}_${requestSequence}`;
}