Skip to content

Commit

Permalink
[chore] much improved paging package (#2182)
Browse files Browse the repository at this point in the history
  • Loading branch information
NyaaaWhatsUpDoc committed Sep 7, 2023
1 parent 14ef098 commit b093947
Show file tree
Hide file tree
Showing 15 changed files with 1,154 additions and 445 deletions.
14 changes: 7 additions & 7 deletions internal/api/client/blocks/blocksget.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,20 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
return
}

limit, errWithCode := apiutil.ParseLimit(c.Query(LimitKey), 20, 100, 2)
if err != nil {
page, errWithCode := paging.ParseIDPage(c,
1, // min limit
100, // max limit
20, // default limit
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}

resp, errWithCode := m.processor.BlocksGet(
c.Request.Context(),
authed.Account,
paging.Pager{
SinceID: c.Query(SinceIDKey),
MaxID: c.Query(MaxIDKey),
Limit: limit,
},
page,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
Expand Down
25 changes: 0 additions & 25 deletions internal/cache/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,3 @@ func (c *SliceCache[T]) Load(key string, load func() ([]T, error)) ([]T, error)
// Return data clone for safety.
return slices.Clone(data), nil
}

// LoadRange is functionally the same as .Load(), but will pass the result through provided reslice function before returning a cloned result.
func (c *SliceCache[T]) LoadRange(key string, load func() ([]T, error), reslice func([]T) []T) ([]T, error) {
// Look for follow IDs list in cache under this key.
data, ok := c.Get(key)

if !ok {
var err error

// Not cached, load!
data, err = load()
if err != nil {
return nil, err
}

// Store the data.
c.Set(key, data)
}

// Reslice to range.
slice := reslice(data)

// Return range clone for safety.
return slices.Clone(slice), nil
}
17 changes: 14 additions & 3 deletions internal/db/bundb/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ func (r *relationshipDB) GetAccountFollowRequesting(ctx context.Context, account
return r.GetFollowRequestsByIDs(ctx, followReqIDs)
}

func (r *relationshipDB) GetAccountBlocks(ctx context.Context, accountID string, page *paging.Pager) ([]*gtsmodel.Block, error) {
func (r *relationshipDB) GetAccountBlocks(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Block, error) {
// Load block IDs from cache with database loader callback.
blockIDs, err := r.state.Caches.GTS.BlockIDs().LoadRange(accountID, func() ([]string, error) {
blockIDs, err := r.state.Caches.GTS.BlockIDs().Load(accountID, func() ([]string, error) {
var blockIDs []string

// Block IDs not in cache, perform DB query!
Expand All @@ -162,11 +162,22 @@ func (r *relationshipDB) GetAccountBlocks(ctx context.Context, accountID string,
}

return blockIDs, nil
}, page.PageDesc)
})
if err != nil {
return nil, err
}

// Our cached / selected block IDs are
// ALWAYS stored in descending order.
// Depending on the paging requested
// this may be an unexpected order.
if !page.GetOrder().Ascending() {
blockIDs = paging.Reverse(blockIDs)
}

// Page the resulting block IDs.
blockIDs = page.Page(blockIDs)

// Convert these IDs to full block objects.
return r.GetBlocksByIDs(ctx, blockIDs)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/db/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ type Relationship interface {
CountAccountFollowRequesting(ctx context.Context, accountID string) (int, error)

// GetAccountBlocks returns all blocks originating from the given account, with given optional paging parameters.
GetAccountBlocks(ctx context.Context, accountID string, paging *paging.Pager) ([]*gtsmodel.Block, error)
GetAccountBlocks(ctx context.Context, accountID string, paging *paging.Page) ([]*gtsmodel.Block, error)

// GetNote gets a private note from a source account on a target account, if it exists.
GetNote(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.AccountNote, error)
Expand Down
135 changes: 135 additions & 0 deletions internal/paging/boundary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package paging

// MinID returns an ID boundary with given min ID value,
// using either the `since_id`,"DESC" name,ordering or
// `min_id`,"ASC" name,ordering depending on which is set.
func MinID(minID, sinceID string) Boundary {
/*
Paging with `since_id` vs `min_id`:
limit = 4 limit = 4
+----------+ +----------+
max_id--> |xxxxxxxxxx| | | <-- max_id
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
since_id--> | | |xxxxxxxxxx| <-- min_id
+----------+ +----------+
| | | |
+----------+ +----------+
*/
switch {
case minID != "":
return Boundary{
Name: "min_id",
Value: minID,
Order: OrderAscending,
}
default:
// default min is `since_id`
return Boundary{
Name: "since_id",
Value: sinceID,
Order: OrderDescending,
}
}
}

// MaxID returns an ID boundary with given max
// ID value, and the "max_id" query key set.
func MaxID(maxID string) Boundary {
return Boundary{
Name: "max_id",
Value: maxID,
Order: OrderDescending,
}
}

// MinShortcodeDomain returns a boundary with the given minimum emoji
// shortcode@domain, and the "min_shortcode_domain" query key set.
func MinShortcodeDomain(min string) Boundary {
return Boundary{
Name: "min_shortcode_domain",
Value: min,
Order: OrderAscending,
}
}

// MaxShortcodeDomain returns a boundary with the given maximum emoji
// shortcode@domain, and the "max_shortcode_domain" query key set.
func MaxShortcodeDomain(max string) Boundary {
return Boundary{
Name: "max_shortcode_domain",
Value: max,
Order: OrderDescending,
}
}

// Boundary represents the upper or lower limit in a page slice.
type Boundary struct {
Name string // i.e. query key
Value string
Order Order // NOTE: see Order type for explanation
}

// new creates a new Boundary with the same ordering and name
// as the original (receiving), but with the new provided value.
func (b Boundary) new(value string) Boundary {
return Boundary{
Name: b.Name,
Value: value,
Order: b.Order,
}
}

// Find finds the boundary's set value in input slice, or returns -1.
func (b Boundary) Find(in []string) int {
if zero(b.Value) {
return -1
}
for i := range in {
if in[i] == b.Value {
return i
}
}
return -1
}

// Query returns this boundary as assembled query key=value pair.
func (b Boundary) Query() string {
switch {
case zero(b.Value):
return ""
case b.Name == "":
panic("value without boundary name")
default:
return b.Name + "=" + b.Value
}
}
55 changes: 55 additions & 0 deletions internal/paging/order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package paging

// Order represents the order an input
// page should be sorted and paged in.
//
// NOTE: this does not effect the order of returned
// API results, which must always be in descending
// order. This behaviour is confusing, but we adopt
// it to stay inline with Mastodon API expectations.
type Order int

const (
_default Order = iota
OrderDescending
OrderAscending
)

// Ascending returns whether this Order is ascending.
func (i Order) Ascending() bool {
return i == OrderAscending
}

// Descending returns whether this Order is descending.
func (i Order) Descending() bool {
return i == OrderDescending
}

// String returns a string representation of Order.
func (i Order) String() string {
switch i {
case OrderDescending:
return "Descending"
case OrderAscending:
return "Ascending"
default:
return "not-specified"
}
}
Loading

0 comments on commit b093947

Please sign in to comment.