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:
kitbyte
2026-08-29 16:56:51 +03:00
parent 5d1aa69a97
commit f2e88e9247
8 changed files with 224 additions and 184 deletions
+52 -23
View File
@@ -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
}