mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-db.git
synced 2026-09-21 16:52:02 -07:00
Adding reply count to post metadata
This commit is contained in:
@@ -23,7 +23,9 @@ class Adapters {
|
||||
profilesDb: level.profilesDb
|
||||
})
|
||||
this.postQuery = new PostQuery({
|
||||
postsDb: level.postsDb
|
||||
postsDb: level.postsDb,
|
||||
postParentsDb: level.postParentsDb,
|
||||
postChildrenDb: level.postChildrenDb
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,27 +1,67 @@
|
||||
/*
|
||||
Adapter for scanning posts with stored block height.
|
||||
Excludes reply posts (txids present in postParentsDb).
|
||||
*/
|
||||
|
||||
class PostQuery {
|
||||
constructor (localConfig = {}) {
|
||||
const { postsDb } = localConfig
|
||||
const { postsDb, postParentsDb, postChildrenDb } = localConfig
|
||||
if (!postsDb) {
|
||||
throw new Error('postsDb required when instantiating PostQuery adapter.')
|
||||
}
|
||||
if (!postParentsDb) {
|
||||
throw new Error('postParentsDb required when instantiating PostQuery adapter.')
|
||||
}
|
||||
if (!postChildrenDb) {
|
||||
throw new Error('postChildrenDb required when instantiating PostQuery adapter.')
|
||||
}
|
||||
this.postsDb = postsDb
|
||||
this.postParentsDb = postParentsDb
|
||||
this.postChildrenDb = postChildrenDb
|
||||
this.scanPostsWithBlockHeight = this.scanPostsWithBlockHeight.bind(this)
|
||||
this.scanPostsByAddr = this.scanPostsByAddr.bind(this)
|
||||
this.loadReplyTxids = this.loadReplyTxids.bind(this)
|
||||
this.buildReplyCountMap = this.buildReplyCountMap.bind(this)
|
||||
}
|
||||
|
||||
async loadReplyTxids () {
|
||||
const replyTxids = new Set()
|
||||
|
||||
for await (const [childTxid] of this.postParentsDb.iterator()) {
|
||||
replyTxids.add(childTxid)
|
||||
}
|
||||
|
||||
return replyTxids
|
||||
}
|
||||
|
||||
async buildReplyCountMap () {
|
||||
const counts = new Map()
|
||||
|
||||
for await (const [, child] of this.postChildrenDb.iterator()) {
|
||||
const parentTxid = child.parentTxid
|
||||
if (!parentTxid) continue
|
||||
counts.set(parentTxid, (counts.get(parentTxid) || 0) + 1)
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
|
||||
async scanPostsWithBlockHeight () {
|
||||
const [replyTxids, replyCounts] = await Promise.all([
|
||||
this.loadReplyTxids(),
|
||||
this.buildReplyCountMap()
|
||||
])
|
||||
const posts = []
|
||||
|
||||
for await (const [txid, post] of this.postsDb.iterator()) {
|
||||
if (replyTxids.has(txid)) continue
|
||||
posts.push({
|
||||
txid,
|
||||
addr: post.addr,
|
||||
text: post.text,
|
||||
seen: post.seen,
|
||||
blockHeight: post.blockHeight ?? 0
|
||||
blockHeight: post.blockHeight ?? 0,
|
||||
replyCount: replyCounts.get(txid) ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,16 +69,22 @@ class PostQuery {
|
||||
}
|
||||
|
||||
async scanPostsByAddr (addr) {
|
||||
const [replyTxids, replyCounts] = await Promise.all([
|
||||
this.loadReplyTxids(),
|
||||
this.buildReplyCountMap()
|
||||
])
|
||||
const posts = []
|
||||
|
||||
for await (const [txid, post] of this.postsDb.iterator()) {
|
||||
if (post.addr !== addr) continue
|
||||
if (replyTxids.has(txid)) continue
|
||||
posts.push({
|
||||
txid,
|
||||
addr: post.addr,
|
||||
text: post.text,
|
||||
seen: post.seen,
|
||||
blockHeight: post.blockHeight ?? 0
|
||||
blockHeight: post.blockHeight ?? 0,
|
||||
replyCount: replyCounts.get(txid) ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class PostsRESTControllerLib {
|
||||
* @apiName GetRecentPosts
|
||||
* @apiGroup REST Posts
|
||||
*
|
||||
* @apiDescription Returns posts sorted by block height (newest first), with seen timestamp as tie-breaker.
|
||||
* @apiDescription Returns top-level posts only (replies excluded), sorted by block height (newest first), with seen timestamp as tie-breaker.
|
||||
*
|
||||
* @apiQuery {Number} [limit=100] Page size (max 100)
|
||||
* @apiQuery {Number} [offset=0] Number of posts to skip after sorting
|
||||
@@ -49,6 +49,7 @@ class PostsRESTControllerLib {
|
||||
* @apiSuccess {String} posts.text Post text
|
||||
* @apiSuccess {Number} posts.seen Unix epoch milliseconds
|
||||
* @apiSuccess {Number} posts.blockHeight Block height when indexed
|
||||
* @apiSuccess {Number} posts.replyCount Number of replies to this post
|
||||
* @apiSuccess {Object} pagination Pagination metadata
|
||||
* @apiSuccess {Number} pagination.limit Page size used
|
||||
* @apiSuccess {Number} pagination.offset Offset used
|
||||
@@ -70,7 +71,7 @@ class PostsRESTControllerLib {
|
||||
* @apiName GetPostsByAddr
|
||||
* @apiGroup REST Posts
|
||||
*
|
||||
* @apiDescription Returns posts for a single address sorted by block height (newest first).
|
||||
* @apiDescription Returns top-level posts for a single address (replies excluded), sorted by block height (newest first).
|
||||
*
|
||||
* @apiParam {String} addr Author cash address
|
||||
* @apiQuery {Number} [limit=100] Page size (max 100)
|
||||
@@ -85,6 +86,7 @@ class PostsRESTControllerLib {
|
||||
* @apiSuccess {String} posts.text Post text
|
||||
* @apiSuccess {Number} posts.seen Unix epoch milliseconds
|
||||
* @apiSuccess {Number} posts.blockHeight Block height when indexed
|
||||
* @apiSuccess {Number} posts.replyCount Number of replies to this post
|
||||
* @apiSuccess {Object} pagination Pagination metadata
|
||||
*/
|
||||
async getPostsByAddr (ctx) {
|
||||
|
||||
@@ -6,13 +6,25 @@ describe('#PostQuery', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let postsDb
|
||||
let postParentsDb
|
||||
let postChildrenDb
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
postsDb = {
|
||||
iterator: sandbox.stub()
|
||||
}
|
||||
uut = new PostQuery({ postsDb })
|
||||
postParentsDb = {
|
||||
iterator: sandbox.stub()
|
||||
}
|
||||
postChildrenDb = {
|
||||
iterator: sandbox.stub()
|
||||
}
|
||||
async function * emptyParents () {}
|
||||
async function * emptyChildren () {}
|
||||
postParentsDb.iterator.returns(emptyParents())
|
||||
postChildrenDb.iterator.returns(emptyChildren())
|
||||
uut = new PostQuery({ postsDb, postParentsDb, postChildrenDb })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
@@ -29,8 +41,10 @@ describe('#PostQuery', () => {
|
||||
assert.equal(result.length, 2)
|
||||
assert.equal(result[0].txid, 'tx1')
|
||||
assert.equal(result[0].blockHeight, 600100)
|
||||
assert.equal(result[0].replyCount, 0)
|
||||
assert.equal(result[1].txid, 'tx2')
|
||||
assert.equal(result[1].blockHeight, 600200)
|
||||
assert.equal(result[1].replyCount, 0)
|
||||
})
|
||||
|
||||
it('should use block height 0 when field is missing', async () => {
|
||||
@@ -42,6 +56,7 @@ describe('#PostQuery', () => {
|
||||
const result = await uut.scanPostsWithBlockHeight()
|
||||
|
||||
assert.equal(result[0].blockHeight, 0)
|
||||
assert.equal(result[0].replyCount, 0)
|
||||
})
|
||||
|
||||
it('should scan posts for a single address', async () => {
|
||||
@@ -58,4 +73,72 @@ describe('#PostQuery', () => {
|
||||
assert.equal(result[0].txid, 'tx1')
|
||||
assert.equal(result[1].txid, 'tx3')
|
||||
})
|
||||
|
||||
it('should exclude reply posts from recent scan', async () => {
|
||||
async function * mockParents () {
|
||||
yield ['tx-reply', { parentTxid: 'tx1', childTxid: 'tx-reply', blockHeight: 600150 }]
|
||||
}
|
||||
async function * mockPosts () {
|
||||
yield ['tx1', { addr: 'addr1', text: 'top', seen: 1000, blockHeight: 600100 }]
|
||||
yield ['tx-reply', { addr: 'addr1', text: 'reply', seen: 1500, blockHeight: 600150 }]
|
||||
yield ['tx2', { addr: 'addr2', text: 'other', seen: 2000, blockHeight: 600200 }]
|
||||
}
|
||||
postParentsDb.iterator.returns(mockParents())
|
||||
postsDb.iterator.returns(mockPosts())
|
||||
|
||||
const result = await uut.scanPostsWithBlockHeight()
|
||||
|
||||
assert.equal(result.length, 2)
|
||||
assert.equal(result[0].txid, 'tx1')
|
||||
assert.equal(result[1].txid, 'tx2')
|
||||
})
|
||||
|
||||
it('should exclude reply posts from address scan', async () => {
|
||||
async function * mockParents () {
|
||||
yield ['tx-reply', { parentTxid: 'tx1', childTxid: 'tx-reply', blockHeight: 600150 }]
|
||||
}
|
||||
async function * mockPosts () {
|
||||
yield ['tx1', { addr: 'addr-a', text: 'top', seen: 1000, blockHeight: 600100 }]
|
||||
yield ['tx-reply', { addr: 'addr-a', text: 'reply', seen: 1500, blockHeight: 600150 }]
|
||||
}
|
||||
postParentsDb.iterator.returns(mockParents())
|
||||
postsDb.iterator.returns(mockPosts())
|
||||
|
||||
const result = await uut.scanPostsByAddr('addr-a')
|
||||
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].txid, 'tx1')
|
||||
})
|
||||
|
||||
it('should include replyCount from postChildren scan', async () => {
|
||||
async function * mockChildren () {
|
||||
yield ['tx1:reply-a', { parentTxid: 'tx1', childTxid: 'reply-a', blockHeight: 600150 }]
|
||||
yield ['tx1:reply-b', { parentTxid: 'tx1', childTxid: 'reply-b', blockHeight: 600160 }]
|
||||
yield ['tx2:reply-c', { parentTxid: 'tx2', childTxid: 'reply-c', blockHeight: 600170 }]
|
||||
}
|
||||
async function * mockPosts () {
|
||||
yield ['tx1', { addr: 'addr1', text: 'top', seen: 1000, blockHeight: 600100 }]
|
||||
yield ['tx2', { addr: 'addr2', text: 'other', seen: 2000, blockHeight: 600200 }]
|
||||
}
|
||||
postChildrenDb.iterator.returns(mockChildren())
|
||||
postsDb.iterator.returns(mockPosts())
|
||||
|
||||
const result = await uut.scanPostsWithBlockHeight()
|
||||
|
||||
assert.equal(result.length, 2)
|
||||
assert.equal(result.find((p) => p.txid === 'tx1').replyCount, 2)
|
||||
assert.equal(result.find((p) => p.txid === 'tx2').replyCount, 1)
|
||||
})
|
||||
|
||||
it('should default replyCount to 0 when post has no replies', async () => {
|
||||
async function * mockPosts () {
|
||||
yield ['tx1', { addr: 'addr1', text: 'solo', seen: 1000, blockHeight: 600100 }]
|
||||
}
|
||||
postsDb.iterator.returns(mockPosts())
|
||||
|
||||
const result = await uut.scanPostsByAddr('addr1')
|
||||
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].replyCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user