import { MERGED_CONSOLE_COUNT_EVAL, inspectErrors } from './commands/inspect/core' import { formatLogTimestamp } from './commands/inspect/shared' import type { WsBridge } from './ws-bridge' import type { SootSimRequestEntry } from '@rnx/globals' interface ConsoleSummaryOptions { includeTail?: boolean errorsCommand?: string warningsCommand?: string } interface RequestSummaryOptions { includeTail?: boolean requestsCommand?: string } interface FailureDiagnosticsOptions { errorsCommand?: string warningsCommand?: string requestsCommand?: string } function formatRequestMessage(entry: SootSimRequestEntry) { const target = entry.displayUrl || entry.url if (entry.status != null) { return `${entry.method} ${target} -> ${entry.status}${entry.statusText ? ` ${entry.statusText}` : ''}` } if (entry.error) { return `${entry.method} ${target} -> ${entry.error}` } return `${entry.method} ${target}` } async function callTestBridge( bridge: WsBridge, method: string, ...args: unknown[] ) { return bridge.send({ type: 'call', path: `__sootsimTest.${method}`, args, }) as Promise } export async function printBridgeConsoleSummary( bridge: WsBridge, opts: ConsoleSummaryOptions = {}, ) { const counts = (await bridge.send({ type: 'evaluate', code: MERGED_CONSOLE_COUNT_EVAL, })) as { errors?: number; warnings?: number; total?: number } | null if (!counts || typeof counts !== 'object') return const errors = Math.max(0, Number(counts.errors) || 0) const warnings = Math.max(0, Number(counts.warnings) || 0) if (errors === 0 && warnings === 0) return const parts = [] if (errors > 0) parts.push(`${errors} console error${errors === 1 ? '' : 's'}`) if (warnings > 0) { parts.push(`${warnings} console warning${warnings === 1 ? '' : 's'}`) } console.log(`\n console: ${parts.join(', ')}`) if (opts.errorsCommand) console.log(` inspect: ${opts.errorsCommand}`) if (warnings > 0 && opts.warningsCommand) { console.log(` inspect: ${opts.warningsCommand}`) } if (!opts.includeTail || errors === 0) return // merged read (ws-bridge buffer + observability) so forwarded render-worker // errors are shown in the tail, not just counted. const recentErrors = await inspectErrors(bridge, 5) if (!Array.isArray(recentErrors) || recentErrors.length === 0) return console.log('\n recent console errors:\n') for (const entry of recentErrors as Array<{ timestamp: number args?: unknown[] }>) { const time = formatLogTimestamp(entry.timestamp) const msg = Array.isArray(entry.args) ? entry.args .map((value) => typeof value === 'object' ? JSON.stringify(value) : String(value), ) .join(' ') : String(entry) console.log(` [${time}] ${msg}`) } } export async function printBridgeRequestSummary( bridge: WsBridge, opts: RequestSummaryOptions = {}, ) { const counts = await callTestBridge<{ failed?: number; total?: number }>( bridge, 'getRequestCounts', ) if (!counts || typeof counts !== 'object') return const failed = Math.max(0, Number(counts.failed) || 0) if (failed === 0) return console.log(`\n network: ${failed} failed request${failed === 1 ? '' : 's'}`) if (opts.requestsCommand) console.log(` inspect: ${opts.requestsCommand}`) if (!opts.includeTail) return const recentFailed = await callTestBridge( bridge, 'getFailedRequests', 5, ) if (!Array.isArray(recentFailed) || recentFailed.length === 0) return console.log('\n recent failed requests:\n') for (const entry of recentFailed) { const time = formatLogTimestamp(entry.timestamp) console.log(` [${time}] ${formatRequestMessage(entry)}`) if (entry.responseBody) { console.log(` ${entry.responseBody}`) } else if (entry.error) { console.log(` ${entry.error}`) } } } interface BridgeStateSummary { url?: string simId?: string | null nodeCount?: number mode?: string shell?: { state?: string | null activeApp?: string | null showSwitcher?: boolean switcherPhase?: string | null } | null } export async function printBridgeStateSummary(bridge: WsBridge) { const state = (await bridge.send({ type: 'evaluate', code: `(async () => { const test = window.__sootsimTest const mainShell = window.SootSim?.bridges?.mainShell let shell = null try { shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null } catch {} let nodeCount = 0 try { nodeCount = typeof test?.getNodeCount === 'function' ? await test.getNodeCount() : 0 } catch {} return { url: window.location.href, simId: window.__sootsimSimId || null, nodeCount, mode: window.__sootsimRenderHost ? 'render-worker' : 'main-thread', shell: shell ? { state: shell.state || null, activeApp: shell.activeApp || null, showSwitcher: !!shell.showSwitcher, switcherPhase: shell.switcherPhase || null, } : null, } })()`, })) as BridgeStateSummary | null if (!state || typeof state !== 'object') return console.log('\n state:') if (state.simId) console.log(` sim: ${state.simId}`) if (state.url) console.log(` url: ${state.url}`) if (state.mode) console.log(` mode: ${state.mode}`) const nodeCount = Number(state.nodeCount) || 0 console.log(` nodes: ${nodeCount}${nodeCount > 10 ? ' (ready)' : ' (not ready)'}`) const shell = state.shell if (shell && typeof shell === 'object') { const shellParts = [ shell.state ? `state=${shell.state}` : null, shell.activeApp ? `app=${shell.activeApp}` : null, shell.showSwitcher ? 'switcher=open' : null, shell.switcherPhase ? `phase=${shell.switcherPhase}` : null, ].filter(Boolean) if (shellParts.length > 0) { console.log(` shell: ${shellParts.join(' ')}`) } } } export async function printBridgeFailureDiagnostics( bridge: WsBridge, opts: FailureDiagnosticsOptions = {}, ) { try { await printBridgeStateSummary(bridge) } catch { // ignore state snapshot failures while already handling a failure } try { await printBridgeConsoleSummary(bridge, { includeTail: true, errorsCommand: opts.errorsCommand, warningsCommand: opts.warningsCommand, }) } catch { // ignore console snapshot failures while already handling a failure } try { await printBridgeRequestSummary(bridge, { includeTail: true, requestsCommand: opts.requestsCommand, }) } catch { // ignore request snapshot failures while already handling a failure } }