aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/index-dock-model.js
blob: bd4586b5a91fc947da249bd5c9e3984a98fb82d5 (plain) (blame)
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 } };
}