diff --git a/psf-memo-client/specs/topic-pagination.feature b/psf-memo-client/specs/topic-pagination.feature index d74823d..e7751e3 100644 --- a/psf-memo-client/specs/topic-pagination.feature +++ b/psf-memo-client/specs/topic-pagination.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-17T17:25:15.930044025Z","feature_name":"Topic Pagination","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/topic-pagination.feature","background_hash":"74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Topic Pagination - 2 the topics page can load a later page","scenario_hash":"1d76d7c408593ae66f0f1551b1d583ccdceb0931d1dae35f5d02f8a0cb9636e9","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:25:15.930044025Z"}]} +# acceptance-mutation-manifest-end + # Scenarios: Topic Pagination - 1, Topic Pagination - 2 # # The topics page loads topics in pages of 50 and can move to a later page. The diff --git a/psf-memo-client/src/services/topic-discovery-page.js b/psf-memo-client/src/services/topic-discovery-page.js index a08009d..bc4083c 100644 --- a/psf-memo-client/src/services/topic-discovery-page.js +++ b/psf-memo-client/src/services/topic-discovery-page.js @@ -39,5 +39,5 @@ TopicDiscoveryPage.topicFeedPath = function (room) { module.exports = TopicDiscoveryPage // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T15:33:23.381Z","module_hash":"a238b89f4edc83d06d6a424a0f2e19307187410fe8328b5220289dab5bf18fe6","functions":[{"id":"func/TopicDiscoveryPage.constructor","name":"TopicDiscoveryPage.constructor","line":12,"end_line":16,"hash":"5e7790aac90f4000c71b5e8a4b4b15c6b2895d1315cc05741a4fa2577c9f1d61"},{"id":"func/TopicDiscoveryPage.load","name":"TopicDiscoveryPage.load","line":18,"end_line":27,"hash":"2ce74ae73560c443d862dc97c65adda83ca5cd2bcc4c0df8bb87889d99b60f1d"},{"id":"func/TopicDiscoveryPage.getTopic","name":"TopicDiscoveryPage.getTopic","line":29,"end_line":31,"hash":"7faf4b56db0bc89d32176a1741546958495ba10b5f313581f321de47e58ecb95"},{"id":"func/TopicDiscoveryPage.openTopic","name":"TopicDiscoveryPage.openTopic","line":33,"end_line":37,"hash":"ebd17b5f176c964308b69705b7a53af4c9c392e9856308b3095d69151ebde9aa"}]} +// {"version":1,"tested_at":"2026-09-17T16:44:01.309Z","module_hash":"5d9d81d1a2fa72e4365f6db50999dc28988b51dfe9c90662187ebc8dc634bba9","functions":[{"id":"func/TopicDiscoveryPage.constructor","name":"TopicDiscoveryPage.constructor","line":14,"end_line":21,"hash":"78cbee16b0d08e3f695dc65865466d12424e67d40f16683603585a48adfc2510"},{"id":"func/TopicDiscoveryPage.getTopic","name":"TopicDiscoveryPage.getTopic","line":23,"end_line":25,"hash":"7faf4b56db0bc89d32176a1741546958495ba10b5f313581f321de47e58ecb95"},{"id":"func/TopicDiscoveryPage.openTopic","name":"TopicDiscoveryPage.openTopic","line":27,"end_line":31,"hash":"ebd17b5f176c964308b69705b7a53af4c9c392e9856308b3095d69151ebde9aa"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/specs/backfill-topic-indexes.feature b/psf-memo-db/specs/backfill-topic-indexes.feature index b0352fa..99fe530 100644 --- a/psf-memo-db/specs/backfill-topic-indexes.feature +++ b/psf-memo-db/specs/backfill-topic-indexes.feature @@ -1,3 +1,8 @@ +# mutation-stamp: sha256=62b1088abcd456b0067ce18fd78f32c715254e912046c44d8d67fdd54417147f +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-17T17:23:44.068113041Z","feature_name":"Backfill topic indexes","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/backfill-topic-indexes.feature","background_hash":"386b39215fd2ac58af1a095ce21b3e5a94c26a052be33ebb653986f56d1cdaf5","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Backfill topic indexes - 1 backfill summarizes every room","scenario_hash":"98e2a49f35bf2c57825af892d343b091df3b8ea9938d88877112a244195e2b6a","mutation_count":9,"result":{"Total":9,"Killed":9,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"},{"index":1,"name":"Backfill topic indexes - 2 backfill builds the recency index","scenario_hash":"6210a596d0011bd42ab984430a01e639932e19bb99dd05e6b79c0e3d8e50ccdc","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"},{"index":2,"name":"Backfill topic indexes - 3 backfill is idempotent","scenario_hash":"93b5d81880c0f9bf768aa363353607761c7cebd4d6b1c6cef52eae0f40fd3cc0","mutation_count":6,"result":{"Total":6,"Killed":6,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:23:44.068113041Z"}]} +# acceptance-mutation-manifest-end + # Scenarios: Backfill topic indexes - 1, Backfill topic indexes - 2, Backfill topic indexes - 3 # # The topic backfill utility builds topicSummaries and topicRecency from the diff --git a/psf-memo-db/specs/topic-pagination.feature b/psf-memo-db/specs/topic-pagination.feature index 2f1430a..1f205ae 100644 --- a/psf-memo-db/specs/topic-pagination.feature +++ b/psf-memo-db/specs/topic-pagination.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-17T17:22:41.386110241Z","feature_name":"Topic Pagination","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-pagination.feature","background_hash":"4a01ae658dda9a91b7f19edbeb3e657bef1d666b5355b8dd30496013bed35d7e","implementation_hash":"unknown","scenarios":[]} +# acceptance-mutation-manifest-end + # Scenarios: Topic Pagination - 1, Topic Pagination - 2, Topic Pagination - 3 # # GET /topics returns a page of topics ordered by their most recent post's diff --git a/psf-memo-db/specs/topic-read.feature b/psf-memo-db/specs/topic-read.feature index 38f35f1..6297873 100644 --- a/psf-memo-db/specs/topic-read.feature +++ b/psf-memo-db/specs/topic-read.feature @@ -1,5 +1,5 @@ # acceptance-mutation-manifest-begin -# {"version":1,"tested_at":"2026-09-04T14:07:09.030166090Z","feature_name":"Topic Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-read.feature","background_hash":"922fd45b8606f723073833f0aaca0e6d92b03a263a6b54c60490843e0ac41cc0","implementation_hash":"unknown","scenarios":[{"index":4,"name":"Topic Read - 5 GET /topics orders topics by most recent post","scenario_hash":"fcb48583a39be06c616d4d1636d06ac7e985a16fa7d9f66d9c9c43e95639812c","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-09-04T14:07:09.030166090Z"},{"index":0,"name":"Topic Read - 1 GET /topics lists distinct topics with their post counts","scenario_hash":"a425a0cbf18c7e7fcb7d01c3571556d4c184b013ad7990e724d95d8d100eebb9","mutation_count":8,"result":{"Total":8,"Killed":8,"Survived":0,"Errors":0},"tested_at":"2026-08-28T15:39:33.364455310Z"},{"index":1,"name":"Topic Read - 2 GET /topics/:room/posts returns the posts for a topic sorted by block height descending","scenario_hash":"cffb702052b0df29aa87510e0c45dd01b83c4607e7ce915bc0e660d601f79cce","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-28T15:39:33.364455310Z"}]} +# {"version":1,"tested_at":"2026-09-17T17:24:59.147244904Z","feature_name":"Topic Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/topic-read.feature","background_hash":"1a085ee8fae67e6f4b1217ee65d86b03af1088ca1a468da8a9edef83683332b3","implementation_hash":"unknown","scenarios":[{"index":0,"name":"Topic Read - 1 GET /topics lists distinct topics with their post counts","scenario_hash":"a425a0cbf18c7e7fcb7d01c3571556d4c184b013ad7990e724d95d8d100eebb9","mutation_count":8,"result":{"Total":8,"Killed":8,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"},{"index":1,"name":"Topic Read - 2 GET /topics/:room/posts returns the posts for a topic sorted by block height descending","scenario_hash":"cffb702052b0df29aa87510e0c45dd01b83c4607e7ce915bc0e660d601f79cce","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"},{"index":4,"name":"Topic Read - 5 GET /topics orders topics by most recent post","scenario_hash":"fcb48583a39be06c616d4d1636d06ac7e985a16fa7d9f66d9c9c43e95639812c","mutation_count":1,"result":{"Total":1,"Killed":1,"Survived":0,"Errors":0},"tested_at":"2026-09-17T17:24:59.147244904Z"}]} # acceptance-mutation-manifest-end # Scenarios: Topic Read - 1, Topic Read - 2, Topic Read - 3, Topic Read - 4, Topic Read - 5 diff --git a/psf-memo-db/src/adapters/topic-query.js b/psf-memo-db/src/adapters/topic-query.js index 73983ca..d48cb49 100644 --- a/psf-memo-db/src/adapters/topic-query.js +++ b/psf-memo-db/src/adapters/topic-query.js @@ -170,5 +170,5 @@ class TopicQuery { export default TopicQuery // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-09-04T20:12:29.631Z","module_hash":"c839533977040f56c6f45a1aa1e64ee422567570556c6beac19fe72d0c5970f0","functions":[{"id":"func/TopicQuery.constructor","name":"TopicQuery.constructor","line":16,"end_line":35,"hash":"56e5538d08449cf5870754c0e1e440b7d7e6a90ae5320b678fee77b18abcb81a"},{"id":"func/TopicQuery.roomFromKey","name":"TopicQuery.roomFromKey","line":37,"end_line":40,"hash":"4175916ac2bb8f9f102c70750f336ed980db2a30a1f8f35645534303408aaece"},{"id":"func/TopicQuery.txidFromKey","name":"TopicQuery.txidFromKey","line":42,"end_line":45,"hash":"4f5f5c456f1e74a60acff54d1d5e2d982be53886c2a77c6eff3293133efa9c87"},{"id":"func/TopicQuery.listTopics","name":"TopicQuery.listTopics","line":47,"end_line":76,"hash":"b96bd821ff6eb1e14abe4418004f74418d3276666ca185c544c90d2095b08474"},{"id":"func/TopicQuery.getTopicPostTxids","name":"TopicQuery.getTopicPostTxids","line":78,"end_line":98,"hash":"943729090c4532333410ee001c75ecdd45a562dfffd6f1f6d5e9313489f1b76a"},{"id":"func/TopicQuery.isFollowingRoom","name":"TopicQuery.isFollowingRoom","line":101,"end_line":110,"hash":"a0cf48cc6f4842adf65830b41d536cd6a078c7adf6b837e57259491157e2e97d"},{"id":"func/TopicQuery.listRoomFollowers","name":"TopicQuery.listRoomFollowers","line":113,"end_line":124,"hash":"351d71881e8b0953366056f223a6f06326e8ecd946277abc372ae5771c0b0f08"},{"id":"func/TopicQuery.followAddrFromValue","name":"TopicQuery.followAddrFromValue","line":126,"end_line":130,"hash":"2ce68c35f539e1fd7694774059b4b2afd096014c99a87c7538f3cbb70e26c3c7"}]} +// {"version":1,"tested_at":"2026-09-17T17:27:18.416Z","module_hash":"a31f16e3a18a841a71910ab28019ded60ef8800bc08dcd5460dfb21a84194903","functions":[{"id":"func/TopicQuery.constructor","name":"TopicQuery.constructor","line":24,"end_line":52,"hash":"51f22d20b4d0bb817f3b005e12f1635a3669a77bd741c3f0b386bc71fa28c08e"},{"id":"func/TopicQuery.roomFromKey","name":"TopicQuery.roomFromKey","line":54,"end_line":57,"hash":"4175916ac2bb8f9f102c70750f336ed980db2a30a1f8f35645534303408aaece"},{"id":"func/TopicQuery.txidFromKey","name":"TopicQuery.txidFromKey","line":59,"end_line":62,"hash":"4f5f5c456f1e74a60acff54d1d5e2d982be53886c2a77c6eff3293133efa9c87"},{"id":"func/TopicQuery.roomRange","name":"TopicQuery.roomRange","line":67,"end_line":69,"hash":"ceb99a638ee22ce625c9962d90786bd779de8d946b501640db9b76276c8bd7ea"},{"id":"func/TopicQuery.summaryRoom","name":"TopicQuery.summaryRoom","line":73,"end_line":76,"hash":"8ca2c04ae70839ca8e2deebe6f0661883dacbf3a06731dd9c134f326bf921b7c"},{"id":"func/TopicQuery.recencyRoom","name":"TopicQuery.recencyRoom","line":80,"end_line":84,"hash":"677abdcb0687362d170da432571cb0912bc43d4bd6e7022e3d785714f8bd9402"},{"id":"func/TopicQuery.listTopics","name":"TopicQuery.listTopics","line":90,"end_line":117,"hash":"75cb4c5ee90a44a156119f08226d0d3a8a75c136568676cdbd484c3f1e55a943"},{"id":"func/TopicQuery.getTopicPostTxids","name":"TopicQuery.getTopicPostTxids","line":119,"end_line":137,"hash":"c18e8ca08fa3695cc2d49bbc0a83eb5488831bf14ddfdb9c421afefd4b91d521"},{"id":"func/TopicQuery.isFollowingRoom","name":"TopicQuery.isFollowingRoom","line":140,"end_line":149,"hash":"a0cf48cc6f4842adf65830b41d536cd6a078c7adf6b837e57259491157e2e97d"},{"id":"func/TopicQuery.listRoomFollowers","name":"TopicQuery.listRoomFollowers","line":152,"end_line":161,"hash":"40dc014f433895e930bfec5de78d73ca2f75a023cecb174ddc57f7ec150dd300"},{"id":"func/TopicQuery.followAddrFromValue","name":"TopicQuery.followAddrFromValue","line":163,"end_line":167,"hash":"2ce68c35f539e1fd7694774059b4b2afd096014c99a87c7538f3cbb70e26c3c7"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/controllers/rest-api/topics/controller.js b/psf-memo-db/src/controllers/rest-api/topics/controller.js index 9abf50b..93e22ef 100644 --- a/psf-memo-db/src/controllers/rest-api/topics/controller.js +++ b/psf-memo-db/src/controllers/rest-api/topics/controller.js @@ -150,5 +150,5 @@ class TopicsRESTControllerLib { export default TopicsRESTControllerLib // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T19:53:31.127Z","module_hash":"4c3f3e9152899ed272cb583e558c4522c0f54c37a0ae728f3ec6bc40248a3fdc","functions":[{"id":"func/TopicsRESTControllerLib.constructor","name":"TopicsRESTControllerLib.constructor","line":8,"end_line":23,"hash":"bacd230f3777dda39ec134a69a9a7c1801cb3cd09c0cc86659df324540053028"},{"id":"func/TopicsRESTControllerLib.handleError","name":"TopicsRESTControllerLib.handleError","line":25,"end_line":32,"hash":"b9ba0c7b9752ac2fda3cf058a983a8b6708715e2e4dc882903ab5ef369956e03"},{"id":"func/TopicsRESTControllerLib.getTopics","name":"TopicsRESTControllerLib.getTopics","line":49,"end_line":55,"hash":"b89a696bcd1d601ef86754974dbf181baf2ece7d592674ba4fd87887725d6fa8"},{"id":"func/TopicsRESTControllerLib.getTopicPosts","name":"TopicsRESTControllerLib.getTopicPosts","line":82,"end_line":90,"hash":"8ad5f9b1749968c3fbfea95683bb388a5d99eda0b8fe7a4dce3447d6d47e6dba"},{"id":"func/TopicsRESTControllerLib.getTopicFollowState","name":"TopicsRESTControllerLib.getTopicFollowState","line":110,"end_line":118,"hash":"6d72fecc2bf46276cd00d39cc1c19ed6647beed1f2f3ba187a2c085ed4308611"},{"id":"func/TopicsRESTControllerLib.getTopicFollowers","name":"TopicsRESTControllerLib.getTopicFollowers","line":136,"end_line":143,"hash":"2bcd22520d962c008a3255f5cf6cf536fdeb738d036d3c6b6d61d5d5cf040615"}]} +// {"version":1,"tested_at":"2026-09-17T16:42:30.815Z","module_hash":"50c5f72fa562d8c8f2c0ce8bed281a727b7effd3effbf92f8c4d573003b7b689","functions":[{"id":"func/TopicsRESTControllerLib.constructor","name":"TopicsRESTControllerLib.constructor","line":8,"end_line":23,"hash":"bacd230f3777dda39ec134a69a9a7c1801cb3cd09c0cc86659df324540053028"},{"id":"func/TopicsRESTControllerLib.handleError","name":"TopicsRESTControllerLib.handleError","line":25,"end_line":27,"hash":"801609a06e8f034702fb7293f5c7b0d316a54dfac1921d934e248a3b0a11f58c"},{"id":"func/TopicsRESTControllerLib.getTopics","name":"TopicsRESTControllerLib.getTopics","line":49,"end_line":56,"hash":"e2cf9b957874c42489d8c50b1d371f8191ce94d1002bca4a5ee7d6333d278d11"},{"id":"func/TopicsRESTControllerLib.getTopicPosts","name":"TopicsRESTControllerLib.getTopicPosts","line":84,"end_line":94,"hash":"bc18f51006b1c1079a0584b75d6666bec0b299febdcd565aab071679a2f1fe00"},{"id":"func/TopicsRESTControllerLib.getTopicFollowState","name":"TopicsRESTControllerLib.getTopicFollowState","line":114,"end_line":122,"hash":"6d72fecc2bf46276cd00d39cc1c19ed6647beed1f2f3ba187a2c085ed4308611"},{"id":"func/TopicsRESTControllerLib.getTopicFollowers","name":"TopicsRESTControllerLib.getTopicFollowers","line":140,"end_line":147,"hash":"2bcd22520d962c008a3255f5cf6cf536fdeb738d036d3c6b6d61d5d5cf040615"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-db/src/lib/backfill-topic-indexes.js b/psf-memo-db/src/lib/backfill-topic-indexes.js index ffdc253..9f37036 100644 --- a/psf-memo-db/src/lib/backfill-topic-indexes.js +++ b/psf-memo-db/src/lib/backfill-topic-indexes.js @@ -85,3 +85,7 @@ export async function backfillTopicIndexes ({ roomsDb, topicSummariesDb, topicRe return { rooms: summaries.size } } + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-17T16:40:51.620Z","module_hash":"7a67d2c6ca7ea2494b1ff3a6dcc703c47a0dfd341fe2d593750b82b810f36da1","functions":[{"id":"func/topicRecencyKey","name":"topicRecencyKey","line":26,"end_line":29,"hash":"a034a6023f72d08e40d28bb51e41e370c1a42d5682315b162a7c2a81f7f31310"},{"id":"func/roomFromEntry","name":"roomFromEntry","line":33,"end_line":36,"hash":"5bf2a8338a623f65191729bc2433a87869b085b29547fbb5e42ebaa67d6473c1"},{"id":"func/applyPost","name":"applyPost","line":39,"end_line":43,"hash":"4edf9dd98a5889c62fd60f4107563427f0a55dc0250c27f6ac0af49b6b934c42"},{"id":"func/collectSummaries","name":"collectSummaries","line":45,"end_line":60,"hash":"73a07286477939379fb82ceace71991aa0cd830f041897dbe4cb5748c091a93f"},{"id":"func/removeStale","name":"removeStale","line":63,"end_line":69,"hash":"09cc44be72b9f955722f17c117d8ca18fe953dcf2cbd6efab110798dce4d0417"},{"id":"func/backfillTopicIndexes","name":"backfillTopicIndexes","line":71,"end_line":87,"hash":"7a6c1207032b16c5e7fc90fe9f7e2543b22d126fc0924296573c911dbf21d8c4"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/list-topics.js b/psf-memo-db/src/use-cases/list-topics.js index b19214d..06f7f69 100644 --- a/psf-memo-db/src/use-cases/list-topics.js +++ b/psf-memo-db/src/use-cases/list-topics.js @@ -20,5 +20,5 @@ class ListTopics extends ListUseCase { export default ListTopics // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T15:09:18.069Z","module_hash":"4d7755c0ac3ab79ea014862e6c728fde8af6e76c65046c9691f81ecd00993734","functions":[{"id":"func/ListTopics.constructor","name":"ListTopics.constructor","line":8,"end_line":10,"hash":"0e6872a32236c0cb076d7a728a37d8a40f2aca9bada0395d502cd3dba466c273"},{"id":"func/ListTopics.execute","name":"ListTopics.execute","line":12,"end_line":15,"hash":"c41d34757567d55659cc3a8ba77730a34478dfff9a95af00868aa8c5b2db159a"}]} +// {"version":1,"tested_at":"2026-09-17T16:41:57.476Z","module_hash":"07fb13faa0cead7ef6f073eab4f16dd74ece5eb290c37b306827735fa154320d","functions":[{"id":"func/ListTopics.constructor","name":"ListTopics.constructor","line":9,"end_line":11,"hash":"0e6872a32236c0cb076d7a728a37d8a40f2aca9bada0395d502cd3dba466c273"},{"id":"func/ListTopics.execute","name":"ListTopics.execute","line":13,"end_line":17,"hash":"b54242b912b2b499a9e0023a2428c193c0a2c9989e4b3187d61ad76c8e31ec47"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-indexer/specs/topic-recency-indexing.feature b/psf-memo-indexer/specs/topic-recency-indexing.feature index 154c232..4c6a434 100644 --- a/psf-memo-indexer/specs/topic-recency-indexing.feature +++ b/psf-memo-indexer/specs/topic-recency-indexing.feature @@ -1,3 +1,7 @@ +# acceptance-mutation-manifest-begin +# {"version":1,"tested_at":"2026-09-17T16:45:30.497089416Z","feature_name":"Topic Recency Indexing","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-indexer/specs/topic-recency-indexing.feature","background_hash":"0639f3e7176ddd178bf266b5f62c66fcfff9acda7487916f3496de2a3dd86e3f","implementation_hash":"unknown","scenarios":[]} +# acceptance-mutation-manifest-end + # Scenarios: Topic Recency Indexing - 1, Topic Recency Indexing - 2, Topic Recency Indexing - 3, Topic Recency Indexing - 4, Topic Recency Indexing - 5 # # The indexer maintains two topic indexes so the read side can list topics by diff --git a/psf-memo-indexer/src/use-cases/action-types/helpers.js b/psf-memo-indexer/src/use-cases/action-types/helpers.js index f1602d3..c3f579b 100644 --- a/psf-memo-indexer/src/use-cases/action-types/helpers.js +++ b/psf-memo-indexer/src/use-cases/action-types/helpers.js @@ -105,5 +105,5 @@ export function postChildKey (parentTxid, childTxid) { } // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-28T04:22:49.741Z","module_hash":"9799b30443df95c47881bcc6a74bf9024e65741f94938af1c92dd5fa0180005f","functions":[{"id":"func/logProcessError","name":"logProcessError","line":7,"end_line":13,"hash":"78f39be271917cad97072ed5e2d1f531a30a37bc9c10169516e0457951c438f4"},{"id":"func/utf8FromPush","name":"utf8FromPush","line":15,"end_line":17,"hash":"356fc665a6389e392ec83522210dd95b2af6be341d00dc5edc0bbabfbd511d9e"},{"id":"func/stripLeadingEmptyPushes","name":"stripLeadingEmptyPushes","line":22,"end_line":28,"hash":"be7c401e07e4ef3670428afe7211860038c69da17abcd03508ea5cb640b46163"},{"id":"func/normalizeTwoPushMemoDatas","name":"normalizeTwoPushMemoDatas","line":34,"end_line":46,"hash":"61d050380c2cef7eed8870a66e347f1d01c56246d733d95d692b436cfb4a20ed"},{"id":"func/txHashFromPush","name":"txHashFromPush","line":48,"end_line":55,"hash":"704934308365cf0fbbb72aa437faf5db20a69bd57a16ba37b19c905ddc922ea9"},{"id":"func/followKey","name":"followKey","line":57,"end_line":59,"hash":"e1c5cfdd78afd3945e18e4af06e443efb306c27b07e3b3534c918ef000da9696"},{"id":"func/roomKey","name":"roomKey","line":61,"end_line":63,"hash":"d4f146c7a938bb6b30b13f071609a3ac4e96a26f116e321b804c16114884cd15"},{"id":"func/postHeightKey","name":"postHeightKey","line":65,"end_line":68,"hash":"25bdb7995aeb2823d0f808812251c3fd8c163089ea32b4f39ddf55520e03cbb1"},{"id":"func/addrPostHeightKey","name":"addrPostHeightKey","line":70,"end_line":73,"hash":"b9b6b08e4c5892905dc6577ad22dd48c13598ca205b1b8c13d7cc9622962879f"},{"id":"func/postLikeKey","name":"postLikeKey","line":75,"end_line":77,"hash":"627ce4b3ca6a9d28fbaa50bc1f51973509e7d8b52eda9208343dac1d936d7f51"},{"id":"func/postChildKey","name":"postChildKey","line":79,"end_line":81,"hash":"4d30329db33e34b0461bd1ba70b078fbef285a338f17f7e4d1166ca5bd107893"}]} +// {"version":1,"tested_at":"2026-09-17T16:25:37.308Z","module_hash":"00fa2f64726246ec4e403d93b7cabaf0fb5e2753614533c0b3651b23d6147749","functions":[{"id":"func/logProcessError","name":"logProcessError","line":7,"end_line":13,"hash":"78f39be271917cad97072ed5e2d1f531a30a37bc9c10169516e0457951c438f4"},{"id":"func/utf8FromPush","name":"utf8FromPush","line":15,"end_line":17,"hash":"356fc665a6389e392ec83522210dd95b2af6be341d00dc5edc0bbabfbd511d9e"},{"id":"func/stripLeadingEmptyPushes","name":"stripLeadingEmptyPushes","line":22,"end_line":28,"hash":"be7c401e07e4ef3670428afe7211860038c69da17abcd03508ea5cb640b46163"},{"id":"func/normalizeTwoPushMemoDatas","name":"normalizeTwoPushMemoDatas","line":34,"end_line":46,"hash":"61d050380c2cef7eed8870a66e347f1d01c56246d733d95d692b436cfb4a20ed"},{"id":"func/txHashFromPush","name":"txHashFromPush","line":48,"end_line":55,"hash":"704934308365cf0fbbb72aa437faf5db20a69bd57a16ba37b19c905ddc922ea9"},{"id":"func/followKey","name":"followKey","line":57,"end_line":59,"hash":"e1c5cfdd78afd3945e18e4af06e443efb306c27b07e3b3534c918ef000da9696"},{"id":"func/roomKey","name":"roomKey","line":61,"end_line":63,"hash":"d4f146c7a938bb6b30b13f071609a3ac4e96a26f116e321b804c16114884cd15"},{"id":"func/topicRecencyKey","name":"topicRecencyKey","line":68,"end_line":71,"hash":"ce7146b8dfca80309058c1512818fe2edc9f1b3658cbe1e06aabf78f65136b90"},{"id":"func/isNotFound","name":"isNotFound","line":75,"end_line":77,"hash":"662978032673c9bac6d64c519899ebfed72e477ea39b9fde19039ffa01de183d"},{"id":"func/getIfPresent","name":"getIfPresent","line":80,"end_line":87,"hash":"b012b3ef1d48f45a65fcbd4ff9574443cc98d2c2f7d12a504af74fc3e0d3f5af"},{"id":"func/postHeightKey","name":"postHeightKey","line":89,"end_line":92,"hash":"25bdb7995aeb2823d0f808812251c3fd8c163089ea32b4f39ddf55520e03cbb1"},{"id":"func/addrPostHeightKey","name":"addrPostHeightKey","line":94,"end_line":97,"hash":"b9b6b08e4c5892905dc6577ad22dd48c13598ca205b1b8c13d7cc9622962879f"},{"id":"func/postLikeKey","name":"postLikeKey","line":99,"end_line":101,"hash":"627ce4b3ca6a9d28fbaa50bc1f51973509e7d8b52eda9208343dac1d936d7f51"},{"id":"func/postChildKey","name":"postChildKey","line":103,"end_line":105,"hash":"4d30329db33e34b0461bd1ba70b078fbef285a338f17f7e4d1166ca5bd107893"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-indexer/src/use-cases/action-types/topic-follow.js b/psf-memo-indexer/src/use-cases/action-types/topic-follow.js index 67f7bf5..a6902d8 100644 --- a/psf-memo-indexer/src/use-cases/action-types/topic-follow.js +++ b/psf-memo-indexer/src/use-cases/action-types/topic-follow.js @@ -28,3 +28,7 @@ export async function handleTopicFollow (ctx) { await ensureTopicRoom(adapters, room) } } + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-17T16:34:56.427Z","module_hash":"98bbf2e83e9afae81b2a25b3b03d66a32c9af7864e8eace4aec99b16dfe773b5","functions":[{"id":"func/handleTopicFollow","name":"handleTopicFollow","line":5,"end_line":30,"hash":"6137b8d03c0bcf24ddd08824e462565407677303981b1a376b94e449897e217a"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-indexer/src/use-cases/action-types/topic-indexing.js b/psf-memo-indexer/src/use-cases/action-types/topic-indexing.js index 8542d66..6742130 100644 --- a/psf-memo-indexer/src/use-cases/action-types/topic-indexing.js +++ b/psf-memo-indexer/src/use-cases/action-types/topic-indexing.js @@ -48,3 +48,7 @@ export async function ensureTopicRoom (adapters, room) { await adapters.topicSummaryDb.update(room, { room, postCount: 0, lastHeight: 0 }) await adapters.topicRecencyDb.update(topicRecencyKey(0, room), { room, blockHeight: 0 }) } + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-17T16:22:46.190Z","module_hash":"cdd90074000f9eddb01ce298351d75ae8afdecc8fe34711a0a415099d64a7e70","functions":[{"id":"func/recordTopicPost","name":"recordTopicPost","line":19,"end_line":40,"hash":"91b542cddca534356bcaf15426ec2abccd04d610cf8db4c21de7deb216b250d8"},{"id":"func/ensureTopicRoom","name":"ensureTopicRoom","line":44,"end_line":50,"hash":"ca3cb6538d16deea7546a5bbf1a24a752f803a8929b7da8c3b8ba9582a20816a"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-indexer/src/use-cases/action-types/topic-message.js b/psf-memo-indexer/src/use-cases/action-types/topic-message.js index edb492c..be5f757 100644 --- a/psf-memo-indexer/src/use-cases/action-types/topic-message.js +++ b/psf-memo-indexer/src/use-cases/action-types/topic-message.js @@ -36,3 +36,7 @@ export async function handleTopicMessage (ctx) { await recordTopicPost(adapters, room, blockHeight) } } + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-17T16:35:45.388Z","module_hash":"3bf8a10d51b15167c500fd9d0586eedffb4b6f2191b6f563e9f9b15c75e850c0","functions":[{"id":"func/handleTopicMessage","name":"handleTopicMessage","line":6,"end_line":38,"hash":"d58559b106ed4f846feabd05070886a2962d95bbe55f2438e8d3f42b0f26a5ed"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-indexer/test/unit/use-cases/action-types/helpers.unit.js b/psf-memo-indexer/test/unit/use-cases/action-types/helpers.unit.js index e6e6040..877dce1 100644 --- a/psf-memo-indexer/test/unit/use-cases/action-types/helpers.unit.js +++ b/psf-memo-indexer/test/unit/use-cases/action-types/helpers.unit.js @@ -4,7 +4,9 @@ import { normalizeTwoPushMemoDatas, stripLeadingEmptyPushes, txHashFromPush, - logProcessError + logProcessError, + topicRecencyKey, + isNotFound } from '../../../../src/use-cases/action-types/helpers.js' import { PREFIX_SET_PROFILE_PIC, PREFIX_POST } from '../../../../src/lib/memo-codes.js' @@ -120,4 +122,22 @@ describe('#action-types/helpers', () => { assert.equal(create.firstCall.args[1].blockHeight, 600100) }) }) + + describe('#topicRecencyKey', () => { + it('should encode an inverted, zero-padded height with the room name', () => { + assert.equal(topicRecencyKey(0, 'bitcoin'), '999999999999:bitcoin') + assert.equal(topicRecencyKey(600200, 'bitcoin'), '999999399799:bitcoin') + assert.equal(topicRecencyKey(undefined, 'dev'), '999999999999:dev') + }) + }) + + describe('#isNotFound', () => { + it('should recognize not-found errors and reject everything else', () => { + assert.isTrue(isNotFound({ notFound: true })) + assert.isTrue(isNotFound({ code: 'LEVEL_NOT_FOUND' })) + assert.isTrue(isNotFound({ response: { status: 404 } })) + assert.isFalse(isNotFound({})) + assert.isFalse(isNotFound(null)) + }) + }) }) diff --git a/psf-memo-indexer/test/unit/use-cases/action-types/topic-indexing.unit.js b/psf-memo-indexer/test/unit/use-cases/action-types/topic-indexing.unit.js new file mode 100644 index 0000000..ed9dee7 --- /dev/null +++ b/psf-memo-indexer/test/unit/use-cases/action-types/topic-indexing.unit.js @@ -0,0 +1,85 @@ +import { assert } from 'chai' +import { recordTopicPost } from '../../../../src/use-cases/action-types/topic-indexing.js' +import { topicRecencyKey } from '../../../../src/use-cases/action-types/helpers.js' + +function makeDb () { + const store = new Map() + return { + store, + async get (key) { + if (!store.has(key)) { + const err = new Error('not found') + err.notFound = true + throw err + } + return store.get(key) + }, + async update (key, value) { + store.set(key, value) + }, + async delete (key) { + store.delete(key) + } + } +} + +function makeAdapters () { + return { + topicSummaryDb: makeDb(), + topicRecencyDb: makeDb() + } +} + +// These cover the defensive height fallbacks in recordTopicPost. The system +// always writes a numeric lastHeight, but a missing blockHeight or a legacy +// summary without lastHeight must still land at height zero. +describe('#recordTopicPost height fallbacks', () => { + it('should treat a missing block height as height zero', async () => { + const adapters = makeAdapters() + + await recordTopicPost(adapters, 'bitcoin', undefined) + + assert.deepEqual(adapters.topicSummaryDb.store.get('bitcoin'), { + room: 'bitcoin', + postCount: 1, + lastHeight: 0 + }) + assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(0, 'bitcoin')), { + room: 'bitcoin', + blockHeight: 0 + }) + }) + + it('should delete the zero-height recency record when a legacy summary has no lastHeight', async () => { + const adapters = makeAdapters() + adapters.topicSummaryDb.store.set('bitcoin', { room: 'bitcoin', postCount: 2 }) + adapters.topicRecencyDb.store.set(topicRecencyKey(0, 'bitcoin'), { room: 'bitcoin', blockHeight: 0 }) + + await recordTopicPost(adapters, 'bitcoin', 600100) + + assert.isFalse(adapters.topicRecencyDb.store.has(topicRecencyKey(0, 'bitcoin'))) + assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(600100, 'bitcoin')), { + room: 'bitcoin', + blockHeight: 600100 + }) + assert.deepEqual(adapters.topicSummaryDb.store.get('bitcoin'), { + room: 'bitcoin', + postCount: 3, + lastHeight: 600100 + }) + }) + + it('should delete the stale recency record when the next height equals the legacy fallback', async () => { + const adapters = makeAdapters() + adapters.topicSummaryDb.store.set('bitcoin', { room: 'bitcoin', postCount: 2 }) + adapters.topicRecencyDb.store.set(topicRecencyKey(0, 'bitcoin'), { room: 'bitcoin', blockHeight: 0 }) + + await recordTopicPost(adapters, 'bitcoin', 1) + + assert.isFalse(adapters.topicRecencyDb.store.has(topicRecencyKey(0, 'bitcoin'))) + assert.deepEqual(adapters.topicRecencyDb.store.get(topicRecencyKey(1, 'bitcoin')), { + room: 'bitcoin', + blockHeight: 1 + }) + }) +}) diff --git a/psf-memo-indexer/test/unit/use-cases/action-types/topic-message.unit.js b/psf-memo-indexer/test/unit/use-cases/action-types/topic-message.unit.js index b189123..1271aef 100644 --- a/psf-memo-indexer/test/unit/use-cases/action-types/topic-message.unit.js +++ b/psf-memo-indexer/test/unit/use-cases/action-types/topic-message.unit.js @@ -1,6 +1,7 @@ import { assert } from 'chai' import { handleTopicMessage } from '../../../../src/use-cases/action-types/topic-message.js' import { topicRecencyKey } from '../../../../src/use-cases/action-types/helpers.js' +import { MAX_POST_SIZE } from '../../../../src/lib/memo-codes.js' function makeDb () { const store = new Map() @@ -155,6 +156,27 @@ describe('#handleTopicMessage topic indexes', () => { assert.equal(adapters.topicRecencyDb.store.size, 0) }) + it('should index a topic message with room and message exactly at the maximum size', async () => { + const adapters = makeAdapters() + const room = 'bitcoin' + const message = 'x'.repeat(MAX_POST_SIZE - room.length) + + await processTopicMessage(adapters, { + txid: 'topic-max', + room, + addr: 'bitcoincash:qaddr-a', + height: 600100, + text: message + }) + + assert.equal(adapters.processErrorDb.store.size, 0) + assert.deepEqual(adapters.topicSummaryDb.store.get(room), { + room, + postCount: 1, + lastHeight: 600100 + }) + }) + it('should log a process error and index nothing for an oversized topic message', async () => { const adapters = makeAdapters() const prefix = Buffer.from('6d0c', 'hex')