mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
Three independent defects made catalog search feel unstable. Stale results flash on every search. `isSearchMode` was derived from the raw nuqs `q` while the request body used the 500ms-debounced value, so keystroke #1 flipped into search mode with an empty query. That fired a full empty-query search whose generic top-plugins list rendered for the debounce window before the real results replaced it. Both now read the same debounced value. Grid unmount on every query change. The infinite query had no `placeholderData`, so `data` went undefined whenever the key changed and ListWrapper's `!isLoading` gate tore the whole result grid out, collapsing the container and jumping the scroll position. Measured against a live catalog, a sort change blanked the grid for 152 of 360 sampled frames; with `keepPreviousData` and an `isRefreshing` dim it is 0 of 461. Requests that never settle. The Marketplace oRPC link called `globalThis.fetch` with no deadline, so a stalled connection left the query pending forever with no error state and no retry — the reported permanent spinner. Requests now carry a 15s deadline composed with (never replacing) react-query's abort signal. The server prefetch and the banner fetch get their own 2.5s budget: awaiting them unbounded held the whole RSC response, pushing time-to-first-byte to ~7s against a 3s-delayed API while the browser sat on the previous page. Supporting changes: - `getMarketplacePlugins` and `getMarketplaceCollectionsAndPlugins` no longer swallow every rejection into a successful empty page. That turned outages and aborted keystrokes into "no plugins found", suppressed retries, cached the emptiness for the full staleTime, and killed `getNextPageParam` permanently via `total: 0`. ListWrapper now renders a retry affordance. - The collections fan-out is bounded to 4 concurrent requests instead of firing one per collection at once, and per-collection failures degrade to an empty carousel rather than a blank catalog. - Scroll pagination measures once per frame instead of per scroll event. Each threshold hit calls `fetchNextPage`, which defaults to `cancelRefetch: true`, so an unthrottled burst aborted and restarted the in-flight page request — and the backend counts those aborts against its search circuit breaker. - Cross-page results are deduped by `org/name`; Meilisearch resolves `install_count` ties by document order and the sync task rewrites those documents every minute, so pages can overlap and collide on React keys. - Search queries no longer inherit `retry: 3`; ~7s of backoff reads as a hang.
36 lines
1.7 KiB
TypeScript
36 lines
1.7 KiB
TypeScript
import type { PluginsSearchParams } from '@dify/contracts/marketplace'
|
|
import { infiniteQueryOptions, keepPreviousData } from '@tanstack/react-query'
|
|
import { marketplaceQuery } from '@/service/client'
|
|
import { getMarketplacePlugins } from './utils'
|
|
|
|
export const getMarketplacePluginsInfiniteQueryOptions = (
|
|
queryParams: PluginsSearchParams | undefined,
|
|
) =>
|
|
infiniteQueryOptions({
|
|
queryKey: marketplaceQuery.searchAdvanced.queryKey({
|
|
input: {
|
|
body: queryParams ?? { query: '' },
|
|
params: { kind: queryParams?.type === 'bundle' ? 'bundles' : 'plugins' },
|
|
},
|
|
}),
|
|
queryFn: ({ pageParam = 1, signal }) => getMarketplacePlugins(queryParams, pageParam, signal),
|
|
getNextPageParam: (lastPage) => {
|
|
const nextPage = lastPage.page + 1
|
|
const loaded = lastPage.page * lastPage.page_size
|
|
return loaded < (lastPage.total || 0) ? nextPage : undefined
|
|
},
|
|
initialPageParam: 1,
|
|
enabled: queryParams !== undefined,
|
|
// Hold the previous term's results while the new query is in flight. Without
|
|
// this, `data` goes undefined on every keystroke that survives the debounce,
|
|
// the grid unmounts, the container collapses, and the scroll position jumps —
|
|
// the "jitter" the Marketplace search is reported for. Consumers show a
|
|
// quiet pending state off `isPlaceholderData` instead.
|
|
placeholderData: keepPreviousData,
|
|
// Matches the autocomplete queries. Now that the fetcher propagates
|
|
// failures, react-query's default of 3 retries would hold isFetching true
|
|
// through ~7s of backoff — indistinguishable from a hang. Failing fast and
|
|
// offering an explicit Retry is both honest and fewer requests to abort.
|
|
retry: false,
|
|
})
|