Files: bcf23f7ed7e27cc8755003023c9fd57775f79ca9 / contact / obs.js
3584 bytesRaw
1 | var nest = require('depnest') |
2 | var { Value, computed } = require('mutant') |
3 | var pull = require('pull-stream') |
4 | var ref = require('ssb-ref') |
5 | |
6 | exports.needs = nest({ |
7 | 'sbot.pull.stream': 'first', |
8 | }) |
9 | |
10 | exports.gives = nest({ |
11 | 'contact.obs': ['following', 'followers', 'blocking', 'blockers'], |
12 | 'sbot.hook.publish': true |
13 | }) |
14 | |
15 | exports.create = function (api) { |
16 | var cacheLoading = false |
17 | var cache = {} |
18 | var reverseCache = {} |
19 | |
20 | var sync = Value(false) |
21 | |
22 | return nest({ |
23 | 'contact.obs': { |
24 | |
25 | // states: |
26 | // true = following, |
27 | // null = neutral (may have unfollowed), |
28 | // false = blocking |
29 | |
30 | following: (key) => matchingValueKeys(get(key, cache), true), |
31 | followers: (key) => matchingValueKeys(get(key, reverseCache), true), |
32 | blocking: (key) => matchingValueKeys(get(key, cache), false), |
33 | blockers: (key) => matchingValueKeys(get(key, reverseCache), false) |
34 | }, |
35 | 'sbot.hook.publish': function (msg) { |
36 | if (!isContact(msg)) return |
37 | |
38 | // HACK: make interface more responsive when sbot is busy |
39 | var source = msg.value.author |
40 | var dest = msg.value.content.contact |
41 | var tristate = ( // from ssb-friends |
42 | msg.value.content.following ? true |
43 | : msg.value.content.flagged || msg.value.content.blocking ? false |
44 | : null |
45 | ) |
46 | |
47 | update(source, { [dest]: tristate }, cache) |
48 | update(dest, { [source]: tristate }, reverseCache) |
49 | } |
50 | }) |
51 | |
52 | function matchingValueKeys (state, value) { |
53 | var obs = computed(state, state => { |
54 | return Object.keys(state).filter(key => { |
55 | return state[key] === value |
56 | }) |
57 | }) |
58 | |
59 | obs.sync = sync |
60 | return obs |
61 | } |
62 | |
63 | function loadCache () { |
64 | pull( |
65 | api.sbot.pull.stream(sbot => sbot.friends.stream({live: true})), |
66 | pull.drain(item => { |
67 | if (!sync()) { |
68 | // populate observable cache |
69 | var reverse = {} |
70 | for (var source in item) { |
71 | if (ref.isFeed(source)) { |
72 | update(source, item[source], cache) |
73 | |
74 | // generate reverse lookup |
75 | for (var dest in item[source]) { |
76 | reverse[dest] = reverse[dest] || {} |
77 | reverse[dest][source] = item[source][dest] |
78 | } |
79 | } |
80 | } |
81 | |
82 | // populate reverse observable cache |
83 | for (var dest in reverse) { |
84 | update(dest, reverse[dest], reverseCache) |
85 | } |
86 | |
87 | sync.set(true) |
88 | } else if (item && ref.isFeed(item.from) && ref.isFeed(item.to)) { |
89 | // handle realtime updates |
90 | update(item.from, {[item.to]: item.value}, cache) |
91 | update(item.to, {[item.from]: item.value}, reverseCache) |
92 | } |
93 | }) |
94 | ) |
95 | } |
96 | |
97 | function update (sourceId, values, lookup) { |
98 | // ssb-friends: values = { |
99 | // keyA: true|null|false (friend, neutral, block) |
100 | // keyB: true|null|false (friend, neutral, block) |
101 | // } |
102 | var state = get(sourceId, lookup) |
103 | var lastState = state() |
104 | var changed = false |
105 | |
106 | for (var targetId in values) { |
107 | if (values[targetId] !== lastState[targetId]) { |
108 | lastState[targetId] = values[targetId] |
109 | changed = true |
110 | } |
111 | } |
112 | |
113 | if (changed) { |
114 | state.set(lastState) |
115 | } |
116 | } |
117 | |
118 | function get (id, lookup) { |
119 | if (!ref.isFeed(id)) throw new Error('Contact state requires an id!') |
120 | if (!cacheLoading) { |
121 | cacheLoading = true |
122 | loadCache() |
123 | } |
124 | if (!lookup[id]) { |
125 | lookup[id] = Value({}) |
126 | } |
127 | return lookup[id] |
128 | } |
129 | } |
130 | |
131 | function isContact (msg) { |
132 | return msg.value && msg.value.content && msg.value.content.type === 'contact' |
133 | } |
134 |
Built with git-ssb-web