From f2e88e92477f9196dac5c05b5088f26bf17f0404 Mon Sep 17 00:00:00 2001 From: kitbyte Date: Sat, 29 Aug 2026 16:56:51 +0300 Subject: [PATCH] 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. --- .../default/installed-apps-sync/constants.js | 3 + .../installed-apps-sync/game-status.js | 35 ++---- .../default/installed-apps-sync/index.js | 75 +++++++++---- .../installed-apps-sync/installed-data.js | 103 ++++++++---------- .../default/installed-apps-sync/logger.js | 9 +- .../installed-apps-sync/remote-commands.js | 91 ++++------------ .../default/installed-apps-sync/runtime.js | 76 ++++++++++++- .../default/installed-apps-sync/services.js | 16 ++- 8 files changed, 224 insertions(+), 184 deletions(-) diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/constants.js b/web-panel/bridge/scripts/default/installed-apps-sync/constants.js index 31c1aec..a816494 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/constants.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/constants.js @@ -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" diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js b/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js index 8b67d25..5e9dbd4 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js @@ -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) { diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/index.js b/web-panel/bridge/scripts/default/installed-apps-sync/index.js index c80380f..6fcd19f 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/index.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/index.js @@ -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 } diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js b/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js index 9ed4cba..7fd59d9 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js @@ -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) { diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/logger.js b/web-panel/bridge/scripts/default/installed-apps-sync/logger.js index 862509d..49cad57 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/logger.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/logger.js @@ -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 {} } diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js b/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js index f89f542..be7fa7a 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js @@ -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" + ) } diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/runtime.js b/web-panel/bridge/scripts/default/installed-apps-sync/runtime.js index 660cf7b..89040fb 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/runtime.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/runtime.js @@ -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 diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/services.js b/web-panel/bridge/scripts/default/installed-apps-sync/services.js index 453edb1..e67766a 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/services.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/services.js @@ -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 }