-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
store.js
738 lines (646 loc) · 26.4 KB
/
store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
// SPDX-FileCopyrightText: 2023 the cable-core.js authors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// node core dependencies
const EventEmitter = require('events').EventEmitter
// external database dependencies
const { MemoryLevel } = require("memory-level")
// external dependencies
const storedebug = require("debug")("core:store")
const b4a = require("b4a")
// internal dependencies
const cable = require("cable.js")
const crypto = require("cable.js/cryptography.js")
const constants = require("cable.js/constants.js")
const util = require("./util.js")
// materialized views and indices
const createDatastore = require("./views/data-store.js")
const createChannelStateView = require("./views/channel-state.js")
const createChannelMembershipView = require("./views/channel-membership.js")
const createTopicView = require("./views/topics.js")
const createUserInfoView = require("./views/user-info.js")
const createAuthorView = require("./views/author.js")
const createMessagesView = require("./views/messages.js")
const createDeletedView = require("./views/deleted.js")
const createReverseMapView = require("./views/reverse-hash-map.js")
const createLinksView = require("./views/links.js")
const createBlockedView = require("./views/blocked.js")
// roles and actions are moderation views
const createRolesView = require("./views/roles.js")
const createActionsView = require("./views/actions.js")
// aliases
const TEXT_POST = cable.TEXT_POST
const DELETE_POST = cable.DELETE_POST
const INFO_POST = cable.INFO_POST
const TOPIC_POST = cable.TOPIC_POST
const JOIN_POST = cable.JOIN_POST
const LEAVE_POST = cable.LEAVE_POST
const ROLE_POST = cable.ROLE_POST
const MODERATION_POST = cable.MODERATION_POST
const BLOCK_POST = cable.BLOCK_POST
const UNBLOCK_POST = cable.UNBLOCK_POST
// database interactions
class CableStore extends EventEmitter {
// TODO (2023-02-23): ensure proper handling of duplicates in views that index hashes
// TODO (2023-03-01): in all indexes, ensure that we never have any collisions with non-monotonic timestamps
// TODO (2023-03-01): do an error checking pass in all views, in particular the views that have async functions
// TODO (2023-03-02): look over lexicographic sort with regard to keyspace layout in each view
// TODO (2023-04-21): what shape should progressive history pruning take? how do we, for example, successively forget
// posts to stay within a hard boundary of say persisting max 100k posts on disk?
// TODO (2023-04-21): what should the mechanism look like which makes sure we have some post/info history (say keep
// the last 2 posts) while making sure all others are continually scrubbed?
// TODO (2023-04-21): restoring after a crash needs to be implemented. attempt an idea which acts as a companion index
// to reverseHashMap and the data store. if posts are in data store, but not fully indexed in all other views, they
// should remain in this companion index. once a post has been fully indexed, it is removed from the companion index
// (which acts as a sentinel of sorts)
// TODO (2023-04-21): should we forget about users (wrt channel-state) if they have left a channel and we no longer
// persist any of their messages in said channel? the core concern is that channel state request requires sending the
// latest post/info of **ex-members** which means that over time responses to a channel-state request will just grow
// and grow in size
constructor(level, localPublicKey, opts) {
super()
const storage = opts.storage || "data"
if (!opts) { opts = { temp: true } }
if (!level) { level = MemoryLevel }
this._db = new level(storage)
// NOTE: when adding a new view, increment `this._db.setMaxListeners` below
// we have many views using the same parent level instance -> extend the amount of event listeners to cover for that
// and squelch any "event listener leak" output
this._db.setMaxListeners(13) // one for each view
// reverseMapView maps which views have stored a particular hash. using this view we can removes those entries in
// other views if needed e.g. when a delete happens, when a peer has been blocked and their contents removed, or we are truncating the local database to save space
// note: this view is used by many of the other views which only stores a cable post hash, so it must be initialized
// before other views
this.reverseMapView = createReverseMapView(this._db.sublevel("reverse-hash-map", { valueEncoding: "json" }))
// this.blobs stores binary representations of message payloads by their hashes
this.blobs = createDatastore(this._db.sublevel("data-store", { valueEncoding: "binary" }), this.reverseMapView)
this.channelStateView = createChannelStateView(this._db.sublevel("channel-state", { valueEncoding: "binary" }), this.reverseMapView)
this.channelMembershipView = createChannelMembershipView(this._db.sublevel("channel-membership", { valueEncoding: "json" }))
this.topicView = createTopicView(this._db.sublevel("topics", { valueEncoding: "json" }))
this.userInfoView = createUserInfoView(this._db.sublevel("user-info", { valueEncoding: "binary" }), this.reverseMapView)
this.authorView = createAuthorView(this._db.sublevel("author", { valueEncoding: "binary" }), this.reverseMapView)
this.messagesView = createMessagesView(this._db.sublevel("messages", { valueEncoding: "binary" }), this.reverseMapView)
this.deletedView = createDeletedView(this._db.sublevel("deleted", { valueEncoding: "binary" }))
this.linksView = createLinksView(this._db.sublevel("links", { valueEncoding: "json" }))
this.actionsView = createActionsView(this._db.sublevel("actions", { valueEncoding: "utf8" }), () => { return localPublicKey })
this.rolesView = createRolesView(this._db.sublevel("roles", { valueEncoding: "binary" }))
this.blockedView = createBlockedView(this._db.sublevel("blocked", { valueEncoding: "binary" }))
// used primarily when processing an accepted delete request to delete entries in other views with the results from a reverse hash map query.
// however all views have been added to this map for sake of completeness
this._viewsMap = {
"reverse-hash-map": this.reverseMapView,
"data-store": this.blobs,
"channel-state": this.channelStateView,
"channel-membership": this.channelMembershipView,
"user-info": this.userInfoView,
"author": this.authorView,
"messages": this.messagesView,
"deleted": this.deletedView,
"links": this.linksView,
"actions": this.actionsView,
"roles": this.rolesView,
"blocked": this.blockedView
}
}
_storeNewPost(buf, hash, done) {
let promises = []
let p
// store each new post by associating its hash to the binary post payload
p = new Promise((res, rej) => {
this.blobs.map([{hash, buf}], (err) => {
if (err !== null) {
storedebug("blobs (error: %o)", err)
} else {
storedebug("blobs", err)
}
res()
})
})
promises.push(p)
const obj = cable.parsePost(buf)
// index posts made by author's public key and type of post
p = new Promise((res, rej) => {
this.authorView.map([{...obj, hash}], (err) => {
if (err !== null) {
storedebug("author (error: %o)", err)
} else {
storedebug("author")
}
res()
})
})
promises.push(p)
// index links information for the post: which links it has (if any), and reverse map those links as well to the
// hash of this post
p = new Promise((res, rej) => {
this.linksView.map([{links: obj.links, hash}], (err) => {
if (err) {
storedebug("storeNewPosts:links - error %O", err)
} else {
storedebug("storeNewPosts:links")
}
res()
})
})
promises.push(p)
Promise.all(promises).then(done)
}
// storage methods
// function `done` implements a kind of synching mechanism for each storage method, such that a collection of
// promises can be assembled, with a Promise.all(done) firing when all <view>.map invocations have finished processing
// indexing operations
//
// parameter `isAdmin` is a boolean that describes whether the author was an admin or not
role(buf, isAdmin, done) {
storedebug("role()")
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = ROLE_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.rolesView.map([{ ...obj, hash, isAdmin }], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel || constants.CABAL_CONTEXT)
if (isAdmin) {
this.emit("roles-update", { ...obj })
}
done()
})
}
// parameter `isApplicable` is a boolean that describes whether the author was a a moderation authority or not
moderation(buf, isApplicable, done) {
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = MODERATION_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.actionsView.map([{ ...obj, hash, isApplicable }], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel || constants.CABAL_CONTEXT)
if (isApplicable) {
this.emit("actions-update", { ...obj })
}
done()
})
}
// parameter `isApplicable` is a boolean that describes whether the author was a a moderation authority or not
block(buf, isApplicable, done) {
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = BLOCK_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.actionsView.map([{ ...obj, hash, isApplicable }], res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.blockedView.map([obj], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel || constants.CABAL_CONTEXT)
if (isApplicable) {
this.emit("actions-update", { ...obj })
}
done()
})
}
// parameter `isApplicable` is a boolean that describes whether the author was a a moderation authority or not
unblock(buf, isApplicable, done) {
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = UNBLOCK_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.actionsView.map([{ ...obj, hash, isApplicable }], res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.blockedView.map([obj], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel || constants.CABAL_CONTEXT)
if (isApplicable) {
this.emit("actions-update", { ...obj })
}
done()
})
}
join(buf, done) {
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = JOIN_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.channelStateView.map([{ ...obj, hash}], res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.channelMembershipView.map([obj], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel)
done()
})
}
leave(buf, done) {
if (!done) { done = util.noop }
let promises = []
let p
const hash = crypto.hash(buf)
const obj = LEAVE_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.channelStateView.map([{ ...obj, hash}], res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.channelMembershipView.map([obj], res)
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel)
done()
})
}
del(buf, done) {
if (!done) { done = util.noop }
// the hash of the post/delete message
const hash = crypto.hash(buf)
// note: obj.hashes is hash of the deleted post (not of the post/delete!)
const obj = DELETE_POST.toJSON(buf)
// verify that each hash requested to be deleted is in fact authorized (delete author and post author are the same)
// throw an error (and refuse to store this post/delete) if any of the hashes are authored by someone else
const prom = new Promise((res, rej) => {
this.blobs.api.getMany(obj.hashes, (err, bufs) => {
for (let i = 0; i < bufs.length; i++) {
if (!bufs[i]) {
storedebug("can't find buf corresponding to hash", obj.hashes[i])
continue
}
const post = cable.parsePost(bufs[i])
if (!b4a.equals(post.publicKey, obj.publicKey)) {
storedebug("del (err): hashes to delete %O post author was %O, delete author was %O", obj.hashes, post.publicKey, obj.publicKey)
return rej(new Error("post/delete author and author of hashes to delete did not match"))
}
}
res()
})
})
prom.then(() => {
// persist the post/delete buffer
return new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
})
.then(() => {
let processed = 0
// create a self-contained loop of deletions, where each related batch of deletions is performed in unison
obj.hashes.forEach(hashToDelete => {
deleteHash(hashToDelete, (err) => {
if (err) {
return done(err)
}
processed++
// signal done when all hashes to delete have been processed
if (processed >= obj.hashes.length) {
done()
}
})
})
})
// there was some kind of error, e.g. the delete post tried to delete someone else's post
.catch((err) => {
storedebug("error!", err)
done(err)
})
const deleteHash = (hashToDelete, finished) => {
const promises = []
let p
this.blobs.api.get(hashToDelete, async (err, retrievedBuf) => {
if (err) {
storedebug("delete err'd", err)
return finished(err)
}
const post = cable.parsePost(retrievedBuf)
storedebug("post to delete %O", post)
let channels = []
// TODO (2023-04-21): write a test to verify the following behaviour:
// 1. set a post/info
// 2. set another post/info
// 3. join 2-3 channels
// 4. delete the latest post/info
// 5. name should be regarded as updated to the first post/info name in all joined channels
//
// post/info is the only post type (as of 2023-04) that does not record channel information, and we need to know
// which channels in which to record a delete. we can get this info by querying the channel membership view for
// the publicKey that deleted a post/info, which returns the channels they have belonged to (incl current
// membership)
if (post.postType === constants.INFO_POST) {
const channelProm = new Promise((channelRes, channelRej) => {
this.channelMembershipView.api.getHistoricMembership(post.publicKey, (err, channels) => {
if (err) {
storedebug("deleteHash err'd during getHistoricMembership", err)
}
channelRes(channels)
})
})
channels = await channelProm
} else {
channels = [post.channel]
}
const affectedPublicKey = post.publicKey
// record the post/delete in the messages view for each relevant channel
channels.forEach(channel => {
p = new Promise((res, rej) => {
this.messagesView.map([{ ...obj, channel, hash}], res)
})
promises.push(p)
})
// delete the targeted post in the data store using obj.hash
p = new Promise((res, rej) => {
this.blobs.api.del(hashToDelete, res)
})
promises.push(p)
// record hash of deleted post in deletedView
p = new Promise((res, rej) => {
this.deletedView.map([hashToDelete], res)
})
promises.push(p)
// remove hash from indices that reference it
this.reverseMapView.api.getUses(hashToDelete, (err, uses) => {
// go through each index and delete the entry referencing this hash
for (let [viewName, viewKeys] of uses) {
viewKeys.forEach(key => {
storedebug("delete %s in %s", key, viewName)
this._viewsMap[viewName].api.del(key)
})
}
// finally, remove the related entries in the reverse hash map
this.reverseMapView.api.del(hashToDelete)
// when reindexing finishes, this function receives the new latest post hash and emits it to comply with
// correct live query behaviour wrt channel state request's `future` flag. what to do with the emitted data
// is handled by event consumers (e.g. CableCore)
const hashReceiver = (res) => {
if (res) {
// emit hash to signal reindex returned a new latest hash for channel after deleting the prior latest
this.emit("channel-state-replacement", { postType: res.postType, channel: res.channel, hash: res.hash })
}
}
// reindex accreted views if they were likely to be impacted e.g. reindex channel topic view if channel topic
// was deleted
// note: if what was deleted was a post/info that necessitates updating all channels that identity is or has been a member of
channels.forEach(channel => {
p = new Promise((res, rej) => {
switch (post.postType) {
case constants.JOIN_POST:
case constants.LEAVE_POST:
this._reindexChannelMembership(channel, affectedPublicKey, hashReceiver, res)
break
case constants.INFO_POST:
this._reindexInfoName(channel, affectedPublicKey, hashReceiver, res)
break
case constants.TOPIC_POST:
this._reindexTopic(channel, hashReceiver, res)
break
default:
res()
}
})
promises.push(p)
})
Promise.all(promises).then(() => {
// finally: emit 'store-post' event for the post/delete that were stored, one per relevant channel
channels.forEach(channel => {
this._emitStoredPost(hash, buf, channel)
})
finished()
})
})
})
}
}
// reindex an accreted view by re-putting a cablegram using its hash
_reindexHash (hash, mappingFunction, done) {
storedebug("reindexHash %O", hash)
this.blobs.api.get(hash, (err, buf) => {
if (err) {
storedebug("reindexHash could not find hash %O, returning early", hash)
return done()
}
storedebug("reindex with hash - blobs: err %O buf %O", err, buf)
const obj = cable.parsePost(buf)
mappingFunction([obj], done)
})
}
_reindexInfoName (channel, publicKey, sendHash, done) {
this.userInfoView.api.getLatestInfoHash(publicKey, (err, hash) => {
storedebug("latest name err", err)
storedebug("latest name hash", hash)
if (err && err.notFound) {
this.userInfoView.api.clearInfo(publicKey)
sendHash(null)
return done()
}
this._reindexHash(hash, this.userInfoView.map, done)
sendHash({channel, hash, postType: constants.INFO_POST})
})
}
_reindexTopic (channel, sendHash, done) {
this.channelStateView.api.getLatestTopicHash(channel, (err, hash) => {
storedebug("latest topic err", err)
storedebug("latest topic hash", hash)
if (err && err.notFound) {
this.topicView.api.clearTopic(channel)
sendHash(null)
return done()
}
this._reindexHash(hash, this.topicView.map, done)
sendHash({channel, hash, postType: constants.TOPIC_POST })
})
}
_reindexChannelMembership (channel, publicKey, sendHash, done) {
storedebug("reindex channel membership in %s for %s", channel, util.hex(publicKey))
this.channelStateView.api.getLatestMembershipHash(channel, publicKey, (err, hash) => {
storedebug("membership hash %O err %O", hash, err)
// the only membership record for the given channel was deleted: clear membership information regarding channel
if (!hash || (err && err.notFound)) {
this.channelMembershipView.api.clearMembership(channel, publicKey)
sendHash(null)
return done()
}
// we had prior membership information for channel, get the post and update the index
this._reindexHash(hash, this.channelMembershipView.map, done)
// look up the post by its hash get the exact post type. this doesn't really matter when responding to a live
// channel state request, since we just care if it was either a JOIN_POST or LEAVE_POST, but it's good to be
// correct in case this sees unexpected use somewhere else down the line
this.blobs.api.get((err, buf) => {
if (err || !buf) {
storedebug("reindex channel membership: could not get post associated with hash %O, returning early (err %O)", hash, err)
return
}
const obj = cable.parsePost(buf)
sendHash({channel, hash, postType: obj.postType })
})
})
}
topic(buf, done) {
if (!done) { done = util.noop }
const promises = []
let p
const hash = crypto.hash(buf)
const obj = TOPIC_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.channelStateView.map([{ ...obj, hash}], res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.topicView.map([obj], res)
})
promises.push(p)
// handle implicit channel membership behaviour (posting to a channel one is not a member of)
p = new Promise((res, rej) => {
// check if pubkey is a member of said channel
this.channelMembershipView.api.isInChannel(obj.channel, obj.publicKey, (err, isChannelMember) => {
// if not a member, and they posted a post/topic, register that as an intent to join that channel as a member
if (!err && !isChannelMember) {
this.channelMembershipView.map([obj], res)
return
}
res()
})
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel)
done()
})
}
text(buf, done) {
if (!done) { done = util.noop }
const promises = []
let p
const hash = crypto.hash(buf)
const obj = TEXT_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.messagesView.map([{ ...obj, hash}], res)
})
promises.push(p)
// handle implicit channel membership behaviour (posting to a channel one is not a member of)
p = new Promise((res, rej) => {
this.channelMembershipView.api.isInChannel(obj.channel, obj.publicKey, (err, isChannelMember) => {
// if not a member, and they posted a post/text, register that as an intent to join that channel as a member
if (!err && !isChannelMember) {
this.channelMembershipView.map([obj], res)
return
}
res()
})
})
promises.push(p)
Promise.all(promises).then(() => {
this._emitStoredPost(hash, buf, obj.channel)
done()
})
}
info(buf, done) {
if (!done) { done = util.noop }
const promises = []
let p
const hash = crypto.hash(buf)
const obj = INFO_POST.toJSON(buf)
p = new Promise((res, rej) => {
this._storeNewPost(buf, hash, res)
})
promises.push(p)
p = new Promise((res, rej) => {
this.userInfoView.map([{ ...obj, hash}], res)
})
promises.push(p)
// channel state view keeps track of info posts that set the name
// when we're setting a post/info we are not passed a channel. so to index this post correctly, for how the channel
// state index looks like right now, we need to get a list of channels that the user is in and post the update to
// each of those channels
this.channelMembershipView.api.getHistoricMembership(obj.publicKey, (err, channels) => {
const channelStateMessages = []
channels.forEach(channel => {
channelStateMessages.push({...obj, hash, channel})
})
storedebug(channelStateMessages)
p = new Promise((res, rej) => {
this.channelStateView.map(channelStateMessages, res)
})
promises.push(p)
// emit 'store-post' for each channel indexes have been confirmed to be updated
Promise.all(promises).then(() => {
new Set(channels).forEach(channel => {
this._emitStoredPost(hash, buf, channel)
})
done()
})
})
}
_emitStoredPost(hash, buf, channel) {
const obj = cable.parsePost(buf)
storedebug("store-post", { obj, hash, timestamp: obj.timestamp, channel, postType: util.humanizePostType(obj.postType) })
if (channel) {
this.emit("store-post", { obj, hash, timestamp: obj.timestamp, channel, postType: obj.postType })
} else { // probably a post/info, which has no channel membership information
this.emit("store-post", { obj, hash, timestamp: obj.timestamp, channel: null, postType: obj.postType })
}
}
// get data by list of hashes
getData(hashes, cb) {
this.blobs.api.getMany(hashes, cb)
}
// get hashes by channel name + channel time range
getChannelTimeRange(channel, start, end, limit, cb) {
this.messagesView.api.getChannelTimeRange(channel, start, end, limit, cb)
}
getTopic(channel, cb) {
this.topicView.api.getTopic(channel, cb)
}
}
module.exports = CableStore