feed: pagination — offset/limit + cursor-by-at (before/after/page-before/next-cursor) + 14 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 44s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 44s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SUITES=(basic fanout rank integration content notify home dedupe trending mute)
|
SUITES=(basic fanout rank integration content notify home dedupe trending mute page)
|
||||||
|
|
||||||
OUT_JSON="lib/feed/scoreboard.json"
|
OUT_JSON="lib/feed/scoreboard.json"
|
||||||
OUT_MD="lib/feed/scoreboard.md"
|
OUT_MD="lib/feed/scoreboard.md"
|
||||||
@@ -42,6 +42,7 @@ run_suite() {
|
|||||||
(load "lib/feed/home.sx")
|
(load "lib/feed/home.sx")
|
||||||
(load "lib/feed/trending.sx")
|
(load "lib/feed/trending.sx")
|
||||||
(load "lib/feed/mute.sx")
|
(load "lib/feed/mute.sx")
|
||||||
|
(load "lib/feed/page.sx")
|
||||||
(epoch 2)
|
(epoch 2)
|
||||||
(eval "(define feed-test-pass 0)")
|
(eval "(define feed-test-pass 0)")
|
||||||
(eval "(define feed-test-fail 0)")
|
(eval "(define feed-test-fail 0)")
|
||||||
|
|||||||
50
lib/feed/page.sx
Normal file
50
lib/feed/page.sx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
; feed/page — pagination. Offset/limit for indexed access, and cursor-based
|
||||||
|
; (by :at) for recency feeds, which is stable under inserts: a cursor is the
|
||||||
|
; :at of the last item seen, and the next page is the newest items older than it.
|
||||||
|
;
|
||||||
|
; Requires: lib/feed/stream.sx (feed/recent, feed/take, feed/filter).
|
||||||
|
|
||||||
|
; --- offset / limit ---------------------------------------------------------
|
||||||
|
|
||||||
|
(define
|
||||||
|
feed/page
|
||||||
|
(fn
|
||||||
|
(stream offset limit)
|
||||||
|
(feed/stream (take (drop (feed/items stream) offset) limit))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
feed/page-count
|
||||||
|
(fn (stream limit) (ceil (/ (feed/count stream) limit))))
|
||||||
|
|
||||||
|
; --- cursor (recency feeds) -------------------------------------------------
|
||||||
|
|
||||||
|
; activities strictly older than cursor (scroll down / load older)
|
||||||
|
(define
|
||||||
|
feed/before
|
||||||
|
(fn
|
||||||
|
(stream cursor)
|
||||||
|
(feed/filter stream (fn (a) (< (get a :at) cursor)))))
|
||||||
|
|
||||||
|
; activities strictly newer than cursor (load newer / "N new posts")
|
||||||
|
(define
|
||||||
|
feed/after
|
||||||
|
(fn
|
||||||
|
(stream cursor)
|
||||||
|
(feed/filter stream (fn (a) (> (get a :at) cursor)))))
|
||||||
|
|
||||||
|
; one page: the `limit` newest activities older than cursor, newest first
|
||||||
|
(define
|
||||||
|
feed/page-before
|
||||||
|
(fn
|
||||||
|
(stream cursor limit)
|
||||||
|
(feed/take (feed/recent (feed/before stream cursor)) limit)))
|
||||||
|
|
||||||
|
; cursor to fetch the next (older) page: :at of the last item of a page,
|
||||||
|
; or nil when the page is empty (end of feed)
|
||||||
|
(define
|
||||||
|
feed/next-cursor
|
||||||
|
(fn
|
||||||
|
(page)
|
||||||
|
(let
|
||||||
|
((items (feed/items page)))
|
||||||
|
(if (= (len items) 0) nil (get (last items) :at)))))
|
||||||
@@ -9,9 +9,10 @@
|
|||||||
"home": {"pass": 6, "fail": 0},
|
"home": {"pass": 6, "fail": 0},
|
||||||
"dedupe": {"pass": 9, "fail": 0},
|
"dedupe": {"pass": 9, "fail": 0},
|
||||||
"trending": {"pass": 11, "fail": 0},
|
"trending": {"pass": 11, "fail": 0},
|
||||||
"mute": {"pass": 9, "fail": 0}
|
"mute": {"pass": 9, "fail": 0},
|
||||||
|
"page": {"pass": 14, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 163,
|
"total_pass": 177,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 163
|
"total": 177
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ _Generated by `lib/feed/conformance.sh`_
|
|||||||
| dedupe | 9 | 0 | 9 |
|
| dedupe | 9 | 0 | 9 |
|
||||||
| trending | 11 | 0 | 11 |
|
| trending | 11 | 0 | 11 |
|
||||||
| mute | 9 | 0 | 9 |
|
| mute | 9 | 0 | 9 |
|
||||||
| **Total** | **163** | **0** | **163** |
|
| page | 14 | 0 | 14 |
|
||||||
|
| **Total** | **177** | **0** | **177** |
|
||||||
|
|||||||
86
lib/feed/tests/page.sx
Normal file
86
lib/feed/tests/page.sx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
; Follow-up — pagination (offset + cursor). (feed-test name got expected)
|
||||||
|
|
||||||
|
; ---------- offset / limit ----------
|
||||||
|
|
||||||
|
(define
|
||||||
|
O
|
||||||
|
(feed/stream
|
||||||
|
(list
|
||||||
|
(feed/activity "u" "post" "o1" 1 (list))
|
||||||
|
(feed/activity "u" "post" "o2" 2 (list))
|
||||||
|
(feed/activity "u" "post" "o3" 3 (list))
|
||||||
|
(feed/activity "u" "post" "o4" 4 (list))
|
||||||
|
(feed/activity "u" "post" "o5" 5 (list)))))
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"page 1"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/page O 0 2)))
|
||||||
|
(list "o1" "o2"))
|
||||||
|
(feed-test
|
||||||
|
"page 2"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/page O 2 2)))
|
||||||
|
(list "o3" "o4"))
|
||||||
|
(feed-test
|
||||||
|
"page 3 (partial)"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/page O 4 2)))
|
||||||
|
(list "o5"))
|
||||||
|
(feed-test
|
||||||
|
"page past end empty"
|
||||||
|
(feed/count (feed/page O 10 2))
|
||||||
|
0)
|
||||||
|
(feed-test "page-count 5/2 = 3" (feed/page-count O 2) 3)
|
||||||
|
(feed-test "page-count 5/5 = 1" (feed/page-count O 5) 1)
|
||||||
|
|
||||||
|
; ---------- cursor (recency) ----------
|
||||||
|
|
||||||
|
(define
|
||||||
|
R
|
||||||
|
(feed/stream
|
||||||
|
(list
|
||||||
|
(feed/activity "u" "post" "a" 50 (list))
|
||||||
|
(feed/activity "u" "post" "b" 40 (list))
|
||||||
|
(feed/activity "u" "post" "c" 30 (list))
|
||||||
|
(feed/activity "u" "post" "d" 20 (list))
|
||||||
|
(feed/activity "u" "post" "e" 10 (list)))))
|
||||||
|
|
||||||
|
(define p1 (feed/page-before R 100 2))
|
||||||
|
(feed-test
|
||||||
|
"cursor page 1 newest first"
|
||||||
|
(map (fn (a) (get a :object)) (feed/items p1))
|
||||||
|
(list "a" "b"))
|
||||||
|
(feed-test "next cursor after page 1" (feed/next-cursor p1) 40)
|
||||||
|
|
||||||
|
(define p2 (feed/page-before R (feed/next-cursor p1) 2))
|
||||||
|
(feed-test
|
||||||
|
"cursor page 2"
|
||||||
|
(map (fn (a) (get a :object)) (feed/items p2))
|
||||||
|
(list "c" "d"))
|
||||||
|
(feed-test "next cursor after page 2" (feed/next-cursor p2) 20)
|
||||||
|
|
||||||
|
(define p3 (feed/page-before R (feed/next-cursor p2) 2))
|
||||||
|
(feed-test
|
||||||
|
"cursor page 3 (partial)"
|
||||||
|
(map (fn (a) (get a :object)) (feed/items p3))
|
||||||
|
(list "e"))
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"empty page nil cursor"
|
||||||
|
(feed/next-cursor (feed/page-before R 5 2))
|
||||||
|
nil)
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"after cursor loads newer"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/recent (feed/after R 30))))
|
||||||
|
(list "a" "b"))
|
||||||
|
(feed-test
|
||||||
|
"before cursor count"
|
||||||
|
(feed/count (feed/before R 30))
|
||||||
|
2)
|
||||||
@@ -14,7 +14,7 @@ APL, ACL visibility filtering via `lib/acl/`, federation via fed-sx.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/feed/conformance.sh` → **163/163** (Phases 1–4 + TF-IDF, notifications, home, smart-dedupe, trending, mute)
|
`bash lib/feed/conformance.sh` → **177/177** (Phases 1–4 + TF-IDF, notifications, home, smart-dedupe, trending, mute, pagination)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -157,6 +157,9 @@ are function parameters. Real acl-sx / fed-sx wire in at the call site unchanged
|
|||||||
- [x] Mute/block — `feed/mute-actors` / `feed/mute-tags` / `feed/mute-objects` /
|
- [x] Mute/block — `feed/mute-actors` / `feed/mute-tags` / `feed/mute-objects` /
|
||||||
`feed/apply-prefs`: viewer-controlled per-request filtering (complements ACL's
|
`feed/apply-prefs`: viewer-controlled per-request filtering (complements ACL's
|
||||||
author-controlled visibility) (`mute.sx`); 9 tests. (163/163 total.)
|
author-controlled visibility) (`mute.sx`); 9 tests. (163/163 total.)
|
||||||
|
- [x] Pagination — `feed/page`/`feed/page-count` (offset) + `feed/before`/
|
||||||
|
`feed/after`/`feed/page-before`/`feed/next-cursor` (cursor by :at, stable under
|
||||||
|
inserts) (`page.sx`); 14 tests. (177/177 total.)
|
||||||
|
|
||||||
(none)
|
(none)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user