From 3bc09f9d0374aa63572a381e38a30e2f2cec4da8 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 25 Mar 2026 19:58:49 -0400 Subject: [PATCH] feat(ui): abort all in-flight image fetches on pagination change Pagination component now watches page/perPage via useListContext and calls abortAllInFlight() when either changes, freeing the browser connection pool immediately for the next page's data request. Also adds empty placeholder style to CoverArtAvatar so it renders as a clean transparent area while loading instead of the default person icon. --- ui/src/common/Pagination.jsx | 22 +++++++++++++++++----- ui/src/common/useImageUrl.js | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) 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--