diff --git a/ui/ts/App.tsx b/ui/ts/App.tsx index fe3dc83a..9ac465c5 100644 --- a/ui/ts/App.tsx +++ b/ui/ts/App.tsx @@ -357,6 +357,7 @@ export function App() { const marketRouteContentProps: MarketRouteContentProps = { accountState, + activeUniverseId, activeView: activeZoltarView, hasLoadedZoltarQuestions, loadingZoltarForkAccess, @@ -374,7 +375,7 @@ export function App() { onApproveZoltarForkRep: amount => void approveZoltarForkRep(amount), onCreateMarket: () => void createMarket(), onForkZoltar: () => void forkZoltar(), - onLoadZoltarQuestions: () => void loadZoltarQuestions(), + onLoadZoltarQuestions: loadZoltarQuestions, onMigrateInternalRep: () => void migrateInternalRep(), onMarketFormChange: update => setMarketForm(current => ({ ...current, ...update })), onPrepareRepForMigration: () => void prepareRepForMigration(), diff --git a/ui/ts/components/MarketQuestionsSection.tsx b/ui/ts/components/MarketQuestionsSection.tsx index 3371ae25..71248c2f 100644 --- a/ui/ts/components/MarketQuestionsSection.tsx +++ b/ui/ts/components/MarketQuestionsSection.tsx @@ -10,7 +10,7 @@ type MarketQuestionsSectionProps = { hasLoadedZoltarQuestions: boolean loadingZoltarQuestionCount: boolean loadingZoltarQuestions: boolean - onLoadZoltarQuestions: () => void + onLoadZoltarQuestions: () => Promise onOpenForkTab: () => void onUseQuestionForFork: (questionId: string) => void onUseQuestionForPool: (questionId: string) => void @@ -26,7 +26,13 @@ export function MarketQuestionsSection({ hasForked, hasLoadedZoltarQuestions, lo density='compact' title='Questions' actions={ - } diff --git a/ui/ts/components/MarketSection.tsx b/ui/ts/components/MarketSection.tsx index 0cf58972..158a916d 100644 --- a/ui/ts/components/MarketSection.tsx +++ b/ui/ts/components/MarketSection.tsx @@ -12,6 +12,7 @@ import type { MarketSectionProps } from '../types/components.js' export function MarketSection({ accountState, + activeUniverseId, activeView, hasLoadedZoltarQuestions, loadingZoltarForkAccess, @@ -77,16 +78,17 @@ export function MarketSection({ useEffect(() => { if (view !== 'questions') return + if (loadingZoltarQuestionCount) return + if (zoltarQuestionCount === undefined) return + if (zoltarQuestionCount === 0n) return if (loadingZoltarQuestions) return if (hasLoadedZoltarQuestions) return - if (zoltarQuestionCount === 0n) return - const currentUniverseId = zoltarUniverse?.universeId - if (currentUniverseId !== undefined && lastAutoLoadedQuestionsUniverseId.current === currentUniverseId) return - lastAutoLoadedQuestionsUniverseId.current = currentUniverseId + if (lastAutoLoadedQuestionsUniverseId.current === activeUniverseId) return + lastAutoLoadedQuestionsUniverseId.current = activeUniverseId void Promise.resolve(onLoadZoltarQuestions()).catch(() => { lastAutoLoadedQuestionsUniverseId.current = undefined }) - }, [hasLoadedZoltarQuestions, loadingZoltarQuestions, onLoadZoltarQuestions, view, zoltarQuestionCount, zoltarUniverse?.universeId]) + }, [activeUniverseId, hasLoadedZoltarQuestions, loadingZoltarQuestionCount, loadingZoltarQuestions, onLoadZoltarQuestions, view, zoltarQuestionCount]) return (
diff --git a/ui/ts/hooks/useZoltarUniverse.ts b/ui/ts/hooks/useZoltarUniverse.ts index 0b8290a8..d7dd92f1 100644 --- a/ui/ts/hooks/useZoltarUniverse.ts +++ b/ui/ts/hooks/useZoltarUniverse.ts @@ -115,11 +115,12 @@ export function useZoltarUniverse({ accountAddress, activeUniverseId, autoLoadIn }) } - const loadQuestions = async () => { + const loadQuestions = async (): Promise => { if (!isMounted.current) return const isCountCurrent = nextQuestionCountLoad() const isQuestionsCurrent = nextQuestionsLoad() const readClient = createConnectedReadClient() + let loadError: unknown const countTask = questionCountLoad.run({ isCurrent: isCountCurrent, @@ -128,7 +129,9 @@ export function useZoltarUniverse({ accountAddress, activeUniverseId, autoLoadIn if (!isMounted.current) return zoltarQuestionCount.value = questionCount }, - onError: () => undefined, + onError: error => { + loadError = loadError ?? error + }, }) const questionsTask = questionsLoad.run({ @@ -139,10 +142,15 @@ export function useZoltarUniverse({ accountAddress, activeUniverseId, autoLoadIn zoltarQuestions.value = questions hasLoadedZoltarQuestions.value = true }, - onError: () => undefined, + onError: error => { + loadError = loadError ?? error + }, }) await Promise.allSettled([countTask, questionsTask]) + if (loadError !== undefined) { + throw loadError + } } const createChildUniverse = async (outcomeIndex: bigint) => { diff --git a/ui/ts/tests/marketSection.test.tsx b/ui/ts/tests/marketSection.test.tsx index a89bbd97..188d1058 100644 --- a/ui/ts/tests/marketSection.test.tsx +++ b/ui/ts/tests/marketSection.test.tsx @@ -62,6 +62,7 @@ function createBinaryForkQuestion() { function createMarketSectionProps(overrides: Partial = {}): MarketSectionProps { return { accountState: createAccountState(), + activeUniverseId: 1n, activeView: 'questions', hasLoadedZoltarQuestions: false, loadingZoltarForkAccess: false, @@ -78,7 +79,7 @@ function createMarketSectionProps(overrides: Partial = {}): onCreateChildUniverseForOutcomeIndex: () => undefined, onCreateMarket: () => undefined, onForkZoltar: () => undefined, - onLoadZoltarQuestions: () => undefined, + onLoadZoltarQuestions: async () => undefined, onMarketFormChange: () => undefined, onMigrateInternalRep: () => undefined, onPrepareRepForMigration: () => undefined, @@ -146,9 +147,10 @@ describe('MarketSection', () => { test('auto-loads questions once when opening the questions view without loaded data', async () => { const calls: string[] = [] const initialProps = createMarketSectionProps({ + activeUniverseId: 7n, hasLoadedZoltarQuestions: false, loadingZoltarQuestions: false, - onLoadZoltarQuestions: () => { + onLoadZoltarQuestions: async () => { calls.push('load') }, zoltarQuestionCount: 3n, @@ -163,7 +165,7 @@ describe('MarketSection', () => { render( h(MarketSection, { ...initialProps, - onLoadZoltarQuestions: () => { + onLoadZoltarQuestions: async () => { calls.push('rerender') }, }), @@ -174,6 +176,42 @@ describe('MarketSection', () => { expect(calls).toEqual(['load']) }) + test('does not auto-load questions while the question count is unresolved', async () => { + const calls: string[] = [] + const renderedComponent = await renderIntoDocument( + h( + MarketSection, + createMarketSectionProps({ + onLoadZoltarQuestions: async () => { + calls.push('load') + }, + zoltarQuestionCount: undefined, + }), + ), + ) + cleanupRenderedComponent = renderedComponent.cleanup + + expect(calls).toEqual([]) + }) + + test('does not auto-load questions when the resolved count is zero', async () => { + const calls: string[] = [] + const renderedComponent = await renderIntoDocument( + h( + MarketSection, + createMarketSectionProps({ + onLoadZoltarQuestions: async () => { + calls.push('load') + }, + zoltarQuestionCount: 0n, + }), + ), + ) + cleanupRenderedComponent = renderedComponent.cleanup + + expect(calls).toEqual([]) + }) + test('does not auto-load questions when they are already loaded', async () => { const calls: string[] = [] const renderedComponent = await renderIntoDocument( @@ -181,7 +219,7 @@ describe('MarketSection', () => { MarketSection, createMarketSectionProps({ hasLoadedZoltarQuestions: true, - onLoadZoltarQuestions: () => { + onLoadZoltarQuestions: async () => { calls.push('load') }, zoltarQuestionCount: 3n, @@ -193,20 +231,90 @@ describe('MarketSection', () => { expect(calls).toEqual([]) }) + test('auto-loads questions once after the count resolves above zero even when the universe is unresolved', async () => { + const calls: string[] = [] + const initialProps = createMarketSectionProps({ + activeUniverseId: 12n, + loadingZoltarQuestionCount: true, + onLoadZoltarQuestions: async () => { + calls.push('load') + }, + zoltarQuestionCount: undefined, + zoltarUniverse: undefined, + zoltarUniverseState: 'unknown', + }) + + const renderedComponent = await renderIntoDocument(h(MarketSection, initialProps)) + cleanupRenderedComponent = renderedComponent.cleanup + expect(calls).toEqual([]) + + await act(() => { + render( + h( + MarketSection, + createMarketSectionProps({ + ...initialProps, + loadingZoltarQuestionCount: false, + zoltarQuestionCount: 3n, + }), + ), + renderedComponent.container, + ) + }) + + expect(calls).toEqual(['load']) + }) + + test('does not auto-load questions again on rerender when the active universe id is unchanged', async () => { + const calls: string[] = [] + const initialProps = createMarketSectionProps({ + activeUniverseId: 13n, + onLoadZoltarQuestions: async () => { + calls.push('load') + }, + zoltarQuestionCount: 3n, + zoltarUniverse: undefined, + zoltarUniverseState: 'unknown', + }) + + const renderedComponent = await renderIntoDocument(h(MarketSection, initialProps)) + cleanupRenderedComponent = renderedComponent.cleanup + expect(calls).toEqual(['load']) + + await act(() => { + render( + h( + MarketSection, + createMarketSectionProps({ + ...initialProps, + onLoadZoltarQuestions: async () => { + calls.push('rerender') + }, + }), + ), + renderedComponent.container, + ) + }) + + expect(calls).toEqual(['load']) + }) + test('retries question auto-load when the previous automatic load fails', async () => { const calls: string[] = [] const renderedComponent = await renderIntoDocument( h( MarketSection, createMarketSectionProps({ + activeUniverseId: 9n, hasLoadedZoltarQuestions: false, loadingZoltarQuestions: false, - onLoadZoltarQuestions: () => { + onLoadZoltarQuestions: async () => { calls.push('load') return Promise.reject(new Error('temporary failure')) }, zoltarQuestionCount: 3n, - zoltarUniverse: createZoltarUniverse({ universeId: 9n }), + zoltarUniverse: undefined, + zoltarUniverseState: 'unknown', }), ), ) @@ -219,13 +327,15 @@ describe('MarketSection', () => { h( MarketSection, createMarketSectionProps({ + activeUniverseId: 9n, hasLoadedZoltarQuestions: false, loadingZoltarQuestions: false, - onLoadZoltarQuestions: () => { + onLoadZoltarQuestions: async () => { calls.push('retry') }, zoltarQuestionCount: 3n, - zoltarUniverse: createZoltarUniverse({ universeId: 9n }), + zoltarUniverse: undefined, + zoltarUniverseState: 'unknown', }), ), renderedComponent.container, diff --git a/ui/ts/types/components.ts b/ui/ts/types/components.ts index 982714af..fb8eaa8c 100644 --- a/ui/ts/types/components.ts +++ b/ui/ts/types/components.ts @@ -270,6 +270,7 @@ export type DeploymentRouteContentProps = { export type MarketRouteContentProps = { accountState: AccountState + activeUniverseId: bigint activeView: ZoltarView onApproveZoltarForkRep: (amount?: bigint) => void onCreateChildUniverseForOutcomeIndex: (outcomeIndex: bigint) => void @@ -289,7 +290,7 @@ export type MarketRouteContentProps = { zoltarForkActiveAction: 'approve' | 'fork' | undefined loadingZoltarUniverse: boolean zoltarUniverseState: LoadableValueState - onLoadZoltarQuestions: () => void + onLoadZoltarQuestions: () => Promise onMarketFormChange: (update: Partial) => void onUseQuestionForFork: (questionId: string) => void onUseQuestionForPool: (questionId: string) => void