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.js117
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"