diff --git a/ui/src/common/Pagination.jsx b/ui/src/common/Pagination.jsx index e17d9e63e..0c1a864bb 100644 --- a/ui/src/common/Pagination.jsx +++ b/ui/src/common/Pagination.jsx @@ -1,6 +1,18 @@ -import React from 'react' -import { Pagination as RAPagination } from 'react-admin' +import React, { useEffect, useRef } from 'react' +import { Pagination as RAPagination, useListContext } from 'react-admin' +import { abortAllInFlight } from './useImageUrl' -export const Pagination = (props) => ( - -) +export const Pagination = (props) => { + const { page, perPage } = useListContext() + const prevRef = useRef({ page, perPage }) + + useEffect(() => { + const prev = prevRef.current + if (prev.page !== page || prev.perPage !== perPage) { + abortAllInFlight() + prevRef.current = { page, perPage } + } + }, [page, perPage]) + + return +} diff --git a/ui/src/common/useImageUrl.js b/ui/src/common/useImageUrl.js index ecb8f0a17..ae9f34003 100644 --- a/ui/src/common/useImageUrl.js +++ b/ui/src/common/useImageUrl.js @@ -4,6 +4,18 @@ import { useEffect, useState, useRef } from 'react' // React Admin refreshes (which remount list items) don't re-fetch images. const cache = new Map() const MAX_CACHE_SIZE = 300 +const activeControllers = new Set() + +/** + * Aborts all in-flight image fetches. Call this before navigation/pagination + * so that pending image requests don't block the browser connection pool. + */ +export const abortAllInFlight = () => { + for (const controller of activeControllers) { + controller.abort() + } + activeControllers.clear() +} // Evicts oldest unused entries (Map iterates in insertion order). const evictIfNeeded = () => { @@ -53,6 +65,7 @@ export const useImageUrl = (url) => { } const controller = new AbortController() + activeControllers.add(controller) setImgUrl(null) setLoading(true) setError(false) @@ -83,8 +96,10 @@ export const useImageUrl = (url) => { setImgUrl(objectUrl) } setLoading(false) + activeControllers.delete(controller) }) .catch((err) => { + activeControllers.delete(controller) if (err.name === 'AbortError') { return // Expected on unmount or URL change } @@ -97,6 +112,7 @@ export const useImageUrl = (url) => { return () => { abortedRef.current = true controller.abort() + activeControllers.delete(controller) const entry = cache.get(url) if (entry) { entry.refCount--