aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js126
1 files changed, 126 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js b/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js
new file mode 100644
index 0000000..bd4586b
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js
@@ -0,0 +1,126 @@
+/**
+ * What the indexing dock shows, worked out from what nodes report. No DOM and no
+ * imports, so tests/test_indexing_dock.py runs this file under node as it is.
+ *
+ * Two sources describe a group's indexing:
+ *
+ * - The loopback API (`GET /api/index-status`, the node's ui/app.py) answers on
+ * the node's own machine, for every group it hosts, whatever page is open,
+ * and names the roots.
+ * - MNP `index_progress` pushes reach an operator anywhere, but only while that
+ * group's page holds a connection, and carry counters only (decision D3): the
+ * root under way is a position in the roots table the page already opened,
+ * and the roots waiting are a count.
+ *
+ * Where both describe a group, the loopback answer wins. It is the fuller one.
+ */
+
+// A reconcile pass or a burst of watchdog events is usually a file or two, done
+// in less time than it takes to read a bar. Shown only past either threshold;
+// a root walked for the first time, or again, is always shown.
+export const NOISE_BYTES = 1024 ** 3;
+export const NOISE_MS = 5000;
+// How long "indexing finished" stays on screen.
+export const DONE_MS = 4000;
+
+export function fromLoopback(g) {
+ return {
+ groupId: g.group_id,
+ groupName: g.group_name || '',
+ scanning: Boolean(g.scanning),
+ kind: g.kind || '',
+ root: g.root || '',
+ scannedBytes: g.scanned_bytes || 0,
+ totalBytes: g.total_bytes || 0,
+ filesDone: g.files_done || 0,
+ filesTotal: g.files_total || 0,
+ queued: Array.isArray(g.queued) ? g.queued : [],
+ };
+}
+
+export function fromPush(groupId, s, roots) {
+ const pos = Number.isInteger(s.root_pos) ? s.root_pos : -1;
+ const known = pos >= 0 && Array.isArray(roots) && roots[pos];
+ return {
+ groupId,
+ groupName: '',
+ scanning: Boolean(s.scanning),
+ kind: s.kind || '',
+ root: known ? (roots[pos].name || '') : '',
+ scannedBytes: s.scanned_bytes || 0,
+ totalBytes: s.total_bytes || 0,
+ filesDone: s.files_done || 0,
+ filesTotal: s.files_total || 0,
+ queued: Number.isInteger(s.queued) ? s.queued : 0,
+ };
+}
+
+export function queuedCount(job) {
+ return Array.isArray(job.queued) ? job.queued.length : (job.queued || 0);
+}
+
+export function percent(job) {
+ return job.totalBytes
+ ? Math.min(100, Math.round(100 * job.scannedBytes / job.totalBytes)) : 0;
+}
+
+/** One job per group, loopback first, named after the hub's group list. */
+export function mergeActivity(local, pushed, groups) {
+ const names = new Map((groups || []).map((g) => [g.id, g.name]));
+ const ids = [...new Set([...Object.keys(local), ...Object.keys(pushed)])];
+ return ids.map((gid) => {
+ const job = local[gid] || pushed[gid];
+ return { ...job, groupName: names.get(gid) || job.groupName || gid.slice(0, 8) };
+ });
+}
+
+function busy(job) {
+ return job.scanning || queuedCount(job) > 0;
+}
+
+function loud(job, since, now) {
+ // '' while scanning is a node older than `kind`: shown, as it always was.
+ return job.kind === 'scan' || job.kind === 'rescan' || job.kind === ''
+ || queuedCount(job) > 0
+ || job.totalBytes >= NOISE_BYTES
+ || now - since >= NOISE_MS;
+}
+
+/**
+ * The rows to draw, and what to remember for the next call.
+ *
+ * `memory` is per group: when the current walk was first seen, whether its row
+ * has been shown (a burst that crossed the threshold stays up until it ends),
+ * whether the operator hid it, and when it finished. Pure: same arguments, same
+ * answer, so a render that runs twice does not flash anything twice.
+ */
+export function dockRows(jobs, memory, now) {
+ const next = {};
+ const rows = [];
+ for (const job of jobs) {
+ const gid = job.groupId;
+ const prev = memory[gid] || {};
+ if (busy(job)) {
+ const sig = `${job.kind}|${job.root}`;
+ const since = prev.busy && prev.sig === sig ? prev.since : now;
+ const shown = Boolean(prev.busy && prev.shown) || loud(job, since, now);
+ const hidden = Boolean(prev.busy && prev.hidden);
+ next[gid] = { busy: true, sig, since, shown, hidden, doneAt: 0 };
+ if (shown && !hidden) rows.push({ ...job, state: 'running' });
+ } else if (prev.busy && prev.shown && !prev.hidden) {
+ next[gid] = { busy: false, doneAt: now };
+ rows.push({ ...job, state: 'done' });
+ } else if (prev.doneAt && now - prev.doneAt < DONE_MS) {
+ next[gid] = prev;
+ rows.push({ ...job, state: 'done' });
+ }
+ }
+ return { rows, memory: next };
+}
+
+/** Hidden until that group has nothing left to index. */
+export function hideRow(memory, groupId) {
+ const prev = memory[groupId];
+ if (!prev || !prev.busy) return memory;
+ return { ...memory, [groupId]: { ...prev, hidden: true } };
+}