Files
Wand-Enhancer/web-panel/bridge/scripts/default/installed-apps-sync/index.js
T
kitbyte 13759b1db6 feat: ship 1.0.8.0 release automation and runtime overhaul
- reduce ASAR IO overhead with streamed archive reads, buffered copies, faster relative-path handling and placeholder integrity records
- fix in-place app.asar.unpacked packing/extraction self-copy cases that caused locked-file failures
- tighten JS patch discovery with candidate bundle filters and search hints
- require prebuilt remote-panel dist artifacts and clean up embedded bridge/script packaging
- add unified build entrypoints for PowerShell, cmd and bash and move native CMake output under .tmp
- add release metadata validation, changelog section extraction, pre-commit hook and GitHub Actions validation/release pipelines
- make CHANGELOG the source of truth for release notes and document the tag-driven release flow
- add updater release notes UI with latest/full changelog loading and localize the new update strings
- modularize bridge renderer scripts, add installed apps and game status sync, and support remote launch/stop commands
- centralize bridge protocol, IPC and WebSocket constants and improve LAN IP selection for QR pairing
- refactor remote panel controls/state enums, persist accent color, polish library/session UI and refresh assets
2026-05-06 13:07:09 +03:00

422 lines
10 KiB
JavaScript

import {
BIND_CHANNEL,
BOOTSTRAP_LOG_THROTTLE_ATTEMPTS,
COMMAND_REQUEST_CHANNEL,
FOLLOW_UP_SYNC_DELAY_MS,
GLOBAL_FLAG,
MAX_BOOTSTRAP_ATTEMPTS,
OPTIONAL_SERVICES_RETRY_INTERVAL_MS,
RETRY_DELAY_MS,
SYNC_CHANNEL,
SYNC_INTERVAL_MS,
} from "./constants.js"
import {
createIdleGameSession,
createIdleTrainerStatus,
installGameStatusSubscriptions,
} from "./game-status.js"
import {
buildSnapshot,
makeInstalledAppsSignature,
refreshUnavailableTitles,
resolveInstalledData,
} from "./installed-data.js"
import { createLogger } from "./logger.js"
import { handleRemoteCommandRequest } from "./remote-commands.js"
import {
getAppRoot,
getAureliaContainer,
getRequire,
getWebpackRequire,
hasAppRoot,
isRecord,
summarizeAureliaSubtree,
} from "./runtime.js"
import {
getInstalledAppsService,
getStoreRef,
hasMissingOptionalServices,
resolveOptionalServices,
} from "./services.js"
export function installInstalledAppsSync(WandEnhancer) {
if (globalThis[GLOBAL_FLAG]) {
return
}
globalThis[GLOBAL_FLAG] = true
const state = createState(WandEnhancer)
state.resolveRemoteCommandServices = () => resolveRemoteCommandServices(state)
state.queueSync = (force = false) => queueSync(state, force)
state.queueFollowUpSync = () => queueFollowUpSync(state)
state.log(
"info",
"Script loaded.",
`logFile=${globalThis.__wandInstalledAppsSyncLogFile || "console-only"}`
)
retryBootstrap(state)
}
function createState(WandEnhancer) {
return {
WandEnhancer,
log: createLogger(WandEnhancer),
lastSignature: null,
lastGameStatusSignature: null,
refreshTimer: null,
followUpSyncTimer: null,
pollTimer: null,
optionalServicesTimer: null,
bootstrapAttempts: 0,
bridgeBound: false,
refreshPatched: false,
installedAppsService: null,
gameLifecycleService: null,
trainerVisibilityService: null,
unavailableTitlesService: null,
storeRef: null,
ipcRenderer: null,
lastBootstrapReason: null,
gameLifecycleSubscriptionsInstalled: false,
trainerVisibilitySubscriptionInstalled: false,
trainerEndedSubscriptionInstalled: false,
unavailableTitlesFetchKey: null,
unavailableTitlesFetchPromise: null,
unavailableTitlesById: {},
trainerApiService: null,
trainerService: null,
trainerLaunchRequestCtor: null,
commandListenerInstalled: false,
missingOptionalServiceWarnings: new Set(),
currentGameSession: createIdleGameSession(),
currentRunningTrainer: createIdleTrainerStatus(),
resolveRemoteCommandServices: null,
queueSync: null,
queueFollowUpSync: null,
}
}
function setBootstrapReason(state, reason) {
if (
reason === state.lastBootstrapReason &&
state.bootstrapAttempts % BOOTSTRAP_LOG_THROTTLE_ATTEMPTS !== 0
) {
return
}
state.lastBootstrapReason = reason
state.log(
"info",
`Bootstrap waiting: ${reason}.`,
`attempt=${state.bootstrapAttempts + 1}/${MAX_BOOTSTRAP_ATTEMPTS}`
)
}
function bindBridge(state) {
if (state.bridgeBound || !state.ipcRenderer) {
return
}
state.bridgeBound = true
if (!state.commandListenerInstalled) {
state.ipcRenderer.on(COMMAND_REQUEST_CHANNEL, (event, request) =>
handleRemoteCommandRequest(state, event, request)
)
state.commandListenerInstalled = true
state.log("info", "Bridge remote command handler installed.")
}
try {
void state.ipcRenderer.invoke(BIND_CHANNEL)
state.log("info", "Bridge set-value handler bind requested.")
} catch (error) {
state.log("warn", "Bridge bind failed.", error?.stack || String(error))
}
}
async function syncInstalledApps(state, force = false) {
if (!state.ipcRenderer || (!state.installedAppsService && !state.storeRef)) {
return false
}
const data = resolveInstalledData(state)
if (!data) {
if (state.installedAppsService) {
state.log(
"warn",
"Service resolved but installedApps is empty/undefined. Store fallback also unavailable."
)
}
return false
}
if (Object.keys(data.rawInstalledApps).length > 0) {
await refreshUnavailableTitles(state, data.rawInstalledApps, force)
}
const snapshot = buildSnapshot(state)
if (!snapshot) {
state.log("warn", "Snapshot build returned nothing.")
return false
}
const signature = makeInstalledAppsSignature(snapshot)
if (!force && signature === state.lastSignature) {
return false
}
state.lastSignature = signature
try {
await state.ipcRenderer.invoke(SYNC_CHANNEL, snapshot)
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
}
}
function queueSync(state, force = false) {
if (state.refreshTimer) {
clearTimeout(state.refreshTimer)
}
state.refreshTimer = setTimeout(() => {
state.refreshTimer = null
void syncInstalledApps(state, force)
}, 0)
}
function queueFollowUpSync(state) {
if (state.followUpSyncTimer) {
clearTimeout(state.followUpSyncTimer)
}
state.followUpSyncTimer = setTimeout(() => {
state.followUpSyncTimer = null
void syncInstalledApps(state, true)
}, FOLLOW_UP_SYNC_DELAY_MS)
}
function patchRefreshApps(state) {
if (
!state.installedAppsService ||
state.refreshPatched ||
typeof state.installedAppsService.refreshApps !== "function"
) {
return
}
const originalRefreshApps = state.installedAppsService.refreshApps.bind(
state.installedAppsService
)
state.refreshPatched = true
state.log("info", "refreshApps hook installed.")
state.installedAppsService.refreshApps = async (...args) => {
const result = await originalRefreshApps(...args)
state.log("info", "refreshApps completed; queueing installed apps sync.")
queueSync(state, true)
queueFollowUpSync(state)
return result
}
}
function resolveRemoteCommandServices(state) {
const container = getAureliaContainer()
const webpackRequire = getWebpackRequire()
if (!container || !webpackRequire) {
return false
}
resolveRuntimeServices(state, container, webpackRequire)
return true
}
function resolveRuntimeServices(state, container, webpackRequire) {
resolveOptionalServices(state, container, webpackRequire)
installGameStatusSubscriptions(state)
if (!hasMissingOptionalServices(state)) {
stopOptionalServicesRetry(state)
}
}
function stopOptionalServicesRetry(state) {
if (!state.optionalServicesTimer) {
return
}
clearInterval(state.optionalServicesTimer)
state.optionalServicesTimer = null
}
function startOptionalServicesRetry(state) {
if (state.optionalServicesTimer || !hasMissingOptionalServices(state)) {
return
}
state.optionalServicesTimer = setInterval(() => {
const container = getAureliaContainer()
const webpackRequire = getWebpackRequire()
if (container && webpackRequire) {
resolveRuntimeServices(state, container, webpackRequire)
}
}, OPTIONAL_SERVICES_RETRY_INTERVAL_MS)
state.log(
"info",
"Optional service retry timer started.",
`${OPTIONAL_SERVICES_RETRY_INTERVAL_MS}ms`
)
}
function bootstrap(state) {
if (!hasAppRoot()) {
setBootstrapReason(state, "app root not ready")
return false
}
if (!state.ipcRenderer && !resolveIpcRenderer(state)) {
return false
}
const webpackRequire = getWebpackRequire()
if (!webpackRequire) {
setBootstrapReason(state, "webpack runtime not ready")
return false
}
const container = getAureliaContainer()
if (!container) {
logMissingContainer(state)
setBootstrapReason(state, "Aurelia container not ready")
return false
}
state.log("info", "Aurelia container resolved.")
if (!state.storeRef) {
state.storeRef = getStoreRef(state, container, webpackRequire)
if (!state.storeRef) {
state.log(
"warn",
"Store reference unavailable; unsupported installed titles will be missing."
)
}
}
state.installedAppsService = getInstalledAppsService(
state,
container,
webpackRequire
)
if (!state.installedAppsService) {
setBootstrapReason(state, "installed apps service not ready")
return false
}
resolveRuntimeServices(state, container, webpackRequire)
startOptionalServicesRetry(state)
if (!isRecord(state.installedAppsService.installedApps)) {
state.log(
"info",
"Service instance has no installedApps data yet; reading from store until refreshApps populates it."
)
if (!state.storeRef) {
state.log("warn", "Store fallback also unavailable; will retry on poll.")
}
}
bindBridge(state)
patchRefreshApps(state)
queueSync(state, true)
queueFollowUpSync(state)
startPollTimer(state)
state.log("info", "Installed apps sync ready.")
return true
}
function resolveIpcRenderer(state) {
const electron = getRequire()?.("electron")
if (!electron?.ipcRenderer) {
setBootstrapReason(state, "electron ipcRenderer not ready")
return false
}
state.ipcRenderer = electron.ipcRenderer
state.log("info", "Electron ipcRenderer resolved.")
return true
}
function startPollTimer(state) {
if (state.pollTimer) {
return
}
state.pollTimer = setInterval(() => {
const container = getAureliaContainer()
const webpackRequire = getWebpackRequire()
if (container && webpackRequire) {
resolveRuntimeServices(state, container, webpackRequire)
}
void syncInstalledApps(state)
}, SYNC_INTERVAL_MS)
state.log(
"info",
"Installed apps poll timer started.",
`${SYNC_INTERVAL_MS}ms`
)
}
function logMissingContainer(state) {
if (state.bootstrapAttempts % 10 !== 0) {
return
}
const root = getAppRoot()
const aureliaKeys = root
? Object.getOwnPropertyNames(root)
.filter((key) => key.startsWith("__") || key === "au")
.join(", ")
: "root=null"
state.log(
"warn",
"Aurelia container not found.",
`rootProps=${aureliaKeys || "(none)"}, subtree=${summarizeAureliaSubtree(root)}`
)
}
function retryBootstrap(state) {
if (bootstrap(state)) {
return
}
state.bootstrapAttempts += 1
if (state.bootstrapAttempts < MAX_BOOTSTRAP_ATTEMPTS) {
setTimeout(() => retryBootstrap(state), RETRY_DELAY_MS)
return
}
state.log(
"error",
"Installed apps sync bootstrap exhausted.",
state.lastBootstrapReason || "unknown reason"
)
}