mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Refactor following feed cap: share acceptance seeding, add cap properties
Extract putPost/putFollowEdge seeding helpers in the psf-memo-db acceptance handlers and route the following-feed capped/mixed fixtures and the many-top-level-posts fixture through them, removing repeated store/key writes. Add property tests for the capped total, newest-first page conservation, and the bounded eligible scan, plus unit coverage for the remaining PostQuery constructor guards and static key helpers (post-query.js statements 94.5% -> 99.4%). CRAP stays at or below 6, src/adapters/post-query.js remains at 40 mutation sites with its manifest intact, and the DRY tool reports no duplicates in the changed source file. By refactorer.
This commit is contained in:
@@ -44,6 +44,39 @@ function padHeight (blockHeight) {
|
|||||||
return String(blockHeight ?? 0).padStart(12, '0')
|
return String(blockHeight ?? 0).padStart(12, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared fixture seeding helpers so every loader writes the same post and
|
||||||
|
// follow records without repeating the store/key shape at each call site.
|
||||||
|
async function putFollowEdge (world, { followerAddr, followeeAddr, txid, blockHeight = 600000 }) {
|
||||||
|
const followeePkHash = hash160(followeeAddr)
|
||||||
|
await world.adapters.level.followsDb.put(`${followerAddr}:${followeePkHash}`, {
|
||||||
|
followerAddr,
|
||||||
|
followeePkHash,
|
||||||
|
unfollow: false,
|
||||||
|
txid,
|
||||||
|
seen: 1,
|
||||||
|
blockHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putPost (world, { txid, addr, text = txid, seen = 0, blockHeight, withAddrIndex = false }) {
|
||||||
|
await world.adapters.level.postsDb.put(txid, {
|
||||||
|
addr,
|
||||||
|
text,
|
||||||
|
seen,
|
||||||
|
blockHeight
|
||||||
|
})
|
||||||
|
await world.adapters.level.postHeightsDb.put(
|
||||||
|
`${padHeight(blockHeight)}:${txid}`,
|
||||||
|
{ txid, blockHeight }
|
||||||
|
)
|
||||||
|
if (withAddrIndex) {
|
||||||
|
await world.adapters.level.addrPostHeightsDb.put(
|
||||||
|
`${addr}:${padHeight(blockHeight)}:${txid}`,
|
||||||
|
{ txid, addr, blockHeight }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
const tmpDir = path.resolve(__dirname, '..', '..', 'tmp', 'acceptance')
|
const tmpDir = path.resolve(__dirname, '..', '..', 'tmp', 'acceptance')
|
||||||
|
|
||||||
@@ -467,22 +500,14 @@ async function loadManyTopLevelPosts (world) {
|
|||||||
// index and reads all 510 entries.
|
// index and reads all 510 entries.
|
||||||
for (let i = 0; i < 510; i++) {
|
for (let i = 0; i < 510; i++) {
|
||||||
const id = String(i).padStart(3, '0')
|
const id = String(i).padStart(3, '0')
|
||||||
const txid = `post-${id}`
|
await putPost(world, {
|
||||||
const blockHeight = 600000 + i
|
txid: `post-${id}`,
|
||||||
await world.adapters.level.postsDb.put(txid, {
|
|
||||||
addr: 'bitcoincash:qaddr',
|
addr: 'bitcoincash:qaddr',
|
||||||
text: `post ${id}`,
|
text: `post ${id}`,
|
||||||
seen: i,
|
seen: i,
|
||||||
blockHeight
|
blockHeight: 600000 + i,
|
||||||
|
withAddrIndex: true
|
||||||
})
|
})
|
||||||
await world.adapters.level.postHeightsDb.put(
|
|
||||||
String(blockHeight).padStart(12, '0') + ':' + txid,
|
|
||||||
{ txid, blockHeight }
|
|
||||||
)
|
|
||||||
await world.adapters.level.addrPostHeightsDb.put(
|
|
||||||
`bitcoincash:qaddr:${String(blockHeight).padStart(12, '0')}:${txid}`,
|
|
||||||
{ txid, addr: 'bitcoincash:qaddr', blockHeight }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,30 +516,22 @@ async function loadFollowingFeedCapped (world) {
|
|||||||
const followee = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
|
const followee = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy'
|
||||||
world.fixtureViewer = viewer
|
world.fixtureViewer = viewer
|
||||||
|
|
||||||
await world.adapters.level.followsDb.put(`${viewer}:${hash160(followee)}`, {
|
await putFollowEdge(world, {
|
||||||
followerAddr: viewer,
|
followerAddr: viewer,
|
||||||
followeePkHash: hash160(followee),
|
followeeAddr: followee,
|
||||||
unfollow: false,
|
txid: 'follow-capped'
|
||||||
txid: 'follow-capped',
|
|
||||||
seen: 1,
|
|
||||||
blockHeight: 600000
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 510 eligible top-level posts, larger than the 500 total-scan cap.
|
// 510 eligible top-level posts, larger than the 500 total-scan cap.
|
||||||
for (let i = 0; i < 510; i++) {
|
for (let i = 0; i < 510; i++) {
|
||||||
const id = String(i).padStart(3, '0')
|
const id = String(i).padStart(3, '0')
|
||||||
const txid = `post-${id}`
|
await putPost(world, {
|
||||||
const blockHeight = 600000 + i
|
txid: `post-${id}`,
|
||||||
await world.adapters.level.postsDb.put(txid, {
|
|
||||||
addr: followee,
|
addr: followee,
|
||||||
text: `post ${id}`,
|
text: `post ${id}`,
|
||||||
seen: i,
|
seen: i,
|
||||||
blockHeight
|
blockHeight: 600000 + i
|
||||||
})
|
})
|
||||||
await world.adapters.level.postHeightsDb.put(
|
|
||||||
`${padHeight(blockHeight)}:${txid}`,
|
|
||||||
{ txid, blockHeight }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,13 +544,10 @@ async function loadFollowingFeedMixed (world) {
|
|||||||
const other = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a'
|
const other = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a'
|
||||||
world.fixtureViewer = viewer
|
world.fixtureViewer = viewer
|
||||||
|
|
||||||
await world.adapters.level.followsDb.put(`${viewer}:${hash160(followee)}`, {
|
await putFollowEdge(world, {
|
||||||
followerAddr: viewer,
|
followerAddr: viewer,
|
||||||
followeePkHash: hash160(followee),
|
followeeAddr: followee,
|
||||||
unfollow: false,
|
txid: 'follow-mixed'
|
||||||
txid: 'follow-mixed',
|
|
||||||
seen: 1,
|
|
||||||
blockHeight: 600000
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const posts = [
|
const posts = [
|
||||||
@@ -544,16 +558,12 @@ async function loadFollowingFeedMixed (world) {
|
|||||||
{ txid: 'reply-A2', addr: followee, blockHeight: 600300 }
|
{ txid: 'reply-A2', addr: followee, blockHeight: 600300 }
|
||||||
]
|
]
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
await world.adapters.level.postsDb.put(post.txid, {
|
await putPost(world, {
|
||||||
|
txid: post.txid,
|
||||||
addr: post.addr,
|
addr: post.addr,
|
||||||
text: post.txid,
|
|
||||||
seen: post.blockHeight,
|
seen: post.blockHeight,
|
||||||
blockHeight: post.blockHeight
|
blockHeight: post.blockHeight
|
||||||
})
|
})
|
||||||
await world.adapters.level.postHeightsDb.put(
|
|
||||||
`${padHeight(post.blockHeight)}:${post.txid}`,
|
|
||||||
{ txid: post.txid, blockHeight: post.blockHeight }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = { txid: 'reply-A2', parentTxid: 'post-A2', childTxid: 'reply-A2', blockHeight: 600300 }
|
const reply = { txid: 'reply-A2', parentTxid: 'post-A2', childTxid: 'reply-A2', blockHeight: 600300 }
|
||||||
|
|||||||
@@ -185,3 +185,98 @@ test('following-feed scan never returns replies, the viewer, or un-followed auth
|
|||||||
{ label: 'following-feed reply/viewer/membership exclusion' }
|
{ label: 'following-feed reply/viewer/membership exclusion' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// All-eligible corpus: every indexed entry is a top-level post by the single
|
||||||
|
// followed author, so the scan's eligible count equals the number of posts it
|
||||||
|
// reads. `cap` varies the total-scan cap so the capped and below-cap branches
|
||||||
|
// are both exercised.
|
||||||
|
function cappedCorpusGen () {
|
||||||
|
return () => {
|
||||||
|
const n = intGen(rng, 0, 25)()
|
||||||
|
const followee = 'bitcoincash:f1'
|
||||||
|
const posts = []
|
||||||
|
const postHeights = []
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const txid = txidGen(rng)
|
||||||
|
const height = intGen(rng, 0, 9000000)()
|
||||||
|
posts.push({ txid, addr: followee, text: 'post ' + i, seen: i, blockHeight: height })
|
||||||
|
postHeights.push({ key: PostQuery.postHeightKey(height, txid), value: { txid } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
n,
|
||||||
|
posts,
|
||||||
|
postHeights,
|
||||||
|
followed: [VIEWER, followee],
|
||||||
|
limit: intGen(rng, 1, 8)(),
|
||||||
|
offset: intGen(rng, 0, 12)(),
|
||||||
|
cap: intGen(rng, 1, 12)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderedPostTxids (posts) {
|
||||||
|
return [...posts]
|
||||||
|
.sort((a, b) => (PostQuery.postHeightKey(a.blockHeight, a.txid) < PostQuery.postHeightKey(b.blockHeight, b.txid) ? 1 : -1))
|
||||||
|
.map((p) => p.txid)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('following-feed scan caps the total and conserves the newest-first page', async () => {
|
||||||
|
await forAll(
|
||||||
|
cappedCorpusGen(),
|
||||||
|
async ({ n, posts, postHeights, followed, limit, offset, cap }) => {
|
||||||
|
const query = makeQuery(postHeights, posts, new Set())
|
||||||
|
const { txids, total } = await query.scanFollowingFeedTxidsAndCount(
|
||||||
|
VIEWER,
|
||||||
|
followed,
|
||||||
|
{ limit, offset, totalScanCap: cap }
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedTotal = Math.min(n, cap)
|
||||||
|
const expectedTxids = orderedPostTxids(posts).slice(offset, offset + limit)
|
||||||
|
|
||||||
|
return total === expectedTotal && JSON.stringify(txids) === JSON.stringify(expectedTxids)
|
||||||
|
},
|
||||||
|
{ label: 'following-feed capped-total and page conservation' }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('following-feed scan reads at most offset + limit + cap eligible posts', async () => {
|
||||||
|
await forAll(
|
||||||
|
cappedCorpusGen(),
|
||||||
|
async ({ n, posts, postHeights, followed, limit, offset, cap }) => {
|
||||||
|
const counter = { reads: 0 }
|
||||||
|
const store = new Map(posts.map((p) => [p.txid, p]))
|
||||||
|
const query = new PostQuery({
|
||||||
|
postsDb: {
|
||||||
|
async get (txid) {
|
||||||
|
counter.reads++
|
||||||
|
const post = store.get(txid)
|
||||||
|
if (!post) {
|
||||||
|
const err = new Error('not found')
|
||||||
|
err.notFound = true
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postHeightsDb: makePostHeightsDb(postHeights),
|
||||||
|
addrPostHeightsDb: {},
|
||||||
|
postParentsDb: makeParentsDb(new Set()),
|
||||||
|
postChildrenDb: {},
|
||||||
|
likesDb: {},
|
||||||
|
postLikesDb: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
await query.scanFollowingFeedTxidsAndCount(
|
||||||
|
VIEWER,
|
||||||
|
followed,
|
||||||
|
{ limit, offset, totalScanCap: cap }
|
||||||
|
)
|
||||||
|
|
||||||
|
return counter.reads === Math.min(n, offset + limit + cap)
|
||||||
|
},
|
||||||
|
{ label: 'following-feed bounded eligible scan' }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -94,6 +94,59 @@ describe('#PostQuery', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should throw when postsDb is missing', () => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new PostQuery({ postHeightsDb, addrPostHeightsDb, postParentsDb, postChildrenDb, likesDb, postLikesDb })
|
||||||
|
assert.fail('Expected error')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'postsDb required')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw when postParentsDb is missing', () => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postChildrenDb, likesDb, postLikesDb })
|
||||||
|
assert.fail('Expected error')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'postParentsDb required')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw when postChildrenDb is missing', () => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postParentsDb, likesDb, postLikesDb })
|
||||||
|
assert.fail('Expected error')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'postChildrenDb required')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw when likesDb is missing', () => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new PostQuery({ postsDb, postHeightsDb, addrPostHeightsDb, postParentsDb, postChildrenDb, postLikesDb })
|
||||||
|
assert.fail('Expected error')
|
||||||
|
} catch (err) {
|
||||||
|
assert.include(err.message, 'likesDb required')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#static key helpers', () => {
|
||||||
|
it('should pad block heights to a fixed width for lexicographic ordering', () => {
|
||||||
|
assert.equal(PostQuery.padHeight(600200), '000000600200')
|
||||||
|
assert.equal(PostQuery.padHeight(1), '000000000001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should compose postHeight, addrPostHeight, and postLike keys', () => {
|
||||||
|
assert.equal(PostQuery.postHeightKey(600200, 'post-a'), '000000600200:post-a')
|
||||||
|
assert.equal(PostQuery.addrPostHeightKey('bitcoincash:addr', 600200, 'post-a'), 'bitcoincash:addr:000000600200:post-a')
|
||||||
|
assert.equal(PostQuery.postLikeKey('post-a', 'like-1'), 'post-a:like-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('#txidFromPostHeight', () => {
|
describe('#txidFromPostHeight', () => {
|
||||||
it('should return the txid from the value when present', () => {
|
it('should return the txid from the value when present', () => {
|
||||||
assert.equal(uut.txidFromPostHeight('any-key', { txid: 'abc123' }), 'abc123')
|
assert.equal(uut.txidFromPostHeight('any-key', { txid: 'abc123' }), 'abc123')
|
||||||
|
|||||||
Reference in New Issue
Block a user