Files: 04ad1e20ea2e76b531a77a2df5f1ea712d81f034 / app / page / network.js
8861 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Value, Dict, dictToCollection, onceTrue, computed, watch, watchAll, throttle } = require('mutant') |
3 | const Chart = require('chart.js') |
4 | const pull = require('pull-stream') |
5 | |
6 | const MINUTE = 60 * 1000 |
7 | const DAY = 24 * 60 * MINUTE |
8 | |
9 | const GRAPH_Y_STEP = 50 |
10 | const GRAPH_Y_MIN = 100 |
11 | |
12 | exports.gives = nest({ |
13 | 'app.html.menuItem': true, |
14 | 'app.page.network': true |
15 | }) |
16 | |
17 | exports.needs = nest({ |
18 | 'about.html.avatar': 'first', |
19 | 'app.html.scroller': 'first', |
20 | 'app.sync.goTo': 'first', |
21 | 'sbot.obs.connection': 'first', |
22 | 'sbot.obs.localPeers': 'first', |
23 | 'sbot.obs.connectedPeers': 'first' |
24 | }) |
25 | |
26 | exports.create = function (api) { |
27 | return nest({ |
28 | 'app.html.menuItem': menuItem, |
29 | 'app.page.network': networkPage |
30 | }) |
31 | |
32 | function menuItem () { |
33 | return h('a', { |
34 | 'ev-click': () => api.app.sync.goTo({ page: 'network' }) |
35 | }, '/network') |
36 | } |
37 | |
38 | function networkPage (location) { |
39 | const minsPerStep = 10 |
40 | const scale = 1 * DAY |
41 | |
42 | const state = buildState({ api, minsPerStep, scale }) |
43 | const canvas = h('canvas', { height: 500, width: 1200, style: { height: '500px', width: '1200px' } }) |
44 | |
45 | const page = h('NetworkPage', [ |
46 | h('div.container', [ |
47 | h('h1', 'Network'), |
48 | h('section', [ |
49 | h('h2', [ |
50 | 'Local Peers', |
51 | h('i.fa.fa-question-circle-o', { title: 'these are people on the same WiFi/ LAN as you right now. You might not know some of them yet, but you can click through to find out more about them and follow them if you like.' }) |
52 | ]), |
53 | computed(state.localPeers, peers => { |
54 | if (!peers.length) return h('p', 'No local peers (on same wifi/ LAN)') |
55 | |
56 | return peers.map(peer => api.about.html.avatar(peer)) |
57 | }) |
58 | ]), |
59 | h('section', [ |
60 | h('h2', [ |
61 | 'Remote Peers', |
62 | h('i.fa.fa-question-circle-o', { title: 'these are peers which have fixed addresses, and are likely friends of friends (a.k.a. pubs)' }) |
63 | ]), |
64 | computed(state.remotePeers, peers => { |
65 | if (!peers.length) return h('p', 'No remote peers connected') |
66 | |
67 | return peers.map(peer => api.about.html.avatar(peer)) |
68 | }) |
69 | ]), |
70 | h('section', [ |
71 | h('h2', 'My state'), |
72 | // mix: hello friend, this area is a total Work In Progress. It's a mess but useful diagnostics. |
73 | // Let's redesign and revisit it aye! |
74 | h('div', ['My sequence: ', state.seq]), |
75 | h('div', [ |
76 | 'Replicated:', |
77 | h('div', computed([state.seq, dictToCollection(state.replication)], (seq, replication) => { |
78 | return replication.map(r => { |
79 | if (!r.value.replicating) { |
80 | return h('div', [ |
81 | h('code', r.key), |
82 | ' no ebt data' |
83 | ]) |
84 | } |
85 | |
86 | const { requested, sent } = r.value.replicating |
87 | // TODO report that r.value.seq is NOT the current local value of the seq (well it's ok, just just gets out of sync) |
88 | // const reqDiff = requested - r.value.seq |
89 | const reqDiff = requested - seq |
90 | const sentDiff = sent - seq |
91 | |
92 | return h('div', [ |
93 | h('code', r.key), |
94 | ` - requested: ${requested} `, |
95 | reqDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${reqDiff})`, |
96 | `, sent: ${sent} `, |
97 | sentDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${sentDiff})` |
98 | ]) |
99 | }) |
100 | })) |
101 | ]) |
102 | ]), |
103 | h('section', [ |
104 | h('h2', [ |
105 | 'Received Messages', |
106 | h('i.fa.fa-question-circle-o', { |
107 | title: `Messages received per ${minsPerStep}-minute block over the last ${scale / DAY} days` |
108 | }) |
109 | ]), |
110 | canvas |
111 | ]) |
112 | ]) |
113 | ]) |
114 | |
115 | initialiseChart({ canvas, state }) |
116 | |
117 | var { container } = api.app.html.scroller({ prepend: page }) |
118 | container.title = '/network' |
119 | return container |
120 | } |
121 | } |
122 | |
123 | function initialiseChart ({ canvas, state: { data, range } }) { |
124 | var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) |
125 | |
126 | watch(range, ({ lower, upper }) => { |
127 | // set horizontal scale |
128 | chart.options.scales.xAxes[0].time.min = lower |
129 | chart.options.scales.xAxes[0].time.max = upper |
130 | chart.update() |
131 | }) |
132 | |
133 | watchAll([throttle(data, 300), range], (data, { lower, upper }) => { |
134 | const _data = Object.keys(data) |
135 | .sort((a, b) => a < b ? -1 : +1) |
136 | .map(ts => { |
137 | return { |
138 | t: Number(ts), // NOTE - might need to offset by a half-step ? |
139 | y: data[ts] |
140 | } |
141 | }) |
142 | |
143 | // update chard data |
144 | chart.data.datasets[0].data = _data |
145 | |
146 | // scales the height of the graph (to the visible data)! |
147 | const slice = _data |
148 | .filter(d => d.t >= lower && d.t < upper) |
149 | .map(d => d.y) |
150 | .sort((a, b) => a > b ? -1 : +1) |
151 | |
152 | var h = slice[0] |
153 | if (!h || h < GRAPH_Y_MIN) h = GRAPH_Y_MIN // min-height |
154 | else h = h + (GRAPH_Y_STEP - h % GRAPH_Y_STEP) // round height to multiples of GRAPH_Y_STEP |
155 | chart.options.scales.yAxes[0].ticks.max = h |
156 | |
157 | chart.update() |
158 | }) |
159 | } |
160 | |
161 | // ///// HELPERS ///// |
162 | |
163 | function buildState ({ api, minsPerStep, scale }) { |
164 | // build localPeers, remotePeers |
165 | const localPeers = throttle(api.sbot.obs.localPeers(), 1000) |
166 | const remotePeers = computed([localPeers, throttle(api.sbot.obs.connectedPeers(), 1000)], (local, connected) => { |
167 | return connected.filter(peer => !local.includes(peer)) |
168 | }) |
169 | |
170 | // build seq, replication (my current state, and replicated state) |
171 | const seq = Value() |
172 | const replication = Dict({}) |
173 | onceTrue(api.sbot.obs.connection, server => { |
174 | setInterval(() => { |
175 | // TODO check ebt docs if this is best method |
176 | server.ebt.peerStatus(server.id, (err, data) => { |
177 | if (err) return console.error(err) |
178 | |
179 | seq.set(data.seq) |
180 | for (var peer in data.peers) { |
181 | replication.put(peer, data.peers[peer]) |
182 | } |
183 | }) |
184 | }, 5e3) |
185 | }) |
186 | |
187 | // build data, range |
188 | const data = Dict({ |
189 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, |
190 | [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 |
191 | }) |
192 | onceTrue(api.sbot.obs.connection, server => { |
193 | getData({ data, server, minsPerStep, scale }) |
194 | }) |
195 | |
196 | const latest = Value(toTimeBlock(Date.now(), minsPerStep)) |
197 | // start of the most recent bar |
198 | setInterval(() => { |
199 | latest.set(toTimeBlock(Date.now(), minsPerStep)) |
200 | }, minsPerStep / 4 * MINUTE) |
201 | |
202 | const range = computed([latest], (latest) => { |
203 | return { |
204 | upper: latest + minsPerStep * MINUTE, |
205 | lower: latest + minsPerStep * MINUTE - scale |
206 | } |
207 | }) |
208 | return { |
209 | localPeers, |
210 | remotePeers, |
211 | seq, |
212 | replication, |
213 | data, // TODO rename this !! |
214 | range |
215 | } |
216 | } |
217 | |
218 | function getData ({ data, server, minsPerStep, scale }) { |
219 | const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE |
220 | const lowerBound = upperEnd - scale |
221 | |
222 | const query = [ |
223 | { |
224 | $filter: { |
225 | timestamp: { $gte: lowerBound } |
226 | } |
227 | }, { |
228 | $filter: { |
229 | value: { |
230 | author: { $ne: server.id } |
231 | } |
232 | } |
233 | }, { |
234 | $map: { |
235 | ts: ['timestamp'] |
236 | } |
237 | } |
238 | ] |
239 | |
240 | pull( |
241 | server.query.read({ query, live: true }), |
242 | pull.filter(m => !m.sync), |
243 | pull.map(m => toTimeBlock(m.ts, minsPerStep)), |
244 | pull.drain(ts => { |
245 | if (data.has(ts)) data.put(ts, data.get(ts) + 1) |
246 | else data.put(ts, 1) |
247 | }) |
248 | ) |
249 | } |
250 | |
251 | function toTimeBlock (ts, minsPerStep) { |
252 | return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) |
253 | } |
254 | |
255 | function chartConfig ({ lower, upper }) { |
256 | const barColor = 'hsla(215, 57%, 60%, 1)' |
257 | |
258 | return { |
259 | type: 'bar', |
260 | data: { |
261 | datasets: [{ |
262 | backgroundColor: barColor, |
263 | borderColor: barColor, |
264 | data: [] |
265 | }] |
266 | }, |
267 | options: { |
268 | legend: { |
269 | display: false |
270 | }, |
271 | scales: { |
272 | xAxes: [{ |
273 | type: 'time', |
274 | distribution: 'linear', |
275 | time: { |
276 | // unit: 'day', |
277 | // min: lower, |
278 | // max: upper, |
279 | // tooltipFormat: 'MMMM D', |
280 | // stepSize: 7 |
281 | unit: 'minute', |
282 | min: lower, |
283 | max: upper, |
284 | tooltipFormat: 'HH:mm', |
285 | stepSize: 4 * 60 |
286 | // stepSize: 240 |
287 | }, |
288 | bounds: 'ticks', |
289 | ticks: { |
290 | // maxTicksLimit: 4 // already disabled |
291 | }, |
292 | gridLines: { |
293 | display: false |
294 | } |
295 | // maxBarThickness: 2 |
296 | }], |
297 | |
298 | yAxes: [{ |
299 | ticks: { |
300 | min: 0, |
301 | stepSize: GRAPH_Y_STEP, |
302 | suggestedMax: GRAPH_Y_MIN |
303 | } |
304 | }] |
305 | }, |
306 | animation: { |
307 | // duration: 300 |
308 | } |
309 | } |
310 | } |
311 | } |
312 |
Built with git-ssb-web