Commit 6b5fb10bffe7df13233ba65f8a3631d6121ed3cd
add feed sync and index progress to user interface
Matt McKegg committed on 2/24/2017, 5:34:43 PMParent: 1e7b8b22b00038209334ed25b4c8b3e151780691
Files changed
lib/friends-with-sync.js | ||
---|---|---|
@@ -1,0 +1,184 @@ | ||
1 … | +var Graphmitter = require('graphmitter') | |
2 … | +var pull = require('pull-stream') | |
3 … | +var mlib = require('ssb-msgs') | |
4 … | +var memview = require('level-memview') | |
5 … | +var pushable = require('pull-pushable') | |
6 … | +var mdm = require('mdmanifest') | |
7 … | +var valid = require('scuttlebot/lib/validators') | |
8 … | +var apidoc = require('scuttlebot/lib/apidocs').friends | |
9 … | + | |
10 … | +// friends plugin | |
11 … | +// methods to analyze the social graph | |
12 … | +// maintains a 'follow' and 'flag' graph | |
13 … | + | |
14 … | +function isFunction (f) { | |
15 … | + return 'function' === typeof f | |
16 … | +} | |
17 … | + | |
18 … | +function isString (s) { | |
19 … | + return 'string' === typeof s | |
20 … | +} | |
21 … | + | |
22 … | +function isFriend (friends, a, b) { | |
23 … | + return friends[a] && friends[b] && friends[a][b] && friends[b][a] | |
24 … | +} | |
25 … | + | |
26 … | +exports.name = 'friends' | |
27 … | +exports.version = '1.0.0' | |
28 … | +exports.manifest = mdm.manifest(apidoc) | |
29 … | + | |
30 … | +exports.init = function (sbot, config) { | |
31 … | + | |
32 … | + var graphs = { | |
33 … | + follow: new Graphmitter(), | |
34 … | + flag: new Graphmitter() | |
35 … | + } | |
36 … | + | |
37 … | + // view processor | |
38 … | + var syncCbs = [] | |
39 … | + function awaitSync (cb) { | |
40 … | + if (syncCbs) syncCbs.push(cb) | |
41 … | + else cb() | |
42 … | + } | |
43 … | + | |
44 … | + // read/watch the log for changes to the social graph | |
45 … | + pull(sbot.createLogStream({ live: true }), pull.drain(function (msg) { | |
46 … | + | |
47 … | + if (msg.sync) { | |
48 … | + syncCbs.forEach(function (cb) { cb() }) | |
49 … | + syncCbs = null | |
50 … | + | |
51 … | + if (sbot.gossip) { | |
52 … | + // prioritize friends | |
53 … | + var friends = graphs['follow'].toJSON() | |
54 … | + sbot.gossip.peers().forEach(function(peer) { | |
55 … | + if (isFriend(friends, sbot.id, peer.key)) { | |
56 … | + sbot.gossip.add(peer, 'friends') | |
57 … | + } | |
58 … | + }) | |
59 … | + } | |
60 … | + | |
61 … | + return | |
62 … | + } | |
63 … | + | |
64 … | + var c = msg.value.content | |
65 … | + if (c.type == 'contact') { | |
66 … | + mlib.asLinks(c.contact, 'feed').forEach(function (link) { | |
67 … | + if ('following' in c) { | |
68 … | + if (c.following) | |
69 … | + graphs.follow.edge(msg.value.author, link.link, true) | |
70 … | + else | |
71 … | + graphs.follow.del(msg.value.author, link.link) | |
72 … | + | |
73 … | + } | |
74 … | + if ('flagged' in c) { | |
75 … | + if (c.flagged) | |
76 … | + graphs.flag.edge(msg.value.author, link.link, c.flagged) | |
77 … | + else | |
78 … | + graphs.flag.del(msg.value.author, link.link) | |
79 … | + } | |
80 … | + }) | |
81 … | + } | |
82 … | + })) | |
83 … | + | |
84 … | + return { | |
85 … | + | |
86 … | + get: valid.sync(function (opts) { | |
87 … | + var g = graphs[opts.graph || 'follow'] | |
88 … | + if(!g) throw new Error('opts.graph must be provided') | |
89 … | + return g.get(opts.source, opts.dest) | |
90 … | + }, 'object?'), | |
91 … | + | |
92 … | + all: valid.async(function (graph, cb) { | |
93 … | + if (typeof graph == 'function') { | |
94 … | + cb = graph | |
95 … | + graph = null | |
96 … | + } | |
97 … | + if (!graph) | |
98 … | + graph = 'follow' | |
99 … | + awaitSync(function () { | |
100 … | + cb(null, graphs[graph] ? graphs[graph].toJSON() : null) | |
101 … | + }) | |
102 … | + }, 'string?'), | |
103 … | + | |
104 … | + path: valid.sync(function (opts) { | |
105 … | + if(isString(opts)) | |
106 … | + opts = {source: sbot.id, dest: opts} | |
107 … | + return graphs.follow.path(opts) | |
108 … | + | |
109 … | + }, 'string|object'), | |
110 … | + | |
111 … | + createFriendStream: valid.source(function (opts) { | |
112 … | + opts = opts || {} | |
113 … | + var live = opts.live === true | |
114 … | + var meta = opts.meta === true | |
115 … | + var start = opts.start || sbot.id | |
116 … | + var graph = graphs[opts.graph || 'follow'] | |
117 … | + if(!graph) | |
118 … | + return pull.error(new Error('unknown graph:' + opts.graph)) | |
119 … | + var cancel, ps = pushable(function () { | |
120 … | + cancel && cancel() | |
121 … | + }) | |
122 … | + | |
123 … | + function push (to, hops) { | |
124 … | + return ps.push(meta ? {id: to, hops: hops} : to) | |
125 … | + } | |
126 … | + | |
127 … | + if (live) { | |
128 … | + awaitSync(function () { | |
129 … | + ps.push({sync: true}) | |
130 … | + }) | |
131 … | + } | |
132 … | + | |
133 … | + //by default, also emit your own key. | |
134 … | + if(opts.self !== false) | |
135 … | + push(start, 0) | |
136 … | + | |
137 … | + var conf = config.friends || {} | |
138 … | + cancel = graph.traverse({ | |
139 … | + start: start, | |
140 … | + hops: opts.hops || conf.hops || 3, | |
141 … | + max: opts.dunbar || conf.dunbar || 150, | |
142 … | + each: function (_, to, hops) { | |
143 … | + if(to !== start) push(to, hops) | |
144 … | + } | |
145 … | + }) | |
146 … | + | |
147 … | + if(!live) { cancel(); ps.end() } | |
148 … | + | |
149 … | + return ps | |
150 … | + }, 'createFriendStreamOpts?'), | |
151 … | + | |
152 … | + hops: valid.async(function (start, graph, opts, cb) { | |
153 … | + if (typeof opts == 'function') { // (start|opts, graph, cb) | |
154 … | + cb = opts | |
155 … | + opts = null | |
156 … | + } else if (typeof graph == 'function') { // (start|opts, cb) | |
157 … | + cb = graph | |
158 … | + opts = graph = null | |
159 … | + } | |
160 … | + opts = opts || {} | |
161 … | + if(isString(start)) { // (start, ...) | |
162 … | + // first arg is id string | |
163 … | + opts.start = start | |
164 … | + } else if (start && typeof start == 'object') { // (opts, ...) | |
165 … | + // first arg is opts | |
166 … | + for (var k in start) | |
167 … | + opts[k] = start[k] | |
168 … | + } | |
169 … | + | |
170 … | + var conf = config.friends || {} | |
171 … | + opts.start = opts.start || sbot.id | |
172 … | + opts.dunbar = opts.dunbar || conf.dunbar || 150 | |
173 … | + opts.hops = opts.hops || conf.hops || 3 | |
174 … | + | |
175 … | + var g = graphs[graph || 'follow'] | |
176 … | + if (!g) | |
177 … | + return cb(new Error('Invalid graph type: '+graph)) | |
178 … | + | |
179 … | + awaitSync(function () { | |
180 … | + cb(null, g.traverse(opts)) | |
181 … | + }) | |
182 … | + }, ['feedId', 'string?', 'object?'], ['createFriendStreamOpts']) | |
183 … | + } | |
184 … | +} |
lib/mutant-to-pull.js | ||
---|---|---|
@@ -1,0 +1,20 @@ | ||
1 … | +var watch = require('mutant/watch') | |
2 … | +var pushable = require('pull-pushable') | |
3 … | + | |
4 … | +module.exports = function (obs) { | |
5 … | + var releases = [] | |
6 … | + | |
7 … | + // create listener with `onClose` handler | |
8 … | + var listener = pushable(function onClose () { | |
9 … | + // if listener is found, delete from list | |
10 … | + while (releases.length) { | |
11 … | + releases.pop()() | |
12 … | + } | |
13 … | + }) | |
14 … | + | |
15 … | + releases.push(watch(obs, (value) => { | |
16 … | + listener.push(value) | |
17 … | + })) | |
18 … | + | |
19 … | + return listener | |
20 … | +} |
lib/query-with-progress.js | ||
---|---|---|
@@ -1,0 +1,115 @@ | ||
1 … | + | |
2 … | +var pull = require('pull-stream') | |
3 … | +var path = require('path') | |
4 … | +var Links = require('streamview-links') | |
5 … | +var explain = require('explain-error') | |
6 … | +var Notify = require('pull-notify') | |
7 … | +var Value = require('mutant/value') | |
8 … | +var watchThrottle = require('mutant/watch-throttle') | |
9 … | + | |
10 … | +exports.name = 'query' | |
11 … | +exports.version = '0.1.2' | |
12 … | +exports.manifest = { | |
13 … | + read: 'source', dump: 'source', progress: 'source' | |
14 … | +} | |
15 … | + | |
16 … | +var indexes = [ | |
17 … | + {key: 'clk', value: [['value', 'author'], ['value', 'sequence'], 'timestamp'] }, | |
18 … | + {key: 'typ', value: [['value', 'content', 'type'], 'timestamp'] }, | |
19 … | + {key: 'hsh', value: ['key', 'timestamp']}, | |
20 … | + {key: 'cha', value: [['value', 'content', 'channel'], 'timestamp'] }, | |
21 … | +// {key: 'aty', value: [['value', 'author'], ['value', 'content', 'type'], 'ts']} | |
22 … | +] | |
23 … | + | |
24 … | +//createHistoryStream( id, seq ) | |
25 … | +//[{$filter: {author: <id>, sequence: {$gt: <seq>}}}, {$map: true}] | |
26 … | + | |
27 … | +//messagesByType (type) | |
28 … | + | |
29 … | +//[{$filter: {content: {type: <type>}}}, {$map: true}] | |
30 … | + | |
31 … | +exports.init = function (ssb, config) { | |
32 … | + | |
33 … | + var dir = path.join(config.path, 'query') | |
34 … | + | |
35 … | + var version = 13 | |
36 … | + //it's really nice to tweak a few things | |
37 … | + //and then change the version number, | |
38 … | + //restart the server and have it regenerate the indexes, | |
39 … | + //all consistent again. | |
40 … | + function id (e, emit) { | |
41 … | + return emit(e) | |
42 … | + } | |
43 … | + | |
44 … | + var links = Links(dir, indexes, id, version) | |
45 … | + var notify = Notify() | |
46 … | + var pending = Value(0) | |
47 … | + | |
48 … | + watchThrottle(pending, 200, (value) => { | |
49 … | + notify({pending: Math.max(0, value)}) | |
50 … | + }) | |
51 … | + | |
52 … | + links.init(function (err, since) { | |
53 … | + countChanges(since, function (err, changes) { | |
54 … | + if (err) throw err | |
55 … | + pending.set(changes) | |
56 … | + onChange(() => { | |
57 … | + pending.set(pending() + 1) | |
58 … | + }) | |
59 … | + pull( | |
60 … | + ssb.createLogStream({gt: since || 0, live: true, sync: false}), | |
61 … | + pull.through(function () { | |
62 … | + pending.set(pending() - 1) | |
63 … | + }), | |
64 … | + links.write(function (err) { | |
65 … | + if(err) throw err | |
66 … | + }) | |
67 … | + ) | |
68 … | + }) | |
69 … | + }) | |
70 … | + | |
71 … | + return { | |
72 … | + dump: function () { | |
73 … | + return links.dump() | |
74 … | + }, | |
75 … | + | |
76 … | + read: function (opts) { | |
77 … | + if(opts && 'string' == typeof opts) | |
78 … | + try { opts = {query: JSON.parse(opts) } } catch (err) { | |
79 … | + return pull.error(err) | |
80 … | + } | |
81 … | + return links.read(opts, function (ts, cb) { | |
82 … | + ssb.sublevel('log').get(ts, function (err, key) { | |
83 … | + if(err) return cb(explain(err, 'missing timestamp:'+ts)) | |
84 … | + ssb.get(key, function (err, value) { | |
85 … | + if(err) return cb(explain(err, 'missing key:'+key)) | |
86 … | + cb(null, {key: key, value: value, timestamp: ts}) | |
87 … | + }) | |
88 … | + }) | |
89 … | + }) | |
90 … | + }, | |
91 … | + | |
92 … | + progress: notify.listen | |
93 … | + } | |
94 … | + | |
95 … | + function countChanges (since, cb) { | |
96 … | + var result = 0 | |
97 … | + pull( | |
98 … | + ssb.createLogStream({gt: since || 0, keys: false, values: false}), | |
99 … | + pull.drain(function () { | |
100 … | + result += 1 | |
101 … | + }, function (err) { | |
102 … | + cb(err, result) | |
103 … | + }) | |
104 … | + ) | |
105 … | + } | |
106 … | + | |
107 … | + function onChange (cb) { | |
108 … | + pull( | |
109 … | + ssb.createLogStream({keys: false, values: false, old: false}), | |
110 … | + pull.drain(function () { | |
111 … | + cb() | |
112 … | + }) | |
113 … | + ) | |
114 … | + } | |
115 … | +} |
lib/replicate-with-progress.js | ||
---|---|---|
@@ -1,0 +1,278 @@ | ||
1 … | +'use strict' | |
2 … | +var pull = require('pull-stream') | |
3 … | +var para = require('pull-paramap') | |
4 … | +var Notify = require('pull-notify') | |
5 … | +var Cat = require('pull-cat') | |
6 … | +var Debounce = require('observ-debounce') | |
7 … | +var mdm = require('mdmanifest') | |
8 … | +var apidoc = require('scuttlebot/lib/apidocs').replicate | |
9 … | +var MutantToPull = require('./mutant-to-pull') | |
10 … | +var {Struct, Dict} = require('mutant') | |
11 … | + | |
12 … | +var Pushable = require('pull-pushable') | |
13 … | + | |
14 … | +// compatibility function for old implementations of `latestSequence` | |
15 … | +function toSeq (s) { | |
16 … | + return typeof s === 'number' ? s : s.sequence | |
17 … | +} | |
18 … | + | |
19 … | +module.exports = { | |
20 … | + name: 'replicate', | |
21 … | + version: '2.0.0', | |
22 … | + manifest: mdm.manifest(apidoc), | |
23 … | + init: function (sbot, config) { | |
24 … | + var debounce = Debounce(200) | |
25 … | + var listeners = {} | |
26 … | + var newPeer = Notify() | |
27 … | + | |
28 … | + // keep track of sync progress and provide to client | |
29 … | + | |
30 … | + var start = null | |
31 … | + var count = 0 | |
32 … | + var rate = 0 | |
33 … | + var toSend = {} | |
34 … | + var peerHas = {} | |
35 … | + var pendingPeer = {} | |
36 … | + | |
37 … | + window.pendingPeer = pendingPeer | |
38 … | + | |
39 … | + var syncStatus = Struct({ | |
40 … | + type: 'global', | |
41 … | + incomplete: 0, | |
42 … | + pendingCount: 0, | |
43 … | + pendingPeers: Dict({}, {fixedIndexing: true}), | |
44 … | + feeds: null, | |
45 … | + rate: 0 | |
46 … | + }) | |
47 … | + | |
48 … | + window.syncStatus = syncStatus | |
49 … | + | |
50 … | + debounce(function () { | |
51 … | + var incomplete = 0 | |
52 … | + var totalPending = 0 | |
53 … | + var feeds = Object.keys(toSend).length | |
54 … | + var peers = {} | |
55 … | + | |
56 … | + Object.keys(pendingPeer).forEach(function (peerId) { | |
57 … | + if (pendingPeer[peerId]) { | |
58 … | + totalPending += 1 | |
59 … | + | |
60 … | + if (Object.keys(toSend).some(function (feedId) { | |
61 … | + if (peerHas[peerId] && peerHas[peerId][feedId]) { | |
62 … | + return peerHas[peerId][feedId] > toSend[feedId] | |
63 … | + } | |
64 … | + })) { | |
65 … | + incomplete += 1 | |
66 … | + } | |
67 … | + | |
68 … | + peers[peerId] = pendingPeer[peerId] | |
69 … | + } | |
70 … | + }) | |
71 … | + | |
72 … | + syncStatus.set({ | |
73 … | + incomplete: incomplete, | |
74 … | + feeds: syncStatus.loadedFriends ? feeds : null, | |
75 … | + pendingPeers: peers, | |
76 … | + pending: totalPending, | |
77 … | + rate: rate | |
78 … | + }, {merge: true}) | |
79 … | + }) | |
80 … | + | |
81 … | + pull( | |
82 … | + sbot.createLogStream({old: false, live: true, sync: false, keys: false}), | |
83 … | + pull.drain(function (e) { | |
84 … | + // track writes per second, mainly used for developing initial sync. | |
85 … | + if (!start) start = Date.now() | |
86 … | + var time = (Date.now() - start) / 1000 | |
87 … | + if (time >= 1) { | |
88 … | + rate = count / time | |
89 … | + start = Date.now() | |
90 … | + count = 0 | |
91 … | + } | |
92 … | + var pushable = listeners[e.author] | |
93 … | + | |
94 … | + if (pushable && pushable.sequence === e.sequence) { | |
95 … | + pushable.sequence++ | |
96 … | + pushable.forEach(function (p) { | |
97 … | + p.push(e) | |
98 … | + }) | |
99 … | + } | |
100 … | + count++ | |
101 … | + addPeer({id: e.author, sequence: e.sequence}) | |
102 … | + }) | |
103 … | + ) | |
104 … | + | |
105 … | + sbot.createHistoryStream.hook(function (fn, args) { | |
106 … | + var upto = args[0] || {} | |
107 … | + var seq = upto.sequence || upto.seq | |
108 … | + | |
109 … | + if (this._emit) this._emit('call:createHistoryStream', args[0]) | |
110 … | + | |
111 … | + // if we are calling this locally, skip cleverness | |
112 … | + if (this === sbot) return fn.call(this, upto) | |
113 … | + | |
114 … | + // keep track of each requested value, per feed / per peer. | |
115 … | + peerHas[this.id] = peerHas[this.id] || {} | |
116 … | + peerHas[this.id][upto.id] = seq - 1 | |
117 … | + | |
118 … | + debounce.set() | |
119 … | + | |
120 … | + // handle creating lots of history streams efficiently. | |
121 … | + // maybe this could be optimized in map-filter-reduce queries instead? | |
122 … | + if (toSend[upto.id] == null || (seq > toSend[upto.id])) { | |
123 … | + upto.old = false | |
124 … | + if (!upto.live) return pull.empty() | |
125 … | + var pushable = listeners[upto.id] = listeners[upto.id] || [] | |
126 … | + var p = Pushable(function () { | |
127 … | + var i = pushable.indexOf(p) | |
128 … | + pushable.splice(i, 1) | |
129 … | + }) | |
130 … | + pushable.push(p) | |
131 … | + pushable.sequence = upto.sequence | |
132 … | + return p | |
133 … | + } | |
134 … | + return fn.call(this, upto) | |
135 … | + }) | |
136 … | + | |
137 … | + // collect the IDs of feeds we want to request | |
138 … | + var opts = config.replication || {} | |
139 … | + opts.hops = opts.hops || 3 | |
140 … | + opts.dunbar = opts.dunbar || 150 | |
141 … | + opts.live = true | |
142 … | + opts.meta = true | |
143 … | + | |
144 … | + function localPeers () { | |
145 … | + if (!sbot.gossip) return | |
146 … | + sbot.gossip.peers().forEach(function (e) { | |
147 … | + if (toSend[e.key] == null) { | |
148 … | + addPeer({id: e.key, sequence: 0}) | |
149 … | + } | |
150 … | + }) | |
151 … | + } | |
152 … | + | |
153 … | + // also request local peers. | |
154 … | + if (sbot.gossip) { | |
155 … | + // if we have the gossip plugin active, then include new local peers | |
156 … | + // so that you can put a name to someone on your local network. | |
157 … | + var int = setInterval(localPeers, 1000) | |
158 … | + if (int.unref) int.unref() | |
159 … | + localPeers() | |
160 … | + } | |
161 … | + | |
162 … | + function loadedFriends () { | |
163 … | + console.log('>>>> loaded friends') | |
164 … | + syncStatus.loadedFriends = true | |
165 … | + debounce.set() | |
166 … | + } | |
167 … | + | |
168 … | + function addPeer (upto) { | |
169 … | + if (upto.sync) return loadedFriends() | |
170 … | + if (!upto.id) return console.log('invalid', upto) | |
171 … | + | |
172 … | + if (toSend[upto.id] == null) { | |
173 … | + toSend[upto.id] = Math.max(toSend[upto.id] || 0, upto.sequence || upto.seq || 0) | |
174 … | + newPeer({ id: upto.id, sequence: toSend[upto.id], type: 'new' }) | |
175 … | + debounce.set() | |
176 … | + } else { | |
177 … | + toSend[upto.id] = Math.max(toSend[upto.id] || 0, upto.sequence || upto.seq || 0) | |
178 … | + } | |
179 … | + | |
180 … | + debounce.set() | |
181 … | + } | |
182 … | + | |
183 … | + // create read-streams for the desired feeds | |
184 … | + pull( | |
185 … | + sbot.friends.createFriendStream(opts), | |
186 … | + // filter out duplicates, and also keep track of what we expect to receive | |
187 … | + // lookup the latest sequence from each user | |
188 … | + para(function (data, cb) { | |
189 … | + if (data.sync) return cb(null, data) | |
190 … | + var id = data.id || data | |
191 … | + sbot.latestSequence(id, function (err, seq) { | |
192 … | + cb(null, { | |
193 … | + id: id, sequence: err ? 0 : toSeq(seq) | |
194 … | + }) | |
195 … | + }) | |
196 … | + }, 32), | |
197 … | + pull.drain(addPeer, loadedFriends) | |
198 … | + ) | |
199 … | + | |
200 … | + function upto (opts) { | |
201 … | + opts = opts || {} | |
202 … | + var ary = Object.keys(toSend).map(function (k) { | |
203 … | + return { id: k, sequence: toSend[k] } | |
204 … | + }) | |
205 … | + if (opts.live) { | |
206 … | + return Cat([pull.values(ary), pull.once({sync: true}), newPeer.listen()]) | |
207 … | + } | |
208 … | + | |
209 … | + return pull.values(ary) | |
210 … | + } | |
211 … | + | |
212 … | + sbot.on('rpc:connect', function (rpc) { | |
213 … | + // this is the cli client, just ignore. | |
214 … | + if (rpc.id === sbot.id) return | |
215 … | + // check for local peers, or manual connections. | |
216 … | + localPeers() | |
217 … | + sbot.emit('replicate:start', rpc) | |
218 … | + rpc.on('closed', function () { | |
219 … | + sbot.emit('replicate:finish', toSend) | |
220 … | + }) | |
221 … | + pull( | |
222 … | + upto({live: opts.live}), | |
223 … | + pull.drain(function (upto) { | |
224 … | + if (upto.sync) return | |
225 … | + var last = (upto.sequence || upto.seq || 0) | |
226 … | + pendingPeer[rpc.id] = (pendingPeer[rpc.id] || 0) + 1 | |
227 … | + debounce.set() | |
228 … | + | |
229 … | + pull( | |
230 … | + rpc.createHistoryStream({ | |
231 … | + id: upto.id, | |
232 … | + seq: last + 1, | |
233 … | + live: false, | |
234 … | + keys: false | |
235 … | + }), | |
236 … | + pull.through((msg) => { | |
237 … | + start = Math.max(start, msg.sequence) | |
238 … | + }), | |
239 … | + sbot.createWriteStream(function () { | |
240 … | + // TODO: do something with the error | |
241 … | + // this seems to be thrown fairly regularly whenever something weird happens to the stream | |
242 … | + | |
243 … | + pendingPeer[rpc.id] -= 1 | |
244 … | + debounce.set() | |
245 … | + | |
246 … | + // all synched, now lets keep watching for live changes | |
247 … | + // need to handle this separately because there is no {sync: true} event with HistoryStream | |
248 … | + // and we want to notify the client that sync has completed | |
249 … | + | |
250 … | + pull( | |
251 … | + rpc.createHistoryStream({ | |
252 … | + id: upto.id, | |
253 … | + seq: last + 1, | |
254 … | + live: true, | |
255 … | + keys: false | |
256 … | + }), | |
257 … | + sbot.createWriteStream(function () { | |
258 … | + // TODO: handle error | |
259 … | + }) | |
260 … | + ) | |
261 … | + }) | |
262 … | + ) | |
263 … | + }, function (err) { | |
264 … | + if (err) { | |
265 … | + sbot.emit('log:error', ['replication', rpc.id, 'error', err]) | |
266 … | + } | |
267 … | + }) | |
268 … | + ) | |
269 … | + }) | |
270 … | + | |
271 … | + return { | |
272 … | + changes: function () { | |
273 … | + return MutantToPull(syncStatus) | |
274 … | + }, | |
275 … | + upto: upto | |
276 … | + } | |
277 … | + } | |
278 … | +} |
main-window.js | ||
---|---|---|
@@ -24,9 +24,10 @@ | ||
24 | 24 … | 'keys.sync.id': 'first', |
25 | 25 … | 'blob.sync.url': 'first', |
26 | 26 … | 'page.html.render': 'first', |
27 | 27 … | 'app.html.search': 'first', |
28 | - 'app.views': 'first' | |
28 … | + 'app.views': 'first', | |
29 … | + 'app.html.progressNotifier': 'first' | |
29 | 30 … | })) |
30 | 31 … | |
31 | 32 … | var id = api.keys.sync.id() |
32 | 33 … | var latestUpdate = LatestUpdate() |
@@ -66,8 +67,9 @@ | ||
66 | 67 … | h('strong', ['Patchwork ', latestUpdate, ' has been released.']), ' Click here for more info!' |
67 | 68 … | ]) |
68 | 69 … | ]) |
69 | 70 … | ), |
71 … | + api.app.html.progressNotifier(), | |
70 | 72 … | views.html |
71 | 73 … | ]) |
72 | 74 … | |
73 | 75 … | catchLinks(container, (href, external) => { |
modules/app/html/progress-notifier.js | ||
---|---|---|
@@ -1,0 +1,65 @@ | ||
1 … | +var {computed, when, h, throttle} = require('mutant') | |
2 … | +var nest = require('depnest') | |
3 … | + | |
4 … | +exports.gives = nest('app.html.progressNotifier') | |
5 … | + | |
6 … | +exports.needs = nest({ | |
7 … | + 'progress.html.render': 'first', | |
8 … | + 'progress.obs.global': 'first', | |
9 … | + 'progress.obs.query': 'first' | |
10 … | +}) | |
11 … | + | |
12 … | +exports.create = function (api) { | |
13 … | + return nest('app.html.progressNotifier', function (id) { | |
14 … | + var progress = api.progress.obs.global() | |
15 … | + var queryProgress = api.progress.obs.query() | |
16 … | + | |
17 … | + var maxQueryPending = 0 | |
18 … | + | |
19 … | + var indexProgress = computed([queryProgress.pending], (pending) => { | |
20 … | + if (pending === 0 || pending > maxQueryPending) { | |
21 … | + maxQueryPending = pending | |
22 … | + } | |
23 … | + if (pending === 0) { | |
24 … | + return 1 | |
25 … | + } else { | |
26 … | + return (maxQueryPending - pending) / maxQueryPending | |
27 … | + } | |
28 … | + }) | |
29 … | + | |
30 … | + var downloadProgress = computed([progress.feeds, progress.incomplete], (feeds, incomplete) => { | |
31 … | + if (feeds) { | |
32 … | + return clamp((incomplete - feeds) / feeds) | |
33 … | + } else { | |
34 … | + return 1 | |
35 … | + } | |
36 … | + }) | |
37 … | + | |
38 … | + var hidden = computed([progress.incomplete, progress.feeds, queryProgress.pending], (incomplete, feeds, indexing) => { | |
39 … | + return incomplete <= 5 && !indexing && feeds | |
40 … | + }) | |
41 … | + | |
42 … | + var hasDownloadProgress = computed([progress.feeds, progress.incomplete], (feeds, incomplete) => { | |
43 … | + if (feeds) { | |
44 … | + return incomplete > 5 | |
45 … | + } | |
46 … | + }) | |
47 … | + | |
48 … | + return h('div.info', { hidden: throttle(hidden, 5000) }, [ | |
49 … | + h('div.status', [ | |
50 … | + h('Loading -small', [ | |
51 … | + when(hasDownloadProgress, | |
52 … | + ['Downloading new messages', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: downloadProgress })], | |
53 … | + when(queryProgress.pending, [ | |
54 … | + ['Indexing database', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: indexProgress })] | |
55 … | + ], 'Checking for changes...') | |
56 … | + ) | |
57 … | + ]) | |
58 … | + ]) | |
59 … | + ]) | |
60 … | + }) | |
61 … | +} | |
62 … | + | |
63 … | +function clamp (value) { | |
64 … | + return Math.min(1, Math.max(0, value)) || 0 | |
65 … | +} |
modules/feed/html/rollup.js | ||
---|---|---|
@@ -75,9 +75,9 @@ | ||
75 | 75 … | pull( |
76 | 76 … | getStream({old: false}), |
77 | 77 … | pull.drain((item) => { |
78 | 78 … | var type = item && item.value && item.value.content.type |
79 | - if (type && type !== 'vote') { | |
79 … | + if (type && type !== 'vote' && typeof item.value.content === 'object') { | |
80 | 80 … | if (item.value && item.value.author === api.keys.sync.id() && !updates() && type !== 'git-update') { |
81 | 81 … | return refresh() |
82 | 82 … | } |
83 | 83 … | if (filter) { |
modules/page/html/render/public.js | ||
---|---|---|
@@ -22,8 +22,9 @@ | ||
22 | 22 … | 'about.obs.name': 'first', |
23 | 23 … | 'invite.sheet': 'first', |
24 | 24 … | |
25 | 25 … | 'message.html.compose': 'first', |
26 … | + 'progress.html.peer': 'first', | |
26 | 27 … | |
27 | 28 … | 'feed.html.rollup': 'first', |
28 | 29 … | 'profile.obs.recentlyUpdated': 'first', |
29 | 30 … | 'contact.obs.following': 'first', |
@@ -194,9 +195,12 @@ | ||
194 | 195 … | href: id |
195 | 196 … | }, [ |
196 | 197 … | h('div.avatar', [api.about.html.image(id)]), |
197 | 198 … | h('div.main', [ |
198 | - h('div.name', [ '@', api.about.obs.name(id) ]) | |
199 … | + h('div.name', [ api.about.obs.name(id) ]) | |
200 … | + ]), | |
201 … | + h('div.progress', [ | |
202 … | + api.progress.html.peer(id) | |
199 | 203 … | ]) |
200 | 204 … | ]) |
201 | 205 … | }) |
202 | 206 … | ]) |
modules/progress/html/peer.js | ||
---|---|---|
@@ -1,0 +1,23 @@ | ||
1 … | +var {computed, when} = require('mutant') | |
2 … | +var nest = require('depnest') | |
3 … | + | |
4 … | +exports.gives = nest('progress.html.peer') | |
5 … | + | |
6 … | +exports.needs = nest({ | |
7 … | + 'progress.html.render': 'first', | |
8 … | + 'progress.obs.peer': 'first', | |
9 … | + 'progress.obs.global': 'first' | |
10 … | +}) | |
11 … | + | |
12 … | +exports.create = function (api) { | |
13 … | + return nest('progress.html.peer', function (id) { | |
14 … | + var progress = api.progress.obs.peer(id) | |
15 … | + var feeds = api.progress.obs.global().feeds | |
16 … | + | |
17 … | + var value = computed([progress, feeds], (pending, feeds) => { | |
18 … | + return (feeds - pending) / feeds | |
19 … | + }) | |
20 … | + | |
21 … | + return when(progress, api.progress.html.render(value)) | |
22 … | + }) | |
23 … | +} |
modules/progress/html/render.js | ||
---|---|---|
@@ -1,0 +1,30 @@ | ||
1 … | +var svg = require('mutant/svg-element') | |
2 … | +var computed = require('mutant/computed') | |
3 … | +var nest = require('depnest') | |
4 … | + | |
5 … | +exports.gives = nest('progress.html.render') | |
6 … | + | |
7 … | +exports.create = function (api) { | |
8 … | + return nest('progress.html.render', function (pos) { | |
9 … | + return svg('svg RadialProgress', { | |
10 … | + viewBox: '-20 -20 240 240' | |
11 … | + }, [ | |
12 … | + svg('path', { | |
13 … | + d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200,0', | |
14 … | + 'stroke': '#DADADA', | |
15 … | + 'fill': 'none' | |
16 … | + }), | |
17 … | + svg('path', { | |
18 … | + d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200,0', | |
19 … | + 'stroke-dashoffset': computed(pos, (pos) => { | |
20 … | + pos = Math.min(Math.max(pos, 0), 1) | |
21 … | + return (1 - pos) * 629 | |
22 … | + }), | |
23 … | + 'stroke-width': 40, | |
24 … | + 'stroke-dasharray': 629, | |
25 … | + 'stroke': '#33DA33', | |
26 … | + 'fill': 'none' | |
27 … | + }) | |
28 … | + ]) | |
29 … | + }) | |
30 … | +} |
modules/progress/obs.js | ||
---|---|---|
@@ -1,0 +1,88 @@ | ||
1 … | +var nest = require('depnest') | |
2 … | +var pull = require('pull-stream') | |
3 … | +var {Struct, Dict, computed} = require('mutant') | |
4 … | + | |
5 … | +exports.gives = nest({ | |
6 … | + 'progress.obs': ['global', 'peer', 'query'] | |
7 … | +}) | |
8 … | + | |
9 … | +exports.needs = nest({ | |
10 … | + 'sbot.pull.replicateProgress': 'first', | |
11 … | + 'sbot.pull.queryProgress': 'first' | |
12 … | +}) | |
13 … | + | |
14 … | +exports.create = function (api) { | |
15 … | + var syncStatus = null | |
16 … | + var queryProgress = null | |
17 … | + | |
18 … | + return nest({ | |
19 … | + 'progress.obs': {global, peer, query} | |
20 … | + }) | |
21 … | + | |
22 … | + function global () { | |
23 … | + load() | |
24 … | + return syncStatus | |
25 … | + } | |
26 … | + | |
27 … | + function peer (id) { | |
28 … | + load() | |
29 … | + var result = computed(syncStatus, (status) => { | |
30 … | + return status.pendingPeers[id] || 0 | |
31 … | + }) | |
32 … | + return result | |
33 … | + } | |
34 … | + | |
35 … | + function query () { | |
36 … | + if (!queryProgress) { | |
37 … | + queryProgress = Struct({ | |
38 … | + pending: 0 | |
39 … | + }) | |
40 … | + | |
41 … | + pull( | |
42 … | + api.sbot.pull.queryProgress(), | |
43 … | + pull.drain((event) => { | |
44 … | + queryProgress.set(event) | |
45 … | + }) | |
46 … | + ) | |
47 … | + } | |
48 … | + return queryProgress | |
49 … | + } | |
50 … | + | |
51 … | + function load () { | |
52 … | + if (!syncStatus) { | |
53 … | + syncStatus = Struct({ | |
54 … | + incomplete: 0, | |
55 … | + pendingCount: 0, | |
56 … | + pendingPeers: Dict({}, {fixedIndexing: true}), | |
57 … | + feeds: null, | |
58 … | + rate: 0 | |
59 … | + }) | |
60 … | + | |
61 … | + pull( | |
62 … | + api.sbot.pull.replicateProgress(), | |
63 … | + pull.drain((event) => { | |
64 … | + if (!event.sync) { | |
65 … | + syncStatus.set(event) | |
66 … | + } | |
67 … | + }) | |
68 … | + ) | |
69 … | + } | |
70 … | + } | |
71 … | +} | |
72 … | + | |
73 … | +function Peer (id) { | |
74 … | + return Struct({ | |
75 … | + type: 'peer', | |
76 … | + id: id, | |
77 … | + pending: 0 | |
78 … | + }) | |
79 … | +} | |
80 … | + | |
81 … | +function Feed (id) { | |
82 … | + return Struct({ | |
83 … | + type: 'feed', | |
84 … | + id: id, | |
85 … | + available: 0, | |
86 … | + local: 0 | |
87 … | + }) | |
88 … | +} |
package.json | ||
---|---|---|
@@ -29,14 +29,14 @@ | ||
29 | 29 … | "is-visible": "^2.1.1", |
30 | 30 … | "level": "~1.4.0", |
31 | 31 … | "level-memview": "0.0.0", |
32 | 32 … | "micro-css": "^2.0.0", |
33 | - "mutant": "^3.15.2", | |
33 … | + "mutant": "^3.17.0", | |
34 | 34 … | "mutant-pull-reduce": "^1.0.1", |
35 | 35 … | "non-private-ip": "^1.4.1", |
36 | 36 … | "on-change-network": "0.0.2", |
37 | 37 … | "on-wakeup": "^1.0.1", |
38 | - "patchcore": "github:ssbc/patchcore", | |
38 … | + "patchcore": "~0.3.1", | |
39 | 39 … | "prebuild": "github:mmckegg/prebuild#use-npm-conf", |
40 | 40 … | "pull-abortable": "^4.1.0", |
41 | 41 … | "pull-file": "~1.0.0", |
42 | 42 … | "pull-identify-filetype": "^1.1.0", |
@@ -50,11 +50,9 @@ | ||
50 | 50 … | "scuttlebot": "^9.4.4", |
51 | 51 … | "sorted-array-functions": "~1.0.0", |
52 | 52 … | "ssb-avatar": "^0.2.0", |
53 | 53 … | "ssb-blobs": "~0.1.7", |
54 | - "ssb-fulltext": "^1.0.1", | |
55 | 54 … | "ssb-keys": "~7.0.0", |
56 | - "ssb-links": "~2.0.0", | |
57 | 55 … | "ssb-mentions": "^0.1.1", |
58 | 56 … | "ssb-msgs": "^5.2.0", |
59 | 57 … | "ssb-query": "~0.1.1", |
60 | 58 … | "ssb-ref": "~2.6.2", |
server-process.js | ||
---|---|---|
@@ -5,18 +5,17 @@ | ||
5 | 5 … | |
6 | 6 … | var createSbot = require('scuttlebot') |
7 | 7 … | .use(require('scuttlebot/plugins/master')) |
8 | 8 … | .use(require('scuttlebot/plugins/gossip')) |
9 | - .use(require('scuttlebot/plugins/friends')) | |
10 | - .use(require('scuttlebot/plugins/replicate')) | |
9 … | + .use(require('./lib/friends-with-sync')) | |
10 … | + .use(require('./lib/replicate-with-progress')) | |
11 | 11 … | .use(require('ssb-blobs')) |
12 | 12 … | .use(require('scuttlebot/plugins/invite')) |
13 | 13 … | .use(require('scuttlebot/plugins/block')) |
14 | 14 … | .use(require('scuttlebot/plugins/local')) |
15 | 15 … | .use(require('scuttlebot/plugins/logging')) |
16 | 16 … | .use(require('scuttlebot/plugins/private')) |
17 | - .use(require('ssb-links')) | |
18 | - .use(require('ssb-query')) | |
17 … | + .use(require('./lib/query-with-progress')) | |
19 | 18 … | //.use(require('ssb-fulltext')) // disabled for now |
20 | 19 … | |
21 | 20 … | module.exports = function (ssbConfig) { |
22 | 21 … | var context = { |
styles/loading.mcss | ||
---|---|---|
@@ -16,8 +16,15 @@ | ||
16 | 16 … | width: 16px |
17 | 17 … | } |
18 | 18 … | } |
19 | 19 … | |
20 … | + -small { | |
21 … | + ::before { | |
22 … | + height: 30px | |
23 … | + width: 30px | |
24 … | + } | |
25 … | + } | |
26 … | + | |
20 | 27 … | -large { |
21 | 28 … | ::before { |
22 | 29 … | height: 100px |
23 | 30 … | width: 100px |
styles/main-window.mcss | ||
---|---|---|
@@ -109,10 +109,29 @@ | ||
109 | 109 … | text-decoration: none |
110 | 110 … | background: #c0ffae |
111 | 111 … | } |
112 | 112 … | } |
113 | - box-shadow: 0 0 3px #618b63 | |
113 … | + | |
114 … | + div.status { | |
115 … | + padding: 5px | |
116 … | + background: #7c7c7c | |
117 … | + color: white | |
118 … | + (svg) { | |
119 … | + width: 20px | |
120 … | + height: 20px | |
121 … | + } | |
122 … | + } | |
123 … | + | |
124 … | + [hidden] { | |
125 … | + display: block | |
126 … | + max-height: 0 | |
127 … | + animation: none | |
128 … | + } | |
129 … | + | |
130 … | + max-height: 100px | |
131 … | + box-shadow: 0 0 3px #616161 | |
114 | 132 … | overflow: hidden |
133 … | + transition: 0.5s max-height | |
115 | 134 … | animation: 0.5s slide-in |
116 | 135 … | position: relative |
117 | 136 … | z-index: 1 |
118 | 137 … | } |
styles/profile-list.mcss | ||
---|---|---|
@@ -47,6 +47,15 @@ | ||
47 | 47 … | color: #636363 |
48 | 48 … | -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,1) 90%, rgba(0,0,0,0)) |
49 | 49 … | } |
50 | 50 … | } |
51 … | + | |
52 … | + div.progress { | |
53 … | + display: flex | |
54 … | + flex-direction: column | |
55 … | + svg { | |
56 … | + width: 20px | |
57 … | + flex: 1 | |
58 … | + } | |
59 … | + } | |
51 | 60 … | } |
52 | 61 … | } |
Built with git-ssb-web