diff options
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.js | 126 |
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 } }; +} |