Skip to content

proposal: paginate ListTasks RPC and task list page#1014

Open
icholy-bot wants to merge 2 commits into
masterfrom
proposal/task-pagination
Open

proposal: paginate ListTasks RPC and task list page#1014
icholy-bot wants to merge 2 commits into
masterfrom
proposal/task-pagination

Conversation

@icholy-bot

@icholy-bot icholy-bot commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a design proposal for paginating the ListTasks RPC and the task list page (webui/src/routes/tasks.index.tsx), per the linked issue. No implementation — just the proposal document in proposals/draft/.

The proposed design uses keyset (cursor) pagination:

  • ListTasksRequest gains page_size, page_token, and filter; ListTasksResponse gains next_page_token.
  • An opaque base64 cursor encodes (created_at, id) of the last row; the SQL switches to ORDER BY created_at DESC, id DESC with a keyset predicate and a supporting partial index.
  • The name search moves server-side so it works across all tasks rather than only the loaded page.
  • The web UI switches to useInfiniteQuery with a debounced filter and a "Load more" control.

Key decisions (keyset vs offset, server-side vs client-side search, why not the bare-limit ListExternalEvents convention, "Load more" vs numbered pages) and open questions (default page size, total count, status filtering) are written up in the document for review.

Closes #1012

@icholy-bot icholy-bot requested a review from icholy as a code owner June 15, 2026 21:39
@icholy-bot icholy-bot mentioned this pull request Jun 15, 2026
@icholy

icholy commented Jun 15, 2026

Copy link
Copy Markdown
Owner

@icholy-bot I think I want something like a pagination package which can hide some of these details that you have in the handler.

@icholy-bot

Copy link
Copy Markdown
Collaborator Author

Good call — pushed a revision that extracts the plumbing into a reusable internal/pagination package so the handler stays thin.

The package is generic over the cursor type C and row type T, storage/proto-agnostic (no sqlc/connect/proto imports), and exposes:

  • pagination.Config{Default, Max} — declarative page-size bounds.
  • Parse[C](cfg, pageSize, pageToken) (Page[C], error) — validates size and decodes the opaque token into a typed cursor; returns Page.Cursor (nil on first page) and Page.Limit() (= size + 1).
  • Result[T,C](items, page, cursorOf) ([]T, nextToken, error) — trims the over-fetched slice and builds next_page_token from the last row's cursor.

The ListTasks handler now reduces to roughly:

page, err := pagination.Parse[taskCursor](listTasksPaging, req.PageSize, req.PageToken)
// ... store.ListTasksPage(..., Limit: page.Limit()) ...
tasks, nextToken, err := pagination.Result(tasks, page, func(t *model.Task) taskCursor {
    return taskCursor{CreatedAt: t.CreatedAt, ID: t.ID}
})

Only the taskCursor struct and the cursorOf mapping stay per-RPC; the same package backs any future paginated list (ListEvents, ListLogs, …). Bad page sizes and undecodable tokens both surface as a wrapped pagination.ErrInvalidTokenCodeInvalidArgument.

One placement question for you in the doc: internal/pagination (dedicated package) vs internal/x/pagination to match the existing utility convention — happy to go either way. (task 897)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task pagination

2 participants