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.
This commit is contained in:
Deluan 2026-03-25 19:58:49 -04:00
parent cafcb0bb59
commit 3bc09f9d03
2 changed files with 33 additions and 5 deletions

View file

@ -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) => (
<RAPagination rowsPerPageOptions={[15, 25, 50]} {...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 <RAPagination rowsPerPageOptions={[15, 25, 50]} {...props} />
}

View file

@ -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--