diff --git a/src/adapters/post-query.js b/src/adapters/post-query.js index ea162bd..e3c417f 100644 --- a/src/adapters/post-query.js +++ b/src/adapters/post-query.js @@ -90,6 +90,30 @@ class PostQuery { return posts } + + async buildReplyCountMap () { + const counts = new Map() + let total = 0 + + for await (const [childTxid, child] of this.postChildrenDb.iterator()) { + total++ + + console.log('Indexed reply:', { + childTxid, + child, + parentTxid: child?.parentTxid + }) + + const parentTxid = child?.parentTxid + if (!parentTxid) continue + + counts.set(parentTxid, (counts.get(parentTxid) || 0) + 1) + } + + console.log(`Total postChildrenDb records: ${total}`) + + return counts +} } export default PostQuery diff --git a/src/controllers/rest-api/posts/controller.js b/src/controllers/rest-api/posts/controller.js index 37bb2b0..51e7726 100644 --- a/src/controllers/rest-api/posts/controller.js +++ b/src/controllers/rest-api/posts/controller.js @@ -17,6 +17,7 @@ class PostsRESTControllerLib { this.getRecentPosts = this.getRecentPosts.bind(this) this.getPostsByAddr = this.getPostsByAddr.bind(this) + this.getPostThread = this.getPostThread.bind(this) this.handleError = this.handleError.bind(this) } @@ -98,6 +99,18 @@ class PostsRESTControllerLib { this.handleError(ctx, err) } } + + async getPostThread (ctx) { + try { + const { txid } = ctx.params + + ctx.body = await this.useCases.getPostThread.execute({ + txid + }) + } catch (err) { + this.handleError(ctx, err) + } + } } export default PostsRESTControllerLib diff --git a/src/controllers/rest-api/posts/index.js b/src/controllers/rest-api/posts/index.js index c4c37af..ab2a9b5 100644 --- a/src/controllers/rest-api/posts/index.js +++ b/src/controllers/rest-api/posts/index.js @@ -26,6 +26,7 @@ class PostsRouter { attach (app) { this.router.get('/recent', this.postsRESTController.getRecentPosts) this.router.get('/by/:addr', this.postsRESTController.getPostsByAddr) + this.router.get('/:txid/thread',this.postsRESTController.getPostThread) app.use(this.router.routes()) app.use(this.router.allowedMethods()) } diff --git a/src/use-cases/get-post-thread.js b/src/use-cases/get-post-thread.js new file mode 100644 index 0000000..ccb920d --- /dev/null +++ b/src/use-cases/get-post-thread.js @@ -0,0 +1,101 @@ +/* + Retrieve a Memo post and its nested replies. +*/ + +class GetPostThread { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + + if (!this.adapters) { + throw new Error( + 'Adapters required when instantiating GetPostThread.' + ) + } + + this.execute = this.execute.bind(this) + this.buildThreadNode = this.buildThreadNode.bind(this) + } + + async execute ({ txid } = {}) { + if (!txid || typeof txid !== 'string') { + const err = new Error('A transaction ID is required.') + err.status = 400 + throw err + } + + const rootPost = await this.buildThreadNode(txid) + + if (!rootPost) { + const err = new Error('Post not found.') + err.status = 404 + throw err + } + + return { + post: rootPost + } + } + + async buildThreadNode (txid, visited = new Set()) { + if (visited.has(txid)) return null + + visited.add(txid) + + let post + + try { + post = await this.adapters.postQuery.postsDb.get(txid) + } catch (err) { + if (err.notFound || err.code === 'LEVEL_NOT_FOUND') { + return null + } + + throw err + } + + const childTxids = [] + + for await ( + const [, child] + of this.adapters.postQuery.postChildrenDb.iterator() + ) { + if (child?.parentTxid !== txid) continue + if (!child?.childTxid) continue + + childTxids.push(child.childTxid) + } + + const replies = [] + + for (const childTxid of childTxids) { + const reply = await this.buildThreadNode(childTxid, visited) + + if (reply) { + replies.push(reply) + } + } + + replies.sort((a, b) => { + const blockDifference = + (a.blockHeight ?? 0) - (b.blockHeight ?? 0) + + if (blockDifference !== 0) { + return blockDifference + } + + return (a.seen ?? 0) - (b.seen ?? 0) + }) + + return { + txid, + addr: post.addr, + text: post.text, + seen: post.seen, + blockHeight: post.blockHeight ?? 0, + replyCount: replies.length, + replies + } + } +} + +export default GetPostThread \ No newline at end of file diff --git a/src/use-cases/index.js b/src/use-cases/index.js index 929a602..ad75507 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -5,26 +5,46 @@ import ListRecentProfiles from './list-recent-profiles.js' import ListRecentPosts from './list-recent-posts.js' import ListPostsByAddr from './list-posts-by-addr.js' +import GetPostThread from './get-post-thread.js' class UseCases { constructor (localConfig = {}) { this.adapters = localConfig.adapters + if (!this.adapters) { - throw new Error('Adapters required when instantiating UseCases.') + throw new Error( + 'Adapters required when instantiating UseCases.' + ) } this.listRecentProfiles = null this.listRecentPosts = null this.listPostsByAddr = null + this.getPostThread = null } async start () { - this.listRecentProfiles = new ListRecentProfiles({ adapters: this.adapters }) - this.listRecentPosts = new ListRecentPosts({ adapters: this.adapters }) - this.listPostsByAddr = new ListPostsByAddr({ adapters: this.adapters }) + this.listRecentProfiles = new ListRecentProfiles({ + adapters: this.adapters + }) + + this.listRecentPosts = new ListRecentPosts({ + adapters: this.adapters + }) + + this.listPostsByAddr = new ListPostsByAddr({ + adapters: this.adapters + }) + + this.getPostThread = new GetPostThread({ + adapters: this.adapters + }) + console.log('Use cases initialized.') + return true } } export default UseCases +