Spec topic metadata columns

Define four-column topics page behavior across client, indexer, and DB:
topic name, time since most recent post, post count, and follower count.

By specifier.
This commit is contained in:
Chris Troutner
2026-09-18 05:38:41 -07:00
parent 6ef2e3589e
commit a599b34e38
4 changed files with 217 additions and 1 deletions
@@ -0,0 +1,44 @@
# Scenarios: Topic Metadata Columns - 1, Topic Metadata Columns - 2, Topic Metadata Columns - 3
#
# The topics page renders one row per topic with four columns: the topic name,
# the time since the topic's most recent post, the post count, and the follower
# count. The relative time is computed from the API's lastSeen timestamp (epoch
# milliseconds) against the current time:
# - no posts -> "No posts"
# - less than an hour -> "Less than an hour ago"
# - under 24 hours -> "N hours ago" (1 hour ago when singular)
# - 24 hours or more -> "N days ago" (1 day ago when singular)
Feature: Topic Metadata Columns
Scenario Outline: Topic Metadata Columns - 1 the topics page shows each topic's post count and follower count
Given the psf-memo-db API serves a topic named "<room>" with <postCount> posts
And the topic "<room>" has <followerCount> followers
When I open the topics page
Then the topics page shows the topic "<room>" with <expectedPostCount> posts
And the topics page shows the topic "<room>" with <expectedFollowerCount> followers
Examples:
| room | postCount | followerCount | expectedPostCount | expectedFollowerCount |
| bitcoin | 42 | 17 | 42 | 17 |
| cash | 7 | 3 | 7 | 3 |
Scenario Outline: Topic Metadata Columns - 2 the topics page shows the most recent post in hours or days
Given the current time is 1800000000000
And the psf-memo-db API serves a topic named "<room>" with 1 post
And the topic "<room>" was last posted at <lastSeen>
When I open the topics page
Then the topics page shows the most recent post for the topic "<room>" as "<label>"
Examples:
| room | lastSeen | label |
| bitcoin | 1799998200000 | Less than an hour ago |
| cash | 1799996400000 | 1 hour ago |
| dance | 1799982000000 | 5 hours ago |
| dev | 1799917200000 | 23 hours ago |
| anime | 1799913600000 | 1 day ago |
| music | 1799827200000 | 2 days ago |
Scenario: Topic Metadata Columns - 3 a topic with no posts shows "No posts"
Given the psf-memo-db API serves a topic named "lone" with 0 posts
When I open the topics page
Then the topics page shows the most recent post for the topic "lone" as "No posts"
+84
View File
@@ -0,0 +1,84 @@
# Scenarios: Topic Metadata - 1, Topic Metadata - 2, Topic Metadata - 3, Topic Metadata - 4, Topic Metadata - 5
#
# GET /topics reports each topic's lastSeen (the epoch-millisecond time of the
# room's most recent post, 0 for a follow-only room) and followerCount (the
# number of active follows) alongside the existing postCount. The topic
# backfill utility rebuilds lastSeen and followerCount from the rooms store, and
# running it twice is idempotent.
#
# Fixture "topic-metadata-indexes" (topic index stores):
# topicSummaries:
# memo { room: memo, postCount: 5, lastHeight: 600500, lastSeen: 1700020000000, followerCount: 12 }
# cash { room: cash, postCount: 2, lastHeight: 600400, lastSeen: 1700010000000, followerCount: 4 }
# lone { room: lone, postCount: 0, lastHeight: 0, lastSeen: 0, followerCount: 7 }
# topicRecency:
# memo at 600500, cash at 600400, lone at 0
#
# Fixture "rooms-with-topic-metadata" (rooms store):
# bitcoin:post-100 { room: bitcoin, txid: post-100, type: post, blockHeight: 600100, seen: 1700000000000 }
# bitcoin:post-200 { room: bitcoin, txid: post-200, type: post, blockHeight: 600200, seen: 1700009999000 }
# bitcoin:addr-f { room: bitcoin, addr: bitcoincash:qaddr-f, type: follow, unfollow: false }
# bitcoin:addr-g { room: bitcoin, addr: bitcoincash:qaddr-g, type: follow, unfollow: false }
# cash:post-250 { room: cash, txid: post-250, type: post, blockHeight: 600250, seen: 1700012345000 }
# cash:addr-f { room: cash, addr: bitcoincash:qaddr-f, type: follow, unfollow: true }
# lone:addr-f { room: lone, addr: bitcoincash:qaddr-f, type: follow, unfollow: false }
Feature: Topic Metadata
Background:
Given a psf-memo-db instance with rooms, topicSummaries, and topicRecency stores
Scenario Outline: Topic Metadata - 1 GET /topics returns each topic's last-seen time
Given the fixture "topic-metadata-indexes" is loaded into the topic index stores
When the client requests /topics
Then the response contains the topic "<topic>" last seen at <lastSeen>
Examples:
| topic | lastSeen |
| memo | 1700020000000 |
| cash | 1700010000000 |
| lone | 0 |
Scenario Outline: Topic Metadata - 2 GET /topics returns each topic's follower count
Given the fixture "topic-metadata-indexes" is loaded into the topic index stores
When the client requests /topics
Then the response contains the topic "<topic>" with <followerCount> followers
Examples:
| topic | followerCount |
| memo | 12 |
| cash | 4 |
| lone | 7 |
Scenario Outline: Topic Metadata - 3 backfill records each room's last-seen time
Given the fixture "rooms-with-topic-metadata" is loaded into the rooms store
When the topic backfill utility is run
Then the topicSummaries store records the room "<room>" last seen at <lastSeen>
Examples:
| room | lastSeen |
| bitcoin | 1700009999000 |
| cash | 1700012345000 |
| lone | 0 |
Scenario Outline: Topic Metadata - 4 backfill records each room's follower count
Given the fixture "rooms-with-topic-metadata" is loaded into the rooms store
When the topic backfill utility is run
Then the topicSummaries store records the room "<room>" with <followerCount> followers
Examples:
| room | followerCount |
| bitcoin | 2 |
| cash | 0 |
| lone | 1 |
Scenario Outline: Topic Metadata - 5 backfill is idempotent for topic metadata
Given the fixture "rooms-with-topic-metadata" is loaded into the rooms store
When the topic backfill utility is run
And the topic backfill utility is run again
Then the topicSummaries store records the room "<room>" last seen at <lastSeen>
And the topicSummaries store records the room "<room>" with <followerCount> followers
Examples:
| room | lastSeen | followerCount |
| bitcoin | 1700009999000 | 2 |
| lone | 0 | 1 |
@@ -0,0 +1,78 @@
# Scenarios: Topic Metadata Indexing - 1, Topic Metadata Indexing - 2, Topic Metadata Indexing - 3, Topic Metadata Indexing - 4, Topic Metadata Indexing - 5, Topic Metadata Indexing - 6
#
# The indexer maintains lastSeen and followerCount on each room's
# topicSummaries record so the read side can report when a topic was last
# active and how many addresses follow it.
# - lastSeen is the `seen` timestamp (epoch milliseconds) of the room's most
# recent post; a follow-only room keeps lastSeen 0.
# - followerCount is the number of active follows for the room; an unfollow
# removes its address from the count.
# Reprocessing an action is idempotent, and a follow must not disturb the
# room's post count, lastHeight, or lastSeen.
Feature: Topic Metadata Indexing
Background:
Given a psf-memo-db instance with rooms, topicSummaries, and topicRecency stores
Given a psf-memo-indexer configured to write to that database
Scenario Outline: Topic Metadata Indexing - 1 a topic message records the room's last-seen time
When the indexer processes a Memo topic message <txid> in room "<room>" from <addr> at block height <height> with text "<text>" seen at <seen>
Then the topicSummaries store records the room "<room>" last seen at <lastSeen>
Examples:
| txid | room | addr | height | text | seen | lastSeen |
| topic-a1 | bitcoin | bitcoincash:qaddr-a | 600100 | hello | 1700000000000 | 1700000000000 |
| topic-b1 | cash | bitcoincash:qaddr-b | 600200 | chat | 1700009999000 | 1700009999000 |
Scenario Outline: Topic Metadata Indexing - 2 a newer topic message advances the room's last-seen time
When the indexer processes a Memo topic message <firstTxid> in room "bitcoin" from bitcoincash:qaddr-a at block height <firstHeight> with text "<firstText>" seen at <firstSeen>
And the indexer processes a Memo topic message <secondTxid> in room "bitcoin" from bitcoincash:qaddr-a at block height <secondHeight> with text "<secondText>" seen at <secondSeen>
Then the topicSummaries store records the room "bitcoin" last seen at 1700009999000
Examples:
| firstTxid | secondTxid | firstHeight | secondHeight | firstText | secondText | firstSeen | secondSeen |
| topic-a1 | topic-a2 | 600100 | 600200 | hello | again | 1700000000000 | 1700009999000 |
| topic-a3 | topic-a4 | 600200 | 600100 | later | earlier | 1700009999000 | 1700000000000 |
Scenario Outline: Topic Metadata Indexing - 3 topic follows increase the room's follower count
When the indexer processes a Memo topic follow for room "<room>" from <addr1>
And the indexer processes a Memo topic follow for room "<room>" from <addr2>
Then the topicSummaries store records the room "<room>" with 2 followers
Examples:
| room | addr1 | addr2 |
| bitcoin | bitcoincash:qaddr-a | bitcoincash:qaddr-b |
| cash | bitcoincash:qaddr-c | bitcoincash:qaddr-d |
Scenario Outline: Topic Metadata Indexing - 4 a topic unfollow decreases the room's follower count
When the indexer processes a Memo topic follow for room "<room>" from <addr1>
And the indexer processes a Memo topic follow for room "<room>" from <addr2>
And the indexer processes a Memo topic unfollow for room "<room>" from <addr1>
Then the topicSummaries store records the room "<room>" with 1 follower
Examples:
| room | addr1 | addr2 |
| bitcoin | bitcoincash:qaddr-a | bitcoincash:qaddr-b |
| cash | bitcoincash:qaddr-c | bitcoincash:qaddr-d |
Scenario Outline: Topic Metadata Indexing - 5 reprocessing a topic follow does not change the follower count
When the indexer processes a Memo topic follow for room "<room>" from <addr>
And the indexer processes the same Memo topic follow for room "<room>" from <addr> again
Then the topicSummaries store records the room "<room>" with 1 follower
Examples:
| room | addr |
| bitcoin | bitcoincash:qaddr-a |
| cash | bitcoincash:qaddr-b |
Scenario Outline: Topic Metadata Indexing - 6 a follow preserves the room's post metadata
When the indexer processes a Memo topic message <txid> in room "<room>" from <addr> at block height <height> with text "<text>" seen at <seen>
And the indexer processes a Memo topic follow for room "<room>" from <addr>
Then the topicSummaries store contains the room "<room>" with postCount 1 and lastHeight <lastHeight>
And the topicSummaries store records the room "<room>" last seen at <lastSeen>
And the topicSummaries store records the room "<room>" with 1 follower
Examples:
| txid | room | addr | height | text | seen | lastHeight | lastSeen |
| topic-d1 | bitcoin | bitcoincash:qaddr-a | 600400 | followed | 1700012345000 | 600400 | 1700012345000 |
| topic-e1 | cash | bitcoincash:qaddr-b | 600500 | tagged | 1700012999000 | 600500 | 1700012999000 |
+11 -1
View File
@@ -31,7 +31,17 @@ focus is **front-end improvements** to `psf-memo-client` (the React SPA).
## In progress
_(none)_
- **Topic metadata columns (spec approved):** add the four memo.cash topic
columns to the `/topics` page: topic name, time since the most recent post,
post count, and follower count. Requires new indexer metadata (`lastSeen`,
`followerCount`) on `topicSummaries`, `GET /topics` returning them, a
backfill for existing `rooms`, and client rendering with relative-time
formatting (`No posts` / `Less than an hour ago` / `N hours ago` /
`N days ago`). Client + indexer + DB. Specs:
`psf-memo-indexer/specs/topic-metadata-indexing.feature`,
`psf-memo-db/specs/topic-metadata.feature`,
`psf-memo-client/specs/topic-metadata-columns.feature`. Task
`topic-metadata`; spec committed, handed off to coder.
## Recently completed