grain-serve serves the gallery feeds, named by URI
MergedThis adds social.grain.feed.getFeed and describeFeeds, with the nine feeds ported from hatk. It also turns the plan check into a check of the SQL the code actually builds.
I read read/feed.rs, location.rs, foryou.rs and camera.rs in full. I did not read the tests or the rest of the diff. CI had not finished when I looked (test, rust, check and harness were still queued or running), so I can't say it passes.
I found nothing that must change.
- H3 ranges: I checked the bit arithmetic in
cells(). The child digits run 0 to 6, the unused digits stay 7, and the same-length lowercase hex strings makeBETWEENcompare correctly. - Cursors and sorting: The
(key, tie) < (?2, ?3)cursor and its tie-breaker are sound. - Auth: Following and For You require a signed-in viewer.
- Camera name port: The byte-based
striponly slices at ASCII match boundaries, so it won't panic on non-ASCII input.
Non-blocking: the Camera feed runs SELECT DISTINCT make || ' ' || model FROM photo_exif and cleans every row on each request. That is a full scan that grows with the index. If it shows up in profiling, cache the cleaned-name map or store the cleaned name at index time.
- grain-serve serves the gallery feeds, named by URI
- main
Stacked on grain-serve answers profiles, galleries, comments, follows and stories (
read/views): its commits are this branch's first, and this pull's own change is its last commit. Merge that one first.What changes
social.grain.feed.getFeedanddescribeFeeds(read/feed.rs). A feed is named byat://<the server's DID>/social.grain.feed.generator/<name>, asapp.bsky.feed.getFeednames a generator.describeFeedslists the nine with their labels. A URI naming no feed, or another server's, is 400UnknownFeed; a server with no DID is 501. Each page is hydrated byviews::galleries_at, from the previous pull.The nine feeds, ported query for query from
server/feeds:feed::seencovers that: an account shown, no hiding label from grain's labeler or the author, at least one photo, and no block either way or mute by the viewer. They sort bysort_at, the earlier ofcreatedAtand when the index first held the record, as hatk'sgalleryFeedTabledoes.createdAt.read/camera.rs) reads the camera through a port ofcleanCameraName, so the raw EXIF make and model and the cleaned name find the same galleries. The port is hand-written; the workspace has no regex crate, and the seven rules need none.read/location.rs) reads a place's label every way hatk did (three, two or one parts, withexpandCountryAliasesasplace::spellings), plus the place's own name. Where the label finds nothing it falls back to thelocationcell. A city cell (resolution 5) takes in every cell within it.read/foryou.rs) keeps hatk's steps and weights: seeds, co-favoriters who got there first or within a day after, path scores, smoothing, a six-hour half-life and the popularity penalty. With nothing to go on it falls back to the month's most favorited. The time sums are done with SQLite'sjulianday.Lexicon.
getFeeddeclaresname, the place label the web already sends to the Location feed.The plan check reads what runs. The hand-written feed and hydration queries in
index::tests::the_feeds_read_by_indexare gone.read::tests::the_reads_read_by_indexnow checks the SQL each feed and each view builds, taken from the functions that build it. The views' SQL is lifted into named consts and functions for that. What is left inindex::tests(renamedthe_queries_still_to_port_read_by_index) is the queries of methods not yet ported.Why
The second read step of the plan, after views (#1) and before search (#3). Where hatk and Bluesky part, this follows Bluesky:
AuthenticationRequiredwithout one), as a timeline is. hatk took the account from theactorparameter, so anyone could ask for anyone's.<sort key>::<uri>cursor, not hatk's base64 of the same. For You keeps an offset, since a ranking has no key to resume from. A malformed cursor is 400.cellToParenton each in JavaScript, then paged by finding the cursor's URI in the result. H3 numbers a cell's descendants at one resolution in one run, its own digits then the finer ones from 0 to 6. So the galleries within a city are ten ranges ofgallery.h3(resolutions 6 to 15) plus the cell itself, read ongallery_h3. The ranges were checked against h3-js'scellToChildren.How it was checked
tests/read.rsgains a test a feed. Each puts fixtures into the index and callsgetFeedthroughtests/common, soanswers.mjsholds every page togalleryView. The tests assert:Unit tests cover
cleanCameraName's documented examples, the H3 ranges, the label readings and country spellings. The plan check covers every feed's SQL.Nothing was built or run on this machine, except the H3 and camera-name expectations, worked out with h3-js and the TypeScript
cleanCameraName. CI runs fmt, clippy, the tests, the coverage gate and the answers check.Shortcomings
social.grain.feed.generator, is a choice: no lexicon defines a record by that name. It is only ever a name here.json_extractover the galleries insort_atorder, as hatk's did. It walks an index, so the plan check passes it, but a rare label walks far.dev.hatk.getFeed; moving it is cutover's work.Review
Start at
read/feed.rs, thenread/location.rsfor the H3 ranges, which are the one new idea here.🤖 Generated with Claude Code