Files: eb15fe70a933fb5906d227f9b4d88d570c179867 / index.js
8128 bytesRaw
1 | var pull = require('pull-stream') |
2 | var paramap = require('pull-paramap') |
3 | var asyncMemo = require('asyncmemo') |
4 | var issueSchemas = require('./lib/schemas') |
5 | var multicb = require('multicb') |
6 | |
7 | function Cache(fn, ssb) { |
8 | return asyncMemo(fn) |
9 | return function (key, cb) { ac.get(key, cb) } |
10 | } |
11 | |
12 | function truncate(str, len) { |
13 | return str.length > len ? str.substr(0, len) + '...' : str |
14 | } |
15 | |
16 | exports.name = 'issues' |
17 | |
18 | exports.manifest = { |
19 | get: 'async', |
20 | list: 'source', |
21 | new: 'async', |
22 | edit: 'async', |
23 | close: 'async', |
24 | reopen: 'async', |
25 | getMention: 'sync', |
26 | isStatusChanged: 'sync' |
27 | } |
28 | |
29 | exports.schemas = issueSchemas |
30 | |
31 | function isStatusChanged(msg, issue) { |
32 | var mention = getMention(msg, issue) |
33 | return mention ? mention.open : null |
34 | } |
35 | |
36 | function getMention(msg, issue) { |
37 | var c = msg.value.content |
38 | if (msg.key == issue.id || c.issue == issue.id || c.link == issue.id) |
39 | if (c.open != null) |
40 | return c |
41 | if (c.issues) { |
42 | var mention |
43 | for (var i = 0; i < c.issues.length; i++) { |
44 | mention = getMention({value: { |
45 | timestamp: msg.value.timestamp, |
46 | author: msg.value.author, |
47 | content: c.issues[i] |
48 | }}, issue) |
49 | if (mention) |
50 | return mention |
51 | } |
52 | } |
53 | } |
54 | |
55 | function isMsgIdRange(opts) { |
56 | return ( |
57 | (opts.gt && opts.gt[0] === '%') || |
58 | (opts.lt && opts.lt[0] === '%') || |
59 | (opts.gte && opts.gte[0] === '%') || |
60 | (opts.lte && opts.lte[0] === '%') |
61 | ) |
62 | } |
63 | |
64 | function passthrough(read) { |
65 | return read |
66 | } |
67 | |
68 | function optsToRangeFilter(opts) { |
69 | if (!opts || ( |
70 | opts.gt == null || |
71 | opts.lt == null || |
72 | opts.gte == null || |
73 | opts.lte == null |
74 | )) return passthrough |
75 | return pull.filter(function (msg) { |
76 | return (opts.gt == null || msg.timestamp > opts.gt) |
77 | && (opts.lt == null || msg.timestamp < opts.lt) |
78 | && (opts.gte == null || msg.timestamp >= opts.gte) |
79 | && (opts.lte == null || msg.timestamp <= opts.lte) |
80 | }) |
81 | } |
82 | |
83 | function optsToRange(opts) { |
84 | var range = {} |
85 | var defined = false |
86 | if (opts.gt != null) defined = true, range.$gt = opts.gt |
87 | if (opts.lt != null) defined = true, range.$lt = opts.lt |
88 | if (opts.gte != null) defined = true, range.$gte = opts.gt |
89 | if (opts.lte != null) defined = true, range.$lte = opts.lt |
90 | return defined ? range : undefined |
91 | } |
92 | |
93 | exports.init = function (ssb) { |
94 | |
95 | var ssbGet = asyncMemo(ssb.get) |
96 | var liveStreams = [] |
97 | |
98 | var getIssue = asyncMemo(function (id, cb) { |
99 | var issue = {} |
100 | var issueMsg |
101 | |
102 | ssbGet(id, function (err, msg) { |
103 | msg = {key: id, value: msg} |
104 | if (err) return cb(err) |
105 | issueMsg = msg |
106 | issue.id = msg.key |
107 | issue.msg = msg |
108 | issue.author = msg.value.author |
109 | var c = msg.value.content |
110 | issue.project = c.project |
111 | issue.text = c.text || c.title || JSON.stringify(msg, null, 2) |
112 | issue.created_at = issue.updated_at = msg.value.timestamp |
113 | if (c.project) |
114 | ssbGet(c.project, gotProjectMsg) |
115 | else |
116 | getLinks() |
117 | }) |
118 | |
119 | function gotProjectMsg(err, msg) { |
120 | if (err) return cb(err) |
121 | issue.projectAuthor = msg.author |
122 | getLinks() |
123 | } |
124 | |
125 | function getLinks() { |
126 | var now = Date.now() |
127 | // compute the result from the past data |
128 | pull( |
129 | ssb.links({dest: id, reverse: true, values: true, |
130 | old: true, live: false, sync: false}), |
131 | pull.drain(onOldMsg, onOldEnd) |
132 | ) |
133 | // keep the results up-to-date in the future |
134 | var read = ssb.links({dest: id, values: true, |
135 | old: false, live: true, sync: false}) |
136 | liveStreams.push(read) |
137 | pull( |
138 | read, |
139 | pull.drain(onNewMsg, onNewEnd) |
140 | ) |
141 | } |
142 | |
143 | function onOldMsg(msg) { |
144 | if (!msg.value) |
145 | return |
146 | var c = msg.value.content |
147 | |
148 | // handle updates to issue |
149 | if (msg.key == id || c.issue == id || c.link == id) { |
150 | if (c.open != null && issue.open == null) |
151 | issue.open = c.open |
152 | if (c.title != null && issue.title == null) |
153 | issue.title = c.title |
154 | if (msg.value.timestamp > issue.updated_at) |
155 | issue.updated_at = msg.value.timestamp |
156 | } |
157 | |
158 | // handle updates via mention |
159 | if (c.issues) { |
160 | for (var i = 0; i < c.issues.length; i++) |
161 | onOldMsg({value: { |
162 | timestamp: msg.value.timestamp, |
163 | author: msg.value.author, |
164 | content: c.issues[i] |
165 | }}) |
166 | } |
167 | |
168 | checkReady() |
169 | } |
170 | |
171 | function onNewMsg(msg) { |
172 | if (!msg.value) |
173 | return |
174 | var c = msg.value.content |
175 | |
176 | // handle updates to issue |
177 | if (msg.key == id || c.issue == id || c.link == id) { |
178 | if (c.open != null) |
179 | issue.open = c.open |
180 | if (c.title != null) |
181 | issue.title = c.title |
182 | if (msg.value.timestamp > issue.updated_at) |
183 | issue.updated_at = msg.value.timestamp |
184 | } |
185 | |
186 | // handle updates via mention |
187 | if (c.issues) { |
188 | for (var i = 0; i < c.issues.length; i++) |
189 | onNewMsg({value: { |
190 | timestamp: msg.value.timestamp, |
191 | author: msg.value.author, |
192 | content: c.issues[i] |
193 | }}) |
194 | } |
195 | } |
196 | |
197 | function checkReady() { |
198 | // call back once all the issue properties are set |
199 | if (issue.open != null && issue.title != null) { |
200 | var _cb = cb |
201 | delete cb |
202 | _cb(null, issue) |
203 | } |
204 | } |
205 | |
206 | function onOldEnd(err) { |
207 | if (err) { |
208 | if (cb) cb(err) |
209 | else console.error(err) |
210 | return |
211 | } |
212 | // process the root message last |
213 | onOldMsg(issueMsg) |
214 | // if callback hasn't been called yet, the issue is missing a field |
215 | if (cb) { |
216 | if (issue.open == null) |
217 | issue.open = true |
218 | if (issue.title == null) |
219 | issue.title = truncate(issue.text.split('\n')[0], 250) || issue.id |
220 | checkReady() |
221 | } |
222 | } |
223 | |
224 | function onNewEnd(err) { |
225 | if (err) { |
226 | if (cb) cb(err) |
227 | else console.error(err) |
228 | } |
229 | } |
230 | }) |
231 | |
232 | function deinit(cb) { |
233 | var done = multicb() |
234 | // cancel all live streams |
235 | liveStreams.forEach(function (read) { |
236 | read(true, done()) |
237 | }) |
238 | done(cb) |
239 | } |
240 | |
241 | function listIssues(opts) { |
242 | opts.type = 'issue' |
243 | return pull( |
244 | opts.project && !isMsgIdRange(opts) ? ( |
245 | ssb.backlinks ? ssb.backlinks.read({ |
246 | reverse: opts.reverse, |
247 | live: opts.live, |
248 | query: [{$filter: { |
249 | value: {content: {type: opts.type}}, |
250 | dest: opts.project, |
251 | rts: optsToRange(opts) |
252 | }}] |
253 | }) : pull( |
254 | ssb.links({ |
255 | reverse: opts.reverse, |
256 | live: opts.live, |
257 | dest: opts.project, |
258 | values: true, |
259 | rel: 'project' |
260 | }), |
261 | pull.filter(function (msg) { |
262 | return msg.value.content.type === opts.type |
263 | }), |
264 | optsToRangeFilter(opts) |
265 | ) |
266 | ) : ssb.messagesByType(opts), |
267 | pull.unique('key'), |
268 | pull.filter(function (msg) { |
269 | return (!opts.project || opts.project == msg.value.content.project) |
270 | && (!opts.author || opts.author == msg.value.author) |
271 | }), |
272 | paramap(function (msg, cb) { |
273 | getIssue(msg.key, cb) |
274 | }, 8), |
275 | pull.filter(opts.open != null && function (pr) { |
276 | return pr.open == opts.open |
277 | }) |
278 | ) |
279 | } |
280 | |
281 | function editIssue(id, opts, cb) { |
282 | var msg |
283 | try { ssb.publish(issueSchemas.edit(id, opts), cb) } |
284 | catch(e) { return cb(e) } |
285 | } |
286 | |
287 | function closeIssue(id, cb) { |
288 | var msg |
289 | try { ssb.publish(issueSchemas.close(id), cb) } |
290 | catch(e) { return cb(e) } |
291 | } |
292 | |
293 | function reopenIssue(id, cb) { |
294 | var msg |
295 | try { msg = issueSchemas.reopen(id) } |
296 | catch(e) { return cb(e) } |
297 | ssb.publish(msg, cb) |
298 | } |
299 | |
300 | function newIssue(opts, cb) { |
301 | var msg |
302 | try { msg = issueSchemas.new(opts.project, opts.title, opts.text) } |
303 | catch(e) { return cb(e) } |
304 | ssb.publish(msg, function (err, msg) { |
305 | if (err) return cb(err) |
306 | getIssue(msg.key, cb) |
307 | }) |
308 | } |
309 | |
310 | return { |
311 | deinit: deinit, |
312 | get: getIssue, |
313 | list: listIssues, |
314 | new: newIssue, |
315 | edit: editIssue, |
316 | close: closeIssue, |
317 | reopen: reopenIssue, |
318 | getMention: getMention, |
319 | isStatusChanged: isStatusChanged |
320 | } |
321 | } |
322 |
Built with git-ssb-web