vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Paginate the whole activity feed
c8cdb6accc47af06a3df2b8b5023c21134fdb812 · Keenan Nemetz · 10d ago · parent 4d938fe8
Paginate the whole activity feed Both activity pages stopped at a fixed number of events: the repository feed was truncated at 100, and the instance feed merged 100 per repository and then took 200 before its pager. Anything older was unreachable. Now that issues, changes, comments and state changes are index rows, the whole feed is assembled from the database and paginated: - the repository feed and the instance feed keep the whole sorted, filtered list, and both show 50 events per page; - the repository feed gains a pager (it had none), and the instance pager now reaches all history; - the kind/actor/assignee filters travel through the page links, so paging never widens the feed; - the repository pager swaps the `#activity-feed` fragment over HTMX like the chips and the filter form, and the page links are plain hrefs when JavaScript is not there; - the feed reads comment summaries (parent, id, author, created) rather than every comment body it will not render. The home rail still asks for a bounded feed, so it keeps its handful of events. The feed is assembled in memory per request, so a page costs O(history) instead of O(page); that is the accepted trade for now, and a SQL event stream is the follow-up if a large history makes it visible.
server/web/home.go +16−14
| @@ -24,10 +24,8 @@ const ( | |||
| 24 | 24 | // homeEventLimit is what fits in the rail without turning the landing page | |
| 25 | 25 | // back into a feed. The full feed has its own page at /activity. | |
| 26 | 26 | homeEventLimit = 10 | |
| 27 | // instanceEventLimit bounds /activity after the per-repository feeds are | ||
| 28 | // merged; instanceEventPage is what one page of it shows. | ||
| 29 | instanceEventLimit = 200 | ||
| 30 | instanceEventPage = 50 | ||
| 27 | // instanceEventPage is what one page of /activity shows. | ||
| 28 | instanceEventPage = 50 | ||
| 31 | 29 | ) | |
| 32 | 30 | ||
| 33 | 31 | // instanceInfo is the identity the landing page shows. It is derived from the | |
| @@ -121,7 +119,8 @@ func (w *Web) InstanceActivity(rw http.ResponseWriter, r *http.Request) { | |||
| 121 | 119 | return | |
| 122 | 120 | } | |
| 123 | 121 | number, _ := strconv.Atoi(r.URL.Query().Get("page")) | |
| 124 | events, pager := paginate(w.instanceEvents(memoOf(r), entries, kind), kind, number, instanceEventPage) | ||
| 122 | events, pager := paginate(w.instanceEvents(memoOf(r), entries, kind), number, instanceEventPage, | ||
| 123 | instanceActivityURL(kind), "Activity pages") | ||
| 125 | 124 | w.render(rw, "instance_activity", instanceActivityData{ | |
| 126 | 125 | Page: w.page(r, "Activity"), Kind: kind, Events: events, Pager: pager, | |
| 127 | 126 | }, 0) | |
| @@ -166,7 +165,7 @@ func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, [ | |||
| 166 | 165 | if err != nil { | |
| 167 | 166 | continue | |
| 168 | 167 | } | |
| 169 | feed := w.repoActivity(memo, m, "", "", "") | ||
| 168 | feed := w.repoActivity(memo, m, "", "", "", homeEventLimit) | ||
| 170 | 169 | card := repoCard{ | |
| 171 | 170 | Name: e.Name, Description: e.Description, Branch: m.DefaultBranch, | |
| 172 | 171 | Private: true, Commit: feed.LastCommit, LastTag: feed.LastTag, | |
| @@ -208,15 +207,12 @@ func (w *Web) instanceEvents(memo *requestMemo, entries []index.Entry, kind stri | |||
| 208 | 207 | if err != nil { | |
| 209 | 208 | continue | |
| 210 | 209 | } | |
| 211 | for _, ev := range w.repoActivity(memo, m, kind, "", "").Events { | ||
| 210 | for _, ev := range w.repoActivity(memo, m, kind, "", "", 0).Events { | ||
| 212 | 211 | ev.Repo = m.Name | |
| 213 | 212 | events = append(events, ev) | |
| 214 | 213 | } | |
| 215 | 214 | } | |
| 216 | 215 | sort.SliceStable(events, func(i, j int) bool { return events[i].Time.After(events[j].Time) }) | |
| 217 | if len(events) > instanceEventLimit { | ||
| 218 | events = events[:instanceEventLimit] | ||
| 219 | } | ||
| 220 | 216 | return events | |
| 221 | 217 | } | |
| 222 | 218 | ||
| @@ -224,8 +220,10 @@ func (w *Web) instanceEvents(memo *requestMemo, entries []index.Entry, kind stri | |||
| 224 | 220 | // range is clamped to the nearest page, so an old link or a hand-edited URL still | |
| 225 | 221 | // renders something rather than an empty page. The kind filter travels with the | |
| 226 | 222 | // page links, so paging never silently widens the feed. | |
| 227 | func paginate(events []activityEvent, kind string, number, perPage int) ([]activityEvent, pager) { | ||
| 228 | p := newPager(len(events), perPage, number, func(n int) string { | ||
| 223 | // instanceActivityURL builds a page link for /activity, keeping the kind filter | ||
| 224 | // so paging never silently widens the feed. | ||
| 225 | func instanceActivityURL(kind string) func(int) string { | ||
| 226 | return func(n int) string { | ||
| 229 | 227 | v := url.Values{} | |
| 230 | 228 | if kind != "" { | |
| 231 | 229 | v.Set("kind", kind) | |
| @@ -237,8 +235,12 @@ func paginate(events []activityEvent, kind string, number, perPage int) ([]activ | |||
| 237 | 235 | return "/activity?" + q | |
| 238 | 236 | } | |
| 239 | 237 | return "/activity" | |
| 240 | }) | ||
| 241 | p.Label = "Activity pages" | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | func paginate(events []activityEvent, number, perPage int, urlFor func(int) string, label string) ([]activityEvent, pager) { | ||
| 242 | p := newPager(len(events), perPage, number, urlFor) | ||
| 243 | p.Label = label | ||
| 242 | 244 | if len(events) == 0 { | |
| 243 | 245 | return nil, p | |
| 244 | 246 | } | |