mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
fix(web): overlap homepage banners with catalog prefetch
Banners and collection prefetch no longer run back-to-back 2.5s waits. The home variant starts both under one server budget and reuses the dehydrated state so a slow banner cannot add a second document stall.
This commit is contained in:
parent
aa40a404d9
commit
ebccd86338
@ -1,5 +1,6 @@
|
||||
import type { PluginBanner } from '@dify/contracts/marketplace'
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@ -23,6 +24,7 @@ vi.mock('../home/banners', async (importOriginal) => {
|
||||
|
||||
vi.mock('../hydration-server', () => ({
|
||||
HydrateQueryClient: ({ children }: { children: ReactNode }) => children,
|
||||
prefetchMarketplaceDehydratedState: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('../view', () => ({
|
||||
@ -56,10 +58,38 @@ describe('Marketplace server entry', () => {
|
||||
const { default: Marketplace } = await import('../index')
|
||||
const element = await Marketplace({ variant: 'home' })
|
||||
|
||||
render(element)
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{element}
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Server banners: 1')).toBeInTheDocument()
|
||||
expect(mockGetLocaleOnServer).toHaveBeenCalledOnce()
|
||||
expect(mockFetchPluginBanners).toHaveBeenCalledWith('en-US')
|
||||
})
|
||||
|
||||
it('starts catalog prefetch without waiting for banners to finish', async () => {
|
||||
const { prefetchMarketplaceDehydratedState } = await import('../hydration-server')
|
||||
let resolveBanners: (banners: PluginBanner[]) => void = () => {}
|
||||
mockGetLocaleOnServer.mockResolvedValue('en-US')
|
||||
mockFetchPluginBanners.mockImplementation(
|
||||
() =>
|
||||
new Promise<PluginBanner[]>((resolve) => {
|
||||
resolveBanners = resolve
|
||||
}),
|
||||
)
|
||||
vi.mocked(prefetchMarketplaceDehydratedState).mockResolvedValue(undefined)
|
||||
|
||||
const { default: Marketplace } = await import('../index')
|
||||
const renderPromise = Marketplace({ variant: 'home', searchParams: Promise.resolve({}) })
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(prefetchMarketplaceDehydratedState).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockFetchPluginBanners).toHaveBeenCalledWith('en-US')
|
||||
|
||||
resolveBanners([])
|
||||
await renderPromise
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { SearchParams } from 'nuqs/server'
|
||||
import type { MarketplaceSearchParams } from './search-params'
|
||||
import type { DehydratedState } from '@tanstack/react-query'
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
||||
import { createLoader } from 'nuqs/server'
|
||||
import { getQueryClient } from '@/app/get-query-client'
|
||||
@ -16,7 +17,7 @@ import { getCollectionsParams, getMarketplaceCollectionsAndPlugins } from './uti
|
||||
|
||||
// The server side logic should move to marketplace's codebase so that we can get rid of Next.js
|
||||
|
||||
async function getDehydratedState(searchParams?: Promise<SearchParams>) {
|
||||
export async function prefetchMarketplaceDehydratedState(searchParams?: Promise<SearchParams>) {
|
||||
if (!searchParams) {
|
||||
return
|
||||
}
|
||||
@ -49,11 +50,16 @@ async function getDehydratedState(searchParams?: Promise<SearchParams>) {
|
||||
|
||||
export async function HydrateQueryClient({
|
||||
searchParams,
|
||||
prefetchedState,
|
||||
children,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams> | undefined
|
||||
prefetchedState?: DehydratedState
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const dehydratedState = await getDehydratedState(searchParams)
|
||||
const dehydratedState =
|
||||
prefetchedState === undefined
|
||||
? await prefetchMarketplaceDehydratedState(searchParams)
|
||||
: prefetchedState
|
||||
return <HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type { SearchParams } from 'nuqs'
|
||||
import type { MarketplaceViewProps } from './view'
|
||||
import { getLocaleOnServer } from '@/i18n-config/server'
|
||||
import { fetchPluginBanners } from './home/banners'
|
||||
import { HydrateQueryClient } from './hydration-server'
|
||||
import { HydrateQueryClient, prefetchMarketplaceDehydratedState } from './hydration-server'
|
||||
import { withinServerBudget } from './server-budget'
|
||||
import { MarketplaceView } from './view'
|
||||
|
||||
@ -25,18 +25,33 @@ const Marketplace = async ({
|
||||
|
||||
if (variant === 'home') {
|
||||
const locale = language ?? (await getLocaleOnServer())
|
||||
const prefetch = prefetchMarketplaceDehydratedState(searchParams)
|
||||
|
||||
// Banners are decoration on a page whose point is the catalog, so the same
|
||||
// budget that keeps the prefetch from holding the document applies here.
|
||||
// A late resolution just misses this render; nothing waits on it.
|
||||
// Banners are decoration on a page whose point is the catalog. Overlap
|
||||
// them with the catalog prefetch so the document waits at most one budget.
|
||||
// A late banner resolution just misses this render; nothing waits on it.
|
||||
await withinServerBudget(
|
||||
fetchPluginBanners(locale)
|
||||
.then((banners) => {
|
||||
trendingBanners = banners
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep the homepage available if Marketplace banner delivery is down.
|
||||
}),
|
||||
Promise.all([
|
||||
fetchPluginBanners(locale)
|
||||
.then((banners) => {
|
||||
trendingBanners = banners
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep the homepage available if Marketplace banner delivery is down.
|
||||
}),
|
||||
prefetch,
|
||||
]),
|
||||
)
|
||||
|
||||
return (
|
||||
<HydrateQueryClient searchParams={undefined} prefetchedState={await prefetch}>
|
||||
<MarketplaceView
|
||||
{...viewProps}
|
||||
banners={trendingBanners}
|
||||
language={language}
|
||||
variant={variant}
|
||||
/>
|
||||
</HydrateQueryClient>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -15,11 +15,8 @@
|
||||
* blocking. Server rendering degrades exactly when it is too slow to be worth
|
||||
* waiting for.
|
||||
*
|
||||
* Known limitation: the catalog spends this budget twice in sequence — banners
|
||||
* in `index.tsx`, then the prefetch in `hydration-server.tsx` — so the worst
|
||||
* case is 2x. Overlapping them means handing the started prefetch promise down
|
||||
* instead of letting `HydrateQueryClient` own it, which is a wider change than
|
||||
* bounding the waits.
|
||||
* Homepage (`variant="home"`) overlaps banners and catalog prefetch under one
|
||||
* budget so a slow banner cannot add a second 2.5s onto the catalog wait.
|
||||
*/
|
||||
export const SERVER_PREFETCH_BUDGET_MS = 2_500
|
||||
|
||||
|
||||
@ -3,7 +3,10 @@ import { standaloneMarketplaceClient } from '../client'
|
||||
import { standaloneMarketplaceServer } from '../server'
|
||||
|
||||
vi.mock('../../index', () => ({ default: () => null }))
|
||||
vi.mock('../../hydration-server', () => ({ HydrateQueryClient: () => null }))
|
||||
vi.mock('../../hydration-server', () => ({
|
||||
HydrateQueryClient: () => null,
|
||||
prefetchMarketplaceDehydratedState: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('standalone Marketplace host entry', () => {
|
||||
it('exports the client search surface', () => {
|
||||
@ -13,6 +16,9 @@ describe('standalone Marketplace host entry', () => {
|
||||
|
||||
it('exports the server prefetch helpers and creator model', () => {
|
||||
expect(standaloneMarketplaceServer.withinServerBudget).toEqual(expect.any(Function))
|
||||
expect(standaloneMarketplaceServer.prefetchMarketplaceDehydratedState).toEqual(
|
||||
expect.any(Function),
|
||||
)
|
||||
expect(standaloneMarketplaceServer.SERVER_PREFETCH_BUDGET_MS).toBeGreaterThan(0)
|
||||
expect(standaloneMarketplaceServer.parseCreatorSortField('popularity')).toBe('popularity')
|
||||
})
|
||||
|
||||
@ -13,13 +13,14 @@ import {
|
||||
sortCreatorCreations,
|
||||
toPublisherSortQuery,
|
||||
} from '../creator-profile/model'
|
||||
import { HydrateQueryClient } from '../hydration-server'
|
||||
import { HydrateQueryClient, prefetchMarketplaceDehydratedState } from '../hydration-server'
|
||||
import Marketplace from '../index'
|
||||
import { SERVER_PREFETCH_BUDGET_MS, withinServerBudget } from '../server-budget'
|
||||
|
||||
export const standaloneMarketplaceServer = {
|
||||
Marketplace,
|
||||
HydrateQueryClient,
|
||||
prefetchMarketplaceDehydratedState,
|
||||
SERVER_PREFETCH_BUDGET_MS,
|
||||
withinServerBudget,
|
||||
adaptCreatorProfile,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user