diff --git a/lib/feed/conformance.sh b/lib/feed/conformance.sh index fa11ef15..fa17dfc2 100755 --- a/lib/feed/conformance.sh +++ b/lib/feed/conformance.sh @@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then exit 1 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_MD="lib/feed/scoreboard.md" @@ -42,6 +42,7 @@ run_suite() { (load "lib/feed/home.sx") (load "lib/feed/trending.sx") (load "lib/feed/mute.sx") +(load "lib/feed/page.sx") (epoch 2) (eval "(define feed-test-pass 0)") (eval "(define feed-test-fail 0)") diff --git a/lib/feed/page.sx b/lib/feed/page.sx new file mode 100644 index 00000000..de255e7b --- /dev/null +++ b/lib/feed/page.sx @@ -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))))) diff --git a/lib/feed/scoreboard.json b/lib/feed/scoreboard.json index 76cb89ec..85e24120 100644 --- a/lib/feed/scoreboard.json +++ b/lib/feed/scoreboard.json @@ -9,9 +9,10 @@ "home": {"pass": 6, "fail": 0}, "dedupe": {"pass": 9, "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": 163 + "total": 177 } diff --git a/lib/feed/scoreboard.md b/lib/feed/scoreboard.md index 681af0f8..13b93f4b 100644 --- a/lib/feed/scoreboard.md +++ b/lib/feed/scoreboard.md @@ -14,4 +14,5 @@ _Generated by `lib/feed/conformance.sh`_ | dedupe | 9 | 0 | 9 | | trending | 11 | 0 | 11 | | mute | 9 | 0 | 9 | -| **Total** | **163** | **0** | **163** | +| page | 14 | 0 | 14 | +| **Total** | **177** | **0** | **177** | diff --git a/lib/feed/tests/page.sx b/lib/feed/tests/page.sx new file mode 100644 index 00000000..e1a1af87 --- /dev/null +++ b/lib/feed/tests/page.sx @@ -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) diff --git a/plans/feed-on-sx.md b/plans/feed-on-sx.md index 8be2610a..5b7f8e49 100644 --- a/plans/feed-on-sx.md +++ b/plans/feed-on-sx.md @@ -14,7 +14,7 @@ APL, ACL visibility filtering via `lib/acl/`, federation via fed-sx. ## 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 @@ -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` / `feed/apply-prefs`: viewer-controlled per-request filtering (complements ACL's 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)