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