1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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 } };
}
|