Files
Wand-Enhancer-k1tbyte/web-panel/bridge/scripts/default/installed-apps-sync/index.js
T

437 lines
12 KiB
JavaScript
Vendored

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,
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 {
formatError,
getAppRoot,
getAureliaContainer,
getRequire,
getWebpackRequire,
hasAppRoot,
invokeIpc,
isRecord,
summarizeAureliaSubtree,
} from './runtime.js';
import {
getInstalledAppsService,
getStoreRef,
hasMissingOptionalServices,
hasUnresolvedServices,
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,
optionalServicesAttempts: 0,
bridgeBound: false,
bridgeBinding: 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}`,
);
}
async function bindBridge(state) {
if (state.bridgeBound || state.bridgeBinding || !state.ipcRenderer) {
return;
}
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.');
}
// 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 {
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; will retry on the next sync.', formatError(error));
} finally {
state.bridgeBinding = false;
}
}
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;
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 sent;
}
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(() => {
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(
'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.');
}
}
void 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(() => {
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);
state.log('info', 'Installed apps poll timer started.', `${SYNC_INTERVAL_MS}ms`);
}
function logMissingContainer(state) {
if (state.bootstrapAttempts % CONTAINER_LOG_THROTTLE_ATTEMPTS !== 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',
);
}