Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }