git ssb

16+

Dominic / patchbay



Tree: c4c384106bcf50acf4c0fcb3f7b842ad68f25bcb

Files: c4c384106bcf50acf4c0fcb3f7b842ad68f25bcb / app / page / network.js

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

Built with git-ssb-web