mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-09-04 08:13:18 +00:00
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.
This commit is contained in:
@@ -12,9 +12,12 @@ export const RETRY_DELAY_MS = 1000
|
||||
export const MAX_BOOTSTRAP_ATTEMPTS = 60
|
||||
export const SYNC_INTERVAL_MS = 15000
|
||||
export const OPTIONAL_SERVICES_RETRY_INTERVAL_MS = 1000
|
||||
export const MAX_OPTIONAL_SERVICES_ATTEMPTS = 60
|
||||
export const FOLLOW_UP_SYNC_DELAY_MS = 2500
|
||||
export const UNAVAILABLE_TITLES_BATCH_SIZE = 250
|
||||
export const BOOTSTRAP_LOG_THROTTLE_ATTEMPTS = 5
|
||||
export const CONTAINER_LOG_THROTTLE_ATTEMPTS = 10
|
||||
export const CONTAINER_GRAPH_MAX_DEPTH = 4
|
||||
// Wand's webpack module exports the trainer-launch-request class under key `vO`.
|
||||
// Required so `trainerService.launch(req)` records `getMetadata(vO)` state in Wand. See AGENTS.md "Remote Play".
|
||||
export const TRAINER_LAUNCH_REQUEST_EXPORT_KEY = "vO"
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TRAINER_ENDED_EVENT,
|
||||
TRAINER_SNAPSHOT_CHANNEL,
|
||||
} from "./constants.js"
|
||||
import { isRecord, safeString, toStringId } from "./runtime.js"
|
||||
import { invokeIpc, isRecord, safeString, toStringId } from "./runtime.js"
|
||||
|
||||
export function createIdleGameSession() {
|
||||
return {
|
||||
@@ -97,19 +97,7 @@ export function clearTrainerSnapshot(state, reason, clearSession = false) {
|
||||
}
|
||||
|
||||
void syncGameStatus(state, true)
|
||||
if (!state.ipcRenderer) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
void state.ipcRenderer.invoke(TRAINER_SNAPSHOT_CHANNEL, null)
|
||||
} catch (error) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Trainer snapshot clear IPC failed.",
|
||||
error?.stack || String(error)
|
||||
)
|
||||
}
|
||||
void invokeIpc(state, TRAINER_SNAPSHOT_CHANNEL, null, "Trainer snapshot clear")
|
||||
}
|
||||
|
||||
export async function syncGameStatus(state, force = false) {
|
||||
@@ -125,22 +113,21 @@ export async function syncGameStatus(state, force = false) {
|
||||
|
||||
state.lastGameStatusSignature = signature
|
||||
|
||||
try {
|
||||
await state.ipcRenderer.invoke(GAME_STATUS_CHANNEL, snapshot)
|
||||
const sent = await invokeIpc(
|
||||
state,
|
||||
GAME_STATUS_CHANNEL,
|
||||
snapshot,
|
||||
"Game status snapshot",
|
||||
"error"
|
||||
)
|
||||
if (sent) {
|
||||
state.log(
|
||||
"info",
|
||||
"Game status snapshot sent.",
|
||||
`session=${snapshot.session.state}/${snapshot.session.event}, trainer=${snapshot.trainer.state}/${snapshot.trainer.event}`
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
state.log(
|
||||
"error",
|
||||
"Game status snapshot IPC failed.",
|
||||
error?.stack || String(error)
|
||||
)
|
||||
return false
|
||||
}
|
||||
return sent
|
||||
}
|
||||
|
||||
function installLifecycleSubscriptions(state) {
|
||||
|
||||
+52
-23
@@ -2,9 +2,11 @@ import {
|
||||
BIND_CHANNEL,
|
||||
BOOTSTRAP_LOG_THROTTLE_ATTEMPTS,
|
||||
COMMAND_REQUEST_CHANNEL,
|
||||
CONTAINER_LOG_THROTTLE_ATTEMPTS,
|
||||
FOLLOW_UP_SYNC_DELAY_MS,
|
||||
GLOBAL_FLAG,
|
||||
MAX_BOOTSTRAP_ATTEMPTS,
|
||||
MAX_OPTIONAL_SERVICES_ATTEMPTS,
|
||||
OPTIONAL_SERVICES_RETRY_INTERVAL_MS,
|
||||
RETRY_DELAY_MS,
|
||||
SYNC_CHANNEL,
|
||||
@@ -24,11 +26,13 @@ import {
|
||||
import { createLogger } from "./logger.js"
|
||||
import { handleRemoteCommandRequest } from "./remote-commands.js"
|
||||
import {
|
||||
formatError,
|
||||
getAppRoot,
|
||||
getAureliaContainer,
|
||||
getRequire,
|
||||
getWebpackRequire,
|
||||
hasAppRoot,
|
||||
invokeIpc,
|
||||
isRecord,
|
||||
summarizeAureliaSubtree,
|
||||
} from "./runtime.js"
|
||||
@@ -36,6 +40,7 @@ import {
|
||||
getInstalledAppsService,
|
||||
getStoreRef,
|
||||
hasMissingOptionalServices,
|
||||
hasUnresolvedServices,
|
||||
resolveOptionalServices,
|
||||
} from "./services.js"
|
||||
|
||||
@@ -70,7 +75,9 @@ function createState(WandEnhancer) {
|
||||
pollTimer: null,
|
||||
optionalServicesTimer: null,
|
||||
bootstrapAttempts: 0,
|
||||
optionalServicesAttempts: 0,
|
||||
bridgeBound: false,
|
||||
bridgeBinding: false,
|
||||
refreshPatched: false,
|
||||
installedAppsService: null,
|
||||
gameLifecycleService: null,
|
||||
@@ -114,13 +121,11 @@ function setBootstrapReason(state, reason) {
|
||||
)
|
||||
}
|
||||
|
||||
function bindBridge(state) {
|
||||
if (state.bridgeBound || !state.ipcRenderer) {
|
||||
async function bindBridge(state) {
|
||||
if (state.bridgeBound || state.bridgeBinding || !state.ipcRenderer) {
|
||||
return
|
||||
}
|
||||
|
||||
state.bridgeBound = true
|
||||
|
||||
if (!state.commandListenerInstalled) {
|
||||
state.ipcRenderer.on(COMMAND_REQUEST_CHANNEL, (event, request) =>
|
||||
handleRemoteCommandRequest(state, event, request)
|
||||
@@ -129,11 +134,18 @@ function bindBridge(state) {
|
||||
state.log("info", "Bridge remote command handler installed.")
|
||||
}
|
||||
|
||||
// Await the bind: invoke rejects asynchronously, so marking the bridge bound up
|
||||
// front left set-value permanently dead whenever the main-process handler was not
|
||||
// registered yet - and the log still claimed success.
|
||||
state.bridgeBinding = true
|
||||
try {
|
||||
void state.ipcRenderer.invoke(BIND_CHANNEL)
|
||||
state.log("info", "Bridge set-value handler bind requested.")
|
||||
await state.ipcRenderer.invoke(BIND_CHANNEL)
|
||||
state.bridgeBound = true
|
||||
state.log("info", "Bridge set-value handler bound.")
|
||||
} catch (error) {
|
||||
state.log("warn", "Bridge bind failed.", error?.stack || String(error))
|
||||
state.log("warn", "Bridge bind failed; will retry on the next sync.", formatError(error))
|
||||
} finally {
|
||||
state.bridgeBinding = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,22 +182,21 @@ async function syncInstalledApps(state, force = false) {
|
||||
|
||||
state.lastSignature = signature
|
||||
|
||||
try {
|
||||
await state.ipcRenderer.invoke(SYNC_CHANNEL, snapshot)
|
||||
const sent = await invokeIpc(
|
||||
state,
|
||||
SYNC_CHANNEL,
|
||||
snapshot,
|
||||
"Installed apps snapshot",
|
||||
"error"
|
||||
)
|
||||
if (sent) {
|
||||
state.log(
|
||||
"info",
|
||||
"Installed apps snapshot sent.",
|
||||
`apps=${snapshot.apps.length}, catalogGames=${snapshot.diagnostics.catalogGames}, rawInstalledApps=${snapshot.diagnostics.rawInstalledApps}`
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
state.log(
|
||||
"error",
|
||||
"Installed apps snapshot IPC failed.",
|
||||
error?.stack || String(error)
|
||||
)
|
||||
return false
|
||||
}
|
||||
return sent
|
||||
}
|
||||
|
||||
function queueSync(state, force = false) {
|
||||
@@ -269,11 +280,25 @@ function startOptionalServicesRetry(state) {
|
||||
}
|
||||
|
||||
state.optionalServicesTimer = setInterval(() => {
|
||||
state.optionalServicesAttempts += 1
|
||||
|
||||
const container = getAureliaContainer()
|
||||
const webpackRequire = getWebpackRequire()
|
||||
if (container && webpackRequire) {
|
||||
resolveRuntimeServices(state, container, webpackRequire)
|
||||
}
|
||||
|
||||
if (
|
||||
state.optionalServicesTimer &&
|
||||
state.optionalServicesAttempts >= MAX_OPTIONAL_SERVICES_ATTEMPTS
|
||||
) {
|
||||
stopOptionalServicesRetry(state)
|
||||
state.log(
|
||||
"warn",
|
||||
"Optional service retry exhausted.",
|
||||
`attempts=${state.optionalServicesAttempts}`
|
||||
)
|
||||
}
|
||||
}, OPTIONAL_SERVICES_RETRY_INTERVAL_MS)
|
||||
|
||||
state.log(
|
||||
@@ -341,7 +366,7 @@ function bootstrap(state) {
|
||||
}
|
||||
}
|
||||
|
||||
bindBridge(state)
|
||||
void bindBridge(state)
|
||||
patchRefreshApps(state)
|
||||
queueSync(state, true)
|
||||
queueFollowUpSync(state)
|
||||
@@ -369,11 +394,15 @@ function startPollTimer(state) {
|
||||
}
|
||||
|
||||
state.pollTimer = setInterval(() => {
|
||||
const container = getAureliaContainer()
|
||||
const webpackRequire = getWebpackRequire()
|
||||
if (container && webpackRequire) {
|
||||
resolveRuntimeServices(state, container, webpackRequire)
|
||||
if (hasUnresolvedServices(state)) {
|
||||
const container = getAureliaContainer()
|
||||
const webpackRequire = getWebpackRequire()
|
||||
if (container && webpackRequire) {
|
||||
resolveRuntimeServices(state, container, webpackRequire)
|
||||
}
|
||||
}
|
||||
|
||||
void bindBridge(state)
|
||||
void syncInstalledApps(state)
|
||||
}, SYNC_INTERVAL_MS)
|
||||
|
||||
@@ -385,7 +414,7 @@ function startPollTimer(state) {
|
||||
}
|
||||
|
||||
function logMissingContainer(state) {
|
||||
if (state.bootstrapAttempts % 10 !== 0) {
|
||||
if (state.bootstrapAttempts % CONTAINER_LOG_THROTTLE_ATTEMPTS !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "./artwork.js"
|
||||
import {
|
||||
getBasename,
|
||||
formatError,
|
||||
isRecord,
|
||||
normalizeStringList,
|
||||
safeString,
|
||||
@@ -140,7 +141,8 @@ export function buildSnapshot(state) {
|
||||
|
||||
const preferredApp = pickPreferredInstalledApp(
|
||||
rawInstalledApps,
|
||||
getCatalogGameCorrelationIds(game, versions)
|
||||
game,
|
||||
versions
|
||||
)
|
||||
if (!preferredApp) {
|
||||
continue
|
||||
@@ -192,7 +194,7 @@ export function buildSnapshot(state) {
|
||||
|
||||
const preferredApp = pickPreferredInstalledApp(
|
||||
rawInstalledApps,
|
||||
game.correlationIds
|
||||
game
|
||||
)
|
||||
if (!preferredApp) {
|
||||
continue
|
||||
@@ -221,7 +223,7 @@ export function buildSnapshot(state) {
|
||||
}
|
||||
}
|
||||
|
||||
const apps = Array.from(entriesByKey.values()).sort(compareSnapshotEntries)
|
||||
const apps = Array.from(entriesByKey.values()).sort(compareInstalledAppRecords)
|
||||
|
||||
return {
|
||||
instanceId: "wand-installed-apps",
|
||||
@@ -241,22 +243,13 @@ export function buildSnapshot(state) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Structural, not field-by-field. This used to enumerate fields and had already
|
||||
* drifted from the bridge's copy (it listed `location`, the bridge's did not), so a
|
||||
* game moving install directory never reached the panel.
|
||||
*/
|
||||
export function makeInstalledAppsSignature(snapshot) {
|
||||
return snapshot.apps
|
||||
.map((app) =>
|
||||
[
|
||||
app.platform,
|
||||
app.sku,
|
||||
app.displayName,
|
||||
app.gameId ?? "",
|
||||
app.titleId ?? "",
|
||||
app.location,
|
||||
app.imageUrl ?? "",
|
||||
app.platformLastPlayedTimestamp ?? "",
|
||||
app.platformTotalPlaytimeMinutes ?? "",
|
||||
].join("|")
|
||||
)
|
||||
.join("\n")
|
||||
return JSON.stringify(snapshot.apps)
|
||||
}
|
||||
|
||||
export function toInstalledAppRecord(correlationId, app) {
|
||||
@@ -407,7 +400,7 @@ async function fetchUnavailableTitles(state, correlationIds) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Unavailable titles refresh failed.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
} finally {
|
||||
state.unavailableTitlesFetchPromise = null
|
||||
@@ -469,44 +462,60 @@ function normalizeUnavailableTitleGame(game) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCatalogGameCorrelationIds(game, versions) {
|
||||
const correlationIds = []
|
||||
function collectCorrelationIds(game, versions) {
|
||||
const entries = []
|
||||
|
||||
if (Array.isArray(game.correlationIds)) {
|
||||
if (Array.isArray(game?.correlationIds)) {
|
||||
for (const correlationId of game.correlationIds) {
|
||||
if (typeof correlationId === "string" && correlationId.trim()) {
|
||||
correlationIds.push(correlationId.trim())
|
||||
entries.push({ correlationId: correlationId.trim(), version: null })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of versions) {
|
||||
if (
|
||||
typeof version?.correlationId === "string" &&
|
||||
version.correlationId.trim()
|
||||
) {
|
||||
correlationIds.push(version.correlationId.trim())
|
||||
if (Array.isArray(versions)) {
|
||||
for (const version of versions) {
|
||||
if (
|
||||
typeof version?.correlationId === "string" &&
|
||||
version.correlationId.trim()
|
||||
) {
|
||||
entries.push({
|
||||
correlationId: version.correlationId.trim(),
|
||||
version: version.version ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return correlationIds
|
||||
return entries
|
||||
}
|
||||
|
||||
function pickPreferredInstalledApp(rawInstalledApps, correlationIds) {
|
||||
const candidates = Array.from(new Set(correlationIds))
|
||||
.map((correlationId) =>
|
||||
toInstalledAppRecord(correlationId, rawInstalledApps[correlationId])
|
||||
)
|
||||
export function rankInstalledAppCandidates(rawInstalledApps, game, versions) {
|
||||
return Array.from(
|
||||
new Map(
|
||||
collectCorrelationIds(game, versions).map((entry) => [
|
||||
entry.correlationId,
|
||||
entry,
|
||||
])
|
||||
).values()
|
||||
)
|
||||
.map((entry) => {
|
||||
const app = rawInstalledApps?.[entry.correlationId]
|
||||
const record = toInstalledAppRecord(entry.correlationId, app)
|
||||
return record ? { app, version: entry.version ?? null, record } : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort(compareInstalledAppRecords)
|
||||
.sort((left, right) => compareInstalledAppRecords(left.record, right.record))
|
||||
}
|
||||
|
||||
return candidates[0] || null
|
||||
function pickPreferredInstalledApp(rawInstalledApps, game, versions) {
|
||||
return rankInstalledAppCandidates(rawInstalledApps, game, versions)[0]?.record || null
|
||||
}
|
||||
|
||||
function upsertSnapshotEntry(entriesByKey, entry) {
|
||||
const key = getSnapshotEntryKey(entry)
|
||||
const current = entriesByKey.get(key)
|
||||
if (!current || compareSnapshotEntries(entry, current) < 0) {
|
||||
if (!current || compareInstalledAppRecords(entry, current) < 0) {
|
||||
entriesByKey.set(key, entry)
|
||||
}
|
||||
}
|
||||
@@ -523,24 +532,6 @@ function getSnapshotEntryKey(entry) {
|
||||
return `${SNAPSHOT_ENTRY_KEY_PREFIX.APP}${entry.correlationId}`
|
||||
}
|
||||
|
||||
function compareSnapshotEntries(left, right) {
|
||||
const lastPlayedDiff =
|
||||
(right.platformLastPlayedTimestamp ?? 0) -
|
||||
(left.platformLastPlayedTimestamp ?? 0)
|
||||
if (lastPlayedDiff !== 0) {
|
||||
return lastPlayedDiff
|
||||
}
|
||||
|
||||
const playtimeDiff =
|
||||
(right.platformTotalPlaytimeMinutes ?? 0) -
|
||||
(left.platformTotalPlaytimeMinutes ?? 0)
|
||||
if (playtimeDiff !== 0) {
|
||||
return playtimeDiff
|
||||
}
|
||||
|
||||
return compareByIdentity(left, right)
|
||||
}
|
||||
|
||||
function compareByIdentity(left, right) {
|
||||
const displayNameDiff = left.displayName.localeCompare(right.displayName)
|
||||
if (displayNameDiff !== 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LOG_FILE_NAME, LOG_PREFIX } from "./constants.js"
|
||||
import { getRequire } from "./runtime.js"
|
||||
|
||||
// Logging runs inside Wand's own process; a failure here must never take the app down.
|
||||
export function createLogger(WandEnhancer) {
|
||||
let filePath = null
|
||||
|
||||
@@ -12,7 +13,7 @@ export function createLogger(WandEnhancer) {
|
||||
filePath = path.join(os.tmpdir(), LOG_FILE_NAME)
|
||||
globalThis.__wandInstalledAppsSyncLogFile = filePath
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
return function log(level, message, detail) {
|
||||
const method =
|
||||
@@ -21,13 +22,13 @@ export function createLogger(WandEnhancer) {
|
||||
|
||||
try {
|
||||
console[method](LOG_PREFIX, message, detail || "")
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (WandEnhancer?.log) {
|
||||
WandEnhancer.log(`${LOG_PREFIX} ${message}`, detail || "")
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
writeFile(filePath, line)
|
||||
}
|
||||
@@ -42,5 +43,5 @@ function writeFile(filePath, line) {
|
||||
const require = getRequire()
|
||||
const fs = require?.("node:fs")
|
||||
fs?.appendFileSync(filePath, `${line}\n`)
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import {
|
||||
} from "./constants.js"
|
||||
import { clearTrainerSnapshot, syncGameStatus } from "./game-status.js"
|
||||
import {
|
||||
compareInstalledAppRecords,
|
||||
getInstalledVersionsForGame,
|
||||
rankInstalledAppCandidates,
|
||||
resolveInstalledData,
|
||||
toInstalledAppRecord,
|
||||
} from "./installed-data.js"
|
||||
import {
|
||||
formatError,
|
||||
getPreferredLocale,
|
||||
invokeIpc,
|
||||
isRecord,
|
||||
safeString,
|
||||
toStringId,
|
||||
@@ -131,7 +132,7 @@ async function executeRemoteLaunchCommand(state, request) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Remote trainer launch failed.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return buildCommandResponse(request, false, {
|
||||
code: "launch_failed",
|
||||
@@ -176,7 +177,7 @@ async function executeRemoteStopCommand(state, request) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Remote trainer stop failed.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return buildCommandResponse(request, false, {
|
||||
code: "stop_failed",
|
||||
@@ -192,47 +193,14 @@ function getLaunchInfoForGame(gameId, data) {
|
||||
const game = isRecord(data?.catalog?.games?.[gameId])
|
||||
? data.catalog.games[gameId]
|
||||
: null
|
||||
const candidates = []
|
||||
|
||||
if (Array.isArray(game?.correlationIds)) {
|
||||
for (const correlationId of game.correlationIds) {
|
||||
if (typeof correlationId === "string" && correlationId.trim()) {
|
||||
candidates.push({ correlationId: correlationId.trim(), version: null })
|
||||
}
|
||||
}
|
||||
}
|
||||
const top = rankInstalledAppCandidates(
|
||||
data?.rawInstalledApps ?? {},
|
||||
game,
|
||||
versions
|
||||
)[0]
|
||||
|
||||
for (const versionEntry of versions) {
|
||||
if (
|
||||
typeof versionEntry?.correlationId === "string" &&
|
||||
versionEntry.correlationId.trim()
|
||||
) {
|
||||
candidates.push({
|
||||
correlationId: versionEntry.correlationId.trim(),
|
||||
version: versionEntry.version ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rankedCandidates = Array.from(
|
||||
new Map(
|
||||
candidates.map((candidate) => [candidate.correlationId, candidate])
|
||||
).values()
|
||||
)
|
||||
.map((candidate) => normalizeLaunchCandidate(candidate, data))
|
||||
.filter(Boolean)
|
||||
.sort((left, right) =>
|
||||
compareInstalledAppRecords(left.normalizedApp, right.normalizedApp)
|
||||
)
|
||||
|
||||
if (!rankedCandidates[0]) {
|
||||
return { app: null, version: null }
|
||||
}
|
||||
|
||||
return {
|
||||
app: rankedCandidates[0].app,
|
||||
version: rankedCandidates[0].version,
|
||||
}
|
||||
return top ? { app: top.app, version: top.version } : { app: null, version: null }
|
||||
}
|
||||
|
||||
async function resolveTrainerInfoForGame(state, gameId, data) {
|
||||
@@ -251,7 +219,7 @@ async function resolveTrainerInfoForGame(state, gameId, data) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Local trainer lookup failed.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -268,26 +236,12 @@ async function resolveTrainerInfoForGame(state, gameId, data) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Compatible trainer lookup failed.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLaunchCandidate(candidate, data) {
|
||||
const app = data?.rawInstalledApps?.[candidate.correlationId]
|
||||
const normalizedApp = toInstalledAppRecord(candidate.correlationId, app)
|
||||
if (!normalizedApp || !isRecord(app)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
app,
|
||||
version: candidate.version,
|
||||
normalizedApp,
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapTrainerInfo(value) {
|
||||
if (isRecord(value?.trainer)) {
|
||||
return value.trainer
|
||||
@@ -297,17 +251,10 @@ function unwrapTrainerInfo(value) {
|
||||
}
|
||||
|
||||
async function sendRemoteCommandResponse(state, response) {
|
||||
if (!state.ipcRenderer) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await state.ipcRenderer.invoke(COMMAND_RESPONSE_CHANNEL, response)
|
||||
} catch (error) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Remote command response IPC failed.",
|
||||
error?.stack || String(error)
|
||||
)
|
||||
}
|
||||
await invokeIpc(
|
||||
state,
|
||||
COMMAND_RESPONSE_CHANNEL,
|
||||
response,
|
||||
"Remote command response"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,50 @@
|
||||
import { CONTAINER_GRAPH_MAX_DEPTH } from "./constants.js"
|
||||
|
||||
export function isRecord(value) {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
export function formatError(error) {
|
||||
return error?.stack || String(error)
|
||||
}
|
||||
|
||||
export async function invokeIpc(state, channel, payload, label, level = "warn") {
|
||||
if (!state.ipcRenderer) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await state.ipcRenderer.invoke(channel, payload)
|
||||
return true
|
||||
} catch (error) {
|
||||
state.log(level, `${label} IPC failed.`, formatError(error))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isDiagnosticsDebugEnabled() {
|
||||
return globalThis.__wandInstalledAppsSyncDebug === true
|
||||
}
|
||||
|
||||
let lastPredicateErrorLogAt = 0
|
||||
|
||||
function logContainerGraphPredicateError(error) {
|
||||
if (!isDiagnosticsDebugEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (now - lastPredicateErrorLogAt < 1000) {
|
||||
return
|
||||
}
|
||||
|
||||
lastPredicateErrorLogAt = now
|
||||
console.debug(
|
||||
"[wand-installed-apps-sync] container graph predicate threw",
|
||||
formatError(error)
|
||||
)
|
||||
}
|
||||
|
||||
export function getRequire() {
|
||||
return (
|
||||
globalThis.require ||
|
||||
@@ -57,7 +100,26 @@ export function hasAppRoot() {
|
||||
return Boolean(getAppRoot())
|
||||
}
|
||||
|
||||
let cachedAureliaContainer = null
|
||||
|
||||
function isUsableContainer(container) {
|
||||
return isRecord(container) && typeof container.get === "function"
|
||||
}
|
||||
|
||||
export function getAureliaContainer() {
|
||||
if (isUsableContainer(cachedAureliaContainer)) {
|
||||
return cachedAureliaContainer
|
||||
}
|
||||
|
||||
const resolved = resolveAureliaContainer()
|
||||
if (resolved) {
|
||||
cachedAureliaContainer = resolved
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
function resolveAureliaContainer() {
|
||||
const root = getAppRoot()
|
||||
const rootContainer = getContainerFromSubtree(root)
|
||||
if (rootContainer) {
|
||||
@@ -77,6 +139,10 @@ export function getAureliaContainer() {
|
||||
}
|
||||
|
||||
export function summarizeAureliaSubtree(root) {
|
||||
if (!isDiagnosticsDebugEnabled()) {
|
||||
return "(debug-disabled)"
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
return "root=null"
|
||||
}
|
||||
@@ -149,7 +215,11 @@ export function findExportedConstructor(webpackRequire, predicate) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function findInstanceInContainerGraph(root, predicate, maxDepth = 4) {
|
||||
export function findInstanceInContainerGraph(
|
||||
root,
|
||||
predicate,
|
||||
maxDepth = CONTAINER_GRAPH_MAX_DEPTH
|
||||
) {
|
||||
if (!root) {
|
||||
return null
|
||||
}
|
||||
@@ -171,7 +241,9 @@ export function findInstanceInContainerGraph(root, predicate, maxDepth = 4) {
|
||||
if (predicate(value)) {
|
||||
return value
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
logContainerGraphPredicateError(error)
|
||||
}
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
continue
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TRAINER_LAUNCH_REQUEST_EXPORT_KEY } from "./constants.js"
|
||||
import {
|
||||
findExportedConstructor,
|
||||
findInstanceInContainerGraph,
|
||||
formatError,
|
||||
isRecord,
|
||||
} from "./runtime.js"
|
||||
|
||||
@@ -13,6 +14,15 @@ export function hasMissingOptionalServices(state) {
|
||||
)
|
||||
}
|
||||
|
||||
export function hasUnresolvedServices(state) {
|
||||
return (
|
||||
hasMissingOptionalServices(state) ||
|
||||
!state.trainerApiService ||
|
||||
!state.trainerService ||
|
||||
!state.trainerLaunchRequestCtor
|
||||
)
|
||||
}
|
||||
|
||||
const OPTIONAL_SERVICE_SPECS = [
|
||||
{
|
||||
stateKey: "unavailableTitlesService",
|
||||
@@ -87,7 +97,7 @@ export function getInstalledAppsService(state, container, webpackRequire) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Failed to resolve installed apps service from Aurelia container.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -129,7 +139,7 @@ export function getStoreRef(state, container, webpackRequire) {
|
||||
state.log(
|
||||
"warn",
|
||||
"Failed to resolve Store from container.",
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -216,7 +226,7 @@ function getContainerService(state, container, ctor, warningKey, label) {
|
||||
state.log(
|
||||
"warn",
|
||||
`Failed to resolve ${label.toLowerCase()} from Aurelia container.`,
|
||||
error?.stack || String(error)
|
||||
formatError(error)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user