diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/search-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/search-page.js | 83 |
1 files changed, 69 insertions, 14 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js index f144ef0..ec92e76 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js @@ -8,14 +8,22 @@ import { HUB, session, cacheGroupIndex, hubFetch, ensureFreshToken, _loadBundleKey, } from './hub-client.js'; import { FilesPanel, FilePreview } from './files-app.js'; -import { VideoApp, bumpMediaMetaGeneration, bumpThumbGeneration } from './video-app.js'; -import { MusicApp, bumpMusicMetaGeneration } from './music-app.js'; +import { VideoApp } from './video-app.js'; +import { MusicApp } from './music-app.js'; import { PhotosApp } from './photos-app.js'; import { VideoPlayer } from './video-player.js'; import { transfers } from './transfers.js'; const BATCH_SIZE = 3; -const MAX_POOL_SIZE = 3; +// One WebRTC peer connection per group the search view touches. The cap bounds +// how many a busy account (many groups on the hub) keeps open at once; groups +// past it connect lazily when a tile of theirs scrolls into view (video-app.js's +// LazyTile → onNeedConn). Was 3, which meant any account with more than three +// groups thrashed the pool: an evicted transport left a stale `_tRef` on every +// tile of that group, and MediaThumb / useMediaMeta gave up on it with no +// retry — so posters rendered for a moment, then fell back to a spinner for +// good. grenet (one shared group) never hit it; cbesson (many) always did. +const MAX_POOL_SIZE = 12; const DEBOUNCE_MS = 200; const SEARCH_TIMEOUT = 10000; const SEARCH_VIDEO_ROOT = '__search__'; @@ -25,10 +33,14 @@ const SEARCH_PHOTO_ROOTS = ['__search_photos__']; // -- Connection pool ---------------------------------------------------------- class ConnectionPool { - constructor(hubBase) { + constructor(hubBase, onEvict) { this._hubBase = hubBase; this._connections = new Map(); this._connecting = new Map(); + // Called with a groupId whenever this pool closes that group's connection + // (eviction or closeAll). SearchPage uses it to drop its own record so it + // never hands a tile a `_tRef` pointing at a transport just closed here. + this._onEvict = onEvict || (() => {}); } async connect(groupId, token, bundleKey, username, userId) { @@ -94,12 +106,14 @@ class ConnectionPool { const conn = this._connections.get(oldestId); try { conn.transport.close(); } catch {} this._connections.delete(oldestId); + this._onEvict(oldestId); } } closeAll() { - for (const [, conn] of this._connections) { + for (const [id, conn] of this._connections) { try { conn.transport.close(); } catch {} + this._onEvict(id); } this._connections.clear(); for (const [, p] of this._connecting) { @@ -202,13 +216,28 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) const modalTransportRef = useRef(null); const modalGekRef = useRef(null); const groupConns = useRef(new Map()); + // groupId -> how many times its connection has been (re)established. Threaded + // into each entry as `_connGen` and used by video-app.js's tiles as a refetch + // key, so a group's posters/thumbnails recover the moment it reconnects + // (after a pool eviction) instead of staying stuck on a spinner. + const connGenRef = useRef(new Map()); + const mountedRef = useRef(true); const [connectionGen, setConnectionGen] = useState(0); const debounceRef = useRef(null); const [debouncedQuery, setDebouncedQuery] = useState(''); useEffect(() => { - poolRef.current = new ConnectionPool(HUB); - return () => { if (poolRef.current) poolRef.current.closeAll(); }; + mountedRef.current = true; + poolRef.current = new ConnectionPool(HUB, (evictedId) => { + groupConns.current.delete(evictedId); + // Rebuild the entry lists so tiles of the evicted group fall back to a + // null `_tRef` (and pick a live one up again once reconnected). + if (mountedRef.current) setConnectionGen((g) => g + 1); + }); + return () => { + mountedRef.current = false; + if (poolRef.current) poolRef.current.closeAll(); + }; }, []); useEffect(() => { @@ -247,29 +276,50 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) if (!poolRef.current) throw new Error('no pool'); const bundleKey = session.bundleKey || await _loadBundleKey(); const conn = await poolRef.current.connect(groupId, token, bundleKey, username, userId); + + // A concurrent caller for the same group (several tiles mounting at once) + // may already have recorded this exact transport while we awaited. Only + // treat it as new — bump the per-group generation, force a rebuild — when + // it genuinely is, so a group's tiles refetch once per (re)connect rather + // than once per mounting tile. + const existing = groupConns.current.get(groupId); + if (existing && existing.transport === conn.transport) { + existing.lastUsed = Date.now(); + return existing; + } + + const gen = (connGenRef.current.get(groupId) || 0) + 1; + connGenRef.current.set(groupId, gen); const entry = { transport: conn.transport, gek: conn.gek, + gen, tRef: { current: conn.transport }, gRef: { current: conn.gek }, }; groupConns.current.set(groupId, entry); + // No bumpMediaMetaGeneration() / bumpThumbGeneration() here: a fresh + // transport does not invalidate metadata another group already resolved, + // and firing the module-wide reset on every connect is what made the whole + // grid flicker through the pre-connect walk. Recovery for *this* group's + // tiles comes from `_connGen` (threaded into its entries) instead; the + // module-wide bumps stay for an operator's TMDB override/rematch only. setConnectionGen((g) => g + 1); - bumpMediaMetaGeneration(); - bumpThumbGeneration(); - bumpMusicMetaGeneration(); return entry; }, [token, username, userId]); - // Pre-connect to all groups as soon as indexing finishes so thumbnails - // start loading before the user switches views. The pool evicts old - // connections but _thumbBlobCache keeps fetched thumbnails across evictions. + // Warm up connections as soon as indexing finishes so thumbnails start + // loading before the user switches views — but only up to the pool's + // capacity. Warming every group would just evict the earlier ones before + // the user ever gets there; groups past the cap connect lazily when a tile + // of theirs scrolls into view (onNeedConn, below). _thumbBlobCache keeps + // fetched thumbnails across evictions. useEffect(() => { if (fetching || indexedGroups.size === 0) return; let cancelled = false; (async () => { - const groupIds = [...indexedGroups.keys()]; + const groupIds = [...indexedGroups.keys()].slice(0, MAX_POOL_SIZE); for (let i = 0; i < groupIds.length; i += BATCH_SIZE) { if (cancelled) break; const batch = groupIds.slice(i, i + BATCH_SIZE); @@ -325,6 +375,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) groupOwner: data.groupOwner, _tRef: conn ? conn.tRef : null, _gRef: conn ? conn.gRef : null, + _connGen: conn ? conn.gen : 0, }); } } @@ -356,6 +407,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) groupOwner: data.groupOwner, _tRef: conn ? conn.tRef : null, _gRef: conn ? conn.gRef : null, + _connGen: conn ? conn.gen : 0, }); } } @@ -381,6 +433,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) groupOwner: data.groupOwner, _tRef: conn ? conn.tRef : null, _gRef: conn ? conn.gRef : null, + _connGen: conn ? conn.gen : 0, }); } } @@ -407,6 +460,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) groupOwner: data.groupOwner, _tRef: conn ? conn.tRef : null, _gRef: conn ? conn.gRef : null, + _connGen: conn ? conn.gen : 0, }); } } @@ -570,6 +624,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) videoRoot=${SEARCH_VIDEO_ROOT} tmdbConfig=${{ enabled: true }} isNodeAdmin=${false} + onNeedConn=${connectGroup} hideFilter=${true} /> `} |