diff --git a/src/express/results.test.ts b/src/express/results.test.ts index 9a63e79..52a0e81 100644 --- a/src/express/results.test.ts +++ b/src/express/results.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { correctIndex, questionsOf, toMarkdown, withPlanItem, type Material } from './results' +import { correctIndex, keepSettled, questionsOf, toMarkdown, withPlanItem, type Material } from './results' import { sentencesOf, scriptLang } from '../components/AudioPlayer' describe('correctIndex', () => { @@ -65,3 +65,20 @@ describe('withPlanItem', () => { expect(withPlanItem('preparing', undefined)).toBe('preparing') }) }) + +describe('keepSettled', () => { + const ok = (v: number[]): PromiseSettledResult => ({ status: 'fulfilled', value: v }) + const bad = (message: string): PromiseSettledResult => ({ status: 'rejected', reason: new Error(message) }) + + it('leaves out a schedule that failed', () => { + expect(keepSettled([ok([1, 2]), bad('404'), ok([3])])).toEqual([1, 2, 3]) + }) + + it('fails when every schedule failed', () => { + expect(() => keepSettled([bad('500'), bad('502')])).toThrow('500') + }) + + it('is empty without schedules', () => { + expect(keepSettled([])).toEqual([]) + }) +}) diff --git a/src/express/results.ts b/src/express/results.ts index 22c38ac..c9540d9 100644 --- a/src/express/results.ts +++ b/src/express/results.ts @@ -172,14 +172,24 @@ async function sourcesOf({ agent, rule }: Schedule): Promise { })) } +// One schedule the engine cannot answer for (e.g. an archived agent) leaves its results out +// instead of failing them all; only when every schedule fails is that an error. +export function keepSettled(results: PromiseSettledResult[]): T[] { + const kept = results.flatMap(r => (r.status === 'fulfilled' ? r.value : [])) + const failed = results.filter(r => r.status === 'rejected') + if (failed.length) console.warn(`${failed.length} of ${results.length} schedules could not be loaded`, failed.map(r => r.reason)) + if (results.length && failed.length === results.length) throw failed[0].reason + return kept +} + export async function listResults(): Promise<{ materials: Material[]; sources: FoundSource[] }> { const schedules = await expressAgents({ archived: true }) const [materials, sources] = await Promise.all([ - Promise.all(schedules.filter(s => s.rule.type === 'content').map(materialsOf)), - Promise.all(schedules.filter(s => s.rule.type === 'research').map(sourcesOf)), + Promise.allSettled(schedules.filter(s => s.rule.type === 'content').map(materialsOf)), + Promise.allSettled(schedules.filter(s => s.rule.type === 'research').map(sourcesOf)), ]) const newest = (a: T, b: T) => b.createdAt.localeCompare(a.createdAt) - return { materials: materials.flat().sort(newest), sources: sources.flat().sort(newest) } + return { materials: keepSettled(materials).sort(newest), sources: keepSettled(sources).sort(newest) } } const draftContent = (m: Material, id: number) => diff --git a/src/pages/Production.tsx b/src/pages/Production.tsx index 1603016..5812ca2 100644 --- a/src/pages/Production.tsx +++ b/src/pages/Production.tsx @@ -92,7 +92,8 @@ export function ProductionPage() { {graph ? - :
{docs.isError || results.isError ? t('brain.engineDown') : t('prod.loadingGraph')}
} + // The engine's own message: an unreachable engine says so, any other failure says what it was. + :
{(docs.error ?? results.error)?.message ?? t('prod.loadingGraph')}
}