git ssb

0+

cel / ssb-issues



Tree: eb15fe70a933fb5906d227f9b4d88d570c179867

Files: eb15fe70a933fb5906d227f9b4d88d570c179867 / index.js

8128 bytesRaw
1var pull = require('pull-stream')
2var paramap = require('pull-paramap')
3var asyncMemo = require('asyncmemo')
4var issueSchemas = require('./lib/schemas')
5var multicb = require('multicb')
6
7function Cache(fn, ssb) {
8 return asyncMemo(fn)
9 return function (key, cb) { ac.get(key, cb) }
10}
11
12function truncate(str, len) {
13 return str.length > len ? str.substr(0, len) + '...' : str
14}
15
16exports.name = 'issues'
17
18exports.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
29exports.schemas = issueSchemas
30
31function isStatusChanged(msg, issue) {
32 var mention = getMention(msg, issue)
33 return mention ? mention.open : null
34}
35
36function 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
55function 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
64function passthrough(read) {
65 return read
66}
67
68function 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
83function 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
93exports.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