Files
Wand-Enhancer-k1tbyte/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js
T
kitbyte f2e88e9247 fix(renderer-scripts): await the bridge bind and retry it on poll
ipcRenderer.invoke rejects asynchronously, so marking the bridge bound before
awaiting left set-value dead for the session while the log reported success.

Compare installed-app snapshots structurally instead of by an explicit field
list, which had already drifted from the bridge copy and hid location changes.
2026-08-29 16:56:51 +03:00

261 lines
6.4 KiB
JavaScript
Vendored

import {
COMMAND_RESPONSE_CHANNEL,
REMOTE_COMMAND_LAUNCH,
REMOTE_COMMAND_STOP,
REMOTE_COMMAND_TRIGGER,
REMOTE_STOP_EVENT,
} from "./constants.js"
import { clearTrainerSnapshot, syncGameStatus } from "./game-status.js"
import {
getInstalledVersionsForGame,
rankInstalledAppCandidates,
resolveInstalledData,
} from "./installed-data.js"
import {
formatError,
getPreferredLocale,
invokeIpc,
isRecord,
safeString,
toStringId,
} from "./runtime.js"
export function handleRemoteCommandRequest(state, _event, request) {
void (async () => {
let response
if (request?.action === REMOTE_COMMAND_LAUNCH) {
response = await executeRemoteLaunchCommand(state, request)
} else if (request?.action === REMOTE_COMMAND_STOP) {
response = await executeRemoteStopCommand(state, request)
} else {
response = buildCommandResponse(request, false, {
code: "invalid_command",
message: "Unknown remote command.",
})
}
await sendRemoteCommandResponse(state, response)
})()
}
function buildCommandResponse(request, ok, error = null) {
const response = {
requestId: safeString(request?.requestId),
ok,
action:
request?.action === REMOTE_COMMAND_STOP
? REMOTE_COMMAND_STOP
: REMOTE_COMMAND_LAUNCH,
gameId: toStringId(request?.gameId),
titleId: toStringId(request?.titleId),
}
if (!error) {
return response
}
return {
...response,
error,
}
}
async function executeRemoteLaunchCommand(state, request) {
const gameId = toStringId(request?.gameId)
if (!gameId) {
return buildCommandResponse(request, false, {
code: "invalid_game",
message: "A game id is required to launch a trainer.",
})
}
if (!state.resolveRemoteCommandServices()) {
return buildCommandResponse(request, false, {
code: "bridge_not_ready",
message: "The Wand renderer container is not ready yet.",
})
}
if (!state.trainerService) {
return buildCommandResponse(request, false, {
code: "trainer_service_missing",
message: "The Wand trainer service is not available yet.",
})
}
if (!state.trainerLaunchRequestCtor) {
return buildCommandResponse(request, false, {
code: "trainer_launch_missing",
message:
"The Wand trainer launch request constructor is not available yet.",
})
}
const data = resolveInstalledData(state)
if (!data) {
return buildCommandResponse(request, false, {
code: "installations_missing",
message: "Installed game data is not available yet.",
})
}
const launchInfo = getLaunchInfoForGame(gameId, data)
if (!isRecord(launchInfo.app)) {
return buildCommandResponse(request, false, {
code: "game_not_installed",
message: "Wand could not resolve a preferred installation for this game.",
})
}
const trainerInfo = await resolveTrainerInfoForGame(state, gameId, data)
if (!trainerInfo) {
return buildCommandResponse(request, false, {
code: "trainer_not_found",
message: "Wand could not find a compatible trainer for this game.",
})
}
try {
const launchRequest = new state.trainerLaunchRequestCtor(
trainerInfo,
launchInfo.app,
launchInfo.version,
REMOTE_COMMAND_TRIGGER
)
await state.trainerService.launch(launchRequest)
state.queueSync(true)
state.queueFollowUpSync()
void syncGameStatus(state, true)
return buildCommandResponse(request, true)
} catch (error) {
state.log(
"warn",
"Remote trainer launch failed.",
formatError(error)
)
return buildCommandResponse(request, false, {
code: "launch_failed",
message: "Failed to launch the trainer.",
})
}
}
async function executeRemoteStopCommand(state, request) {
if (!state.resolveRemoteCommandServices()) {
return buildCommandResponse(request, false, {
code: "bridge_not_ready",
message: "The Wand renderer container is not ready yet.",
})
}
if (
!state.trainerService ||
typeof state.trainerService.endTrainer !== "function"
) {
return buildCommandResponse(request, false, {
code: "trainer_service_missing",
message: "The Wand trainer service is not available yet.",
})
}
if (
!state.trainerService.trainer &&
state.currentRunningTrainer.state !== "running"
) {
return buildCommandResponse(request, false, {
code: "no_active_trainer",
message: "No trainer is running right now.",
})
}
try {
await state.trainerService.endTrainer()
clearTrainerSnapshot(state, REMOTE_STOP_EVENT, true)
return buildCommandResponse(request, true)
} catch (error) {
state.log(
"warn",
"Remote trainer stop failed.",
formatError(error)
)
return buildCommandResponse(request, false, {
code: "stop_failed",
message: "Failed to stop the running trainer.",
})
}
}
function getLaunchInfoForGame(gameId, data) {
const versions = Array.isArray(data?.installedGameVersions?.[gameId])
? data.installedGameVersions[gameId]
: []
const game = isRecord(data?.catalog?.games?.[gameId])
? data.catalog.games[gameId]
: null
const top = rankInstalledAppCandidates(
data?.rawInstalledApps ?? {},
game,
versions
)[0]
return top ? { app: top.app, version: top.version } : { app: null, version: null }
}
async function resolveTrainerInfoForGame(state, gameId, data) {
if (!state.trainerApiService) {
return null
}
try {
const localTrainer = unwrapTrainerInfo(
await state.trainerApiService.getLatestLocalTrainerForGame(gameId)
)
if (localTrainer) {
return localTrainer
}
} catch (error) {
state.log(
"warn",
"Local trainer lookup failed.",
formatError(error)
)
}
try {
return unwrapTrainerInfo(
await state.trainerApiService.getMostCompatibleTrainerForGame(
gameId,
getPreferredLocale(),
getInstalledVersionsForGame(gameId, data),
false
)
)
} catch (error) {
state.log(
"warn",
"Compatible trainer lookup failed.",
formatError(error)
)
return null
}
}
function unwrapTrainerInfo(value) {
if (isRecord(value?.trainer)) {
return value.trainer
}
return isRecord(value) ? value : null
}
async function sendRemoteCommandResponse(state, response) {
await invokeIpc(
state,
COMMAND_RESPONSE_CHANNEL,
response,
"Remote command response"
)
}