aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
diff options
context:
space:
mode:
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.js83
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} />
`}