~bigbes/sr-ht-dolt

ref: 377c616a8a3b385f18a8d6509c7a6be3a31af2bd sr-ht-dolt/graph/paging.go -rw-r--r-- 3.7 KiB
377c616a — Eugene Blikh web: render memory bodies as markdown, and resolve their references 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package graph

import (
	"context"
	"strconv"

	coremodel "sourcecraft.dev/bigbes/sr-ht-core/model"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
	"sourcecraft.dev/bigbes/sr-ht-dolt/core"
	"sourcecraft.dev/bigbes/sr-ht-dolt/graph/model"
)

// Page sizes for the database listings. The count travels in the cursor, as it
// does on every other sr.ht service, so a client that asked for a page size
// keeps it across pages without repeating itself.
const (
	defaultPageSize = 25
	maxPageSize     = 100
)

// page slices one page out of a listing the store has already filtered to what
// the caller may see, and mints the cursor for the next one.
//
// The slicing is in memory rather than in SQL, which is a real limit and not an
// oversight: ListReposForViewer's rule spans three tables and the listings it
// answers are instance-sized (tens of rows), so the whole set is fetched and cut
// here. A listing that outgrows that wants a keyset query in db/, and this is
// the function that would then become a thin wrapper over it.
//
// The cursor carries the id of the first row of the next page and the page size.
// Resuming starts at the first row whose id is at or below it, so a cursor whose
// row was deleted — or made invisible to this caller — between two pages resumes
// at the next row instead of failing or, worse, silently starting over.
func page(repos []*core.Repo, cursor *coremodel.Cursor, filter *coremodel.Filter) *model.DatabaseCursor {
	count := defaultPageSize
	start := 0
	if cursor != nil {
		if cursor.Count > 0 {
			count = cursor.Count
		}
		if after, err := strconv.Atoi(cursor.Next); err == nil {
			start = len(repos)
			for i, repo := range repos {
				if repo.ID <= after {
					start = i
					break
				}
			}
		}
	}
	// A filter given on this call wins over what the cursor remembers, so a
	// client can change the page size mid-walk without minting a new cursor.
	if filter != nil && filter.Count != nil && *filter.Count > 0 {
		count = *filter.Count
	}
	if count > maxPageSize {
		count = maxPageSize
	}

	end := start + count
	if end > len(repos) {
		end = len(repos)
	}

	out := make([]*model.Database, 0, end-start)
	for _, repo := range repos[start:end] {
		out = append(out, model.NewDatabase(repo))
	}

	page := &model.DatabaseCursor{Results: out}
	if end < len(repos) {
		page.Cursor = &coremodel.Cursor{
			Next:  strconv.Itoa(repos[end].ID),
			Count: count,
		}
	}
	return page
}

// logLimit bounds how many commits one Database.log answers. The cap is the
// surface's, not the client's: a log page is a walk of the on-disk store.
func logLimit(limit *int) int {
	if limit == nil || *limit <= 0 {
		return defaultLogLimit
	}
	if *limit > maxLogLimit {
		return maxLogLimit
	}
	return *limit
}

// cursorNext is the commit hash a log page resumes from, or "" to start at the
// ref's head.
func cursorNext(from *coremodel.Cursor) string {
	if from == nil {
		return ""
	}
	return from.Next
}

// nextCursor wraps browse's "next hash" in the schema's cursor, or nil when the
// walk reached the root commit.
func nextCursor(next string) *coremodel.Cursor {
	if next == "" {
		return nil
	}
	return &coremodel.Cursor{Next: next}
}

// resolveRef answers the ref a field should read: the one the query named, or
// the database's default branch. It returns "" — and no error — for a store
// with no branches at all, which is a database nothing has been pushed to yet;
// the caller turns that into the empty answer for its own field.
func resolveRef(ctx context.Context, sess BrowseSession, ref *string) (string, error) {
	if ref != nil && *ref != "" {
		return *ref, nil
	}
	branches, err := sess.Branches(ctx)
	if err != nil {
		return "", internalError("Database.ref", err)
	}
	if len(branches) == 0 {
		return "", nil
	}
	return browse.DefaultBranch(branches), nil
}