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 | 117 |
1 files changed, 73 insertions, 44 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 ee99ccc..a77e8c3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js @@ -32,6 +32,51 @@ const SEARCH_VIDEO_ROOT = '__search__'; const SEARCH_AUDIO_ROOT = '__search__'; const SEARCH_PHOTO_ROOTS = ['__search_photos__']; +// -- Connecting to a group ---------------------------------------------------- + +/** + * A live connection to one of the nodes serving `groupId`, and its ack — or + * null when the hub lists no node at all. + * + * Every node the hub lists, in turn, exactly as group-page.js does since + * 2026-09-11: the list is in hub registration order, and its head is not + * necessarily a node that answers. Search took `nodes[0]` and stopped, so a + * group with a second, working node counted as unreachable here while it + * opened fine from the sidebar. A refusal naming a state of this browser (a + * code, a passphrase, a device) is the same from every node and stops the + * walk; `not_hosted`, a timeout or a failed connection moves on. + */ +async function connectToGroup(hubBase, groupId, token, bundleKey, username, userId) { + const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token }); + if (!nodesData.nodes || !nodesData.nodes.length) return null; + + const live = (await ensureFreshToken()) || token; + let lastErr = null; + for (const n of nodesData.nodes) { + const transport = new window.MeshBayTransport(hubBase, live); + transport.onNeedToken = async () => (await ensureFreshToken()) || token; + let timer; + try { + const ack = await Promise.race([ + transport.connect( + n.node_id, live, groupId, null, null, bundleKey, + username, userId, null), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT); + }), + ]); + clearTimeout(timer); + return { transport, ack }; + } catch (e) { + clearTimeout(timer); + lastErr = e; + try { transport.close(); } catch {} + if (e.reason && e.reason !== 'not_hosted') throw e; + } + } + throw (lastErr || new Error('no node served this group')); +} + // -- Connection pool ---------------------------------------------------------- class ConnectionPool { @@ -66,29 +111,10 @@ class ConnectionPool { } async _doConnect(groupId, token, bundleKey, username, userId) { - const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token }); - if (!nodesData.nodes || !nodesData.nodes.length) throw new Error('offline'); - - const live = (await ensureFreshToken()) || token; - const transport = new window.MeshBayTransport(this._hubBase, live); - transport.onNeedToken = async () => (await ensureFreshToken()) || token; - - let timer; - try { - await Promise.race([ - transport.connect( - nodesData.nodes[0].node_id, live, groupId, null, null, bundleKey, - username, userId, null), - new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT); - }), - ]); - clearTimeout(timer); - } catch (e) { - clearTimeout(timer); - try { transport.close(); } catch {} - throw e; - } + const found = await connectToGroup( + this._hubBase, groupId, token, bundleKey, username, userId); + if (!found) throw new Error('offline'); + const { transport } = found; let gek = null; if (transport.gekRaw && window.MeshBayCrypto) { @@ -128,24 +154,17 @@ class ConnectionPool { // -- Index fetching ----------------------------------------------------------- async function fetchGroupIndex(groupId, token, bundleKey, username, userId) { - const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token }); - if (!nodesData.nodes || !nodesData.nodes.length) return null; - - const live = (await ensureFreshToken()) || token; - const transport = new window.MeshBayTransport(HUB, live); - transport.onNeedToken = async () => (await ensureFreshToken()) || token; + const found = await connectToGroup(HUB, groupId, token, bundleKey, username, userId); + if (!found) return null; + const { transport, ack } = found; try { - let timer; - const ack = await Promise.race([ - transport.connect( - nodesData.nodes[0].node_id, live, groupId, null, null, bundleKey, - username, userId, null), - new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT); - }), - ]); - clearTimeout(timer); + // The operator asked for this group to stay out of the global listing. + // Decided here, before the index is asked for, so nothing of it is held, + // cached or merged by this page. A listing preference and not a boundary: + // the node cannot tell this request from the group page's, and opening the + // group lists everything. + if (ack.search_listed === false) return { unlisted: true }; const indexMsg = await transport.fetchIndex(); // Plural, with the old scalars as the fallback for a node still speaking @@ -174,6 +193,7 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa const total = groups.length; let done = 0; const unreachable = []; + const unlisted = []; const results = new Map(); for (let i = 0; i < groups.length; i += BATCH_SIZE) { @@ -181,7 +201,9 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa await Promise.all(batch.map(async (g) => { try { const result = await fetchGroupIndex(g.id, token, bundleKey, username, userId); - if (result) { + if (result && result.unlisted) { + unlisted.push(g.name || g.id); + } else if (result) { results.set(g.id, { ...result, groupName: g.name, @@ -195,11 +217,11 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa unreachable.push(g.name || g.id); } done++; - onProgress({ done, total, unreachable: [...unreachable] }); + onProgress({ done, total, unreachable: [...unreachable], unlisted: [...unlisted] }); })); onBatch(new Map(results)); } - return { results, unreachable }; + return { results, unreachable, unlisted }; } // -- SearchPage --------------------------------------------------------------- @@ -281,7 +303,8 @@ function photoUnits(entries) { function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) { const [indexedGroups, setIndexedGroups] = useState(new Map()); - const [progress, setProgress] = useState({ done: 0, total: 0, unreachable: [] }); + const [progress, setProgress] = useState( + { done: 0, total: 0, unreachable: [], unlisted: [] }); const [fetching, setFetching] = useState(false); // Bumped by the Files breadcrumb refresh button — re-runs the all-groups // index fetch below, the only "cache" this page has. @@ -333,7 +356,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) (async () => { setFetching(true); - setProgress({ done: 0, total: groups.length, unreachable: [] }); + setProgress({ done: 0, total: groups.length, unreachable: [], unlisted: [] }); await fetchAllIndexes( groups, token, username, userId, @@ -718,6 +741,12 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) </p> `} + ${!fetching && progress.unlisted.length > 0 && html` + <p class="search-unreachable"> + ${t('search.unlisted', { n: progress.unlisted.length })} + </p> + `} + ${viewMode === 'files' && hasResults && html` <${FilesPanel} groupId="search" |