usercache: cleanup, also elsewhere

removed some old FIXMEs and documented usercache nicely for once,
renaming some things too.
This commit is contained in:
Harvey Tindall
2025-05-22 14:08:17 +01:00
parent 1dfe13951f
commit b53120f271
6 changed files with 57 additions and 46 deletions
+50 -36
View File
@@ -10,7 +10,6 @@ import (
)
const (
// FIXME: Follow mediabrowser, or make tuneable, or both
// After cache is this old, re-sync, but do it in the background and return the old cache.
WEB_USER_CACHE_SYNC = 30 * time.Second
// After cache is this old, re-sync and wait for it and return the new cache.
@@ -19,6 +18,11 @@ const (
USER_DEFAULT_SORT_ASCENDING = true
)
// UserCache caches the transport representation of users,
// complementing the built-in cache of the mediabrowser package.
// Synchronisation runs in the background and consumers receive
// old data for responsiveness unless an extended expiry time has passed.
// It also provides methods for sorting, searching and filtering server-side.
type UserCache struct {
Cache []respUser
Ref []*respUser
@@ -30,9 +34,11 @@ type UserCache struct {
Sorting bool
}
// FIXME: If shouldSync, sync in background and return old version. If shouldWaitForSync, wait for sync and return new one.
// FIXME: If locked, just wait for unlock and return someone elses work.
func (c *UserCache) gen(app *appContext) error {
// MaybeSync (maybe) syncs the cache, resulting in updated UserCache.Cache/.Ref/.Sorted.
// Only syncs if WEB_USER_CACHE_SYNC duration has passed since last one.
// If WEB_USER_CACHE_WAIT_FOR_SYNC duration has passed, this will block until a sync is complete, otherwise it will sync in the background
// (expecting you to use the old cache data). Only one sync will run at a time.
func (c *UserCache) MaybeSync(app *appContext) error {
shouldWaitForSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_WAIT_FOR_SYNC)) || c.Ref == nil || len(c.Ref) == 0
shouldSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_SYNC))
@@ -88,8 +94,8 @@ func (c *UserCache) gen(app *appContext) error {
return nil
}
func (c *UserCache) Gen(app *appContext, sorted bool) ([]*respUser, error) {
if err := c.gen(app); err != nil {
func (c *UserCache) GetUserDTOs(app *appContext, sorted bool) ([]*respUser, error) {
if err := c.MaybeSync(app); err != nil {
return nil, err
}
if sorted && !c.Sorted {
@@ -124,10 +130,11 @@ func bool2int(b bool) int {
return i
}
// Returns -1 if respUser < value, 0 if equal, 1 is greater than
// Sorter compares the given field of two respUsers, returning -1 if a < b, 0 if a == b, 1 if a > b.
type Sorter func(a, b *respUser) int
// Allow sorting by respUser's struct fields (well, it's JSON-representation's fields)
// SortUsersBy returns a Sorter function, which compares the given field of two respUsers, returning -1 if a < b, 0 if a == b, 1 if a > b.
func SortUsersBy(field string) Sorter {
switch field {
case "id":
@@ -204,11 +211,8 @@ func SortUsersBy(field string) Sorter {
}
}
panic(fmt.Errorf("got invalid field %s", field))
return nil
}
type Filter func(*respUser) bool
type CompareResult int
const (
@@ -256,10 +260,13 @@ func (d DateAttempt) Compare(subject int64) int {
return subjectTime.Compare(time.Date(yy, mo, dd, hh, mm, 0, 0, nil))
}
// FIXME: Consider using QueryDTO.Class rather than assuming type from name? Probably not worthwhile though.
func FilterUsersBy(field string, op QueryOperator, value any) Filter {
// Filter returns true if a specific field in the passed respUser matches some internally defined value.
type Filter func(*respUser) bool
// AsFilter returns a Filter function, which compares the queries value to the corresponding field's value in a passed respUser.
func (q QueryDTO) AsFilter() Filter {
operator := Equal
switch op {
switch q.Operator {
case LesserOperator:
operator = Lesser
case EqualOperator:
@@ -268,84 +275,84 @@ func FilterUsersBy(field string, op QueryOperator, value any) Filter {
operator = Greater
}
switch field {
switch q.Field {
case "id":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.ID), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.ID), strings.ToLower(q.Value.(string))) == int(operator)
}
case "name":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(q.Value.(string))) == int(operator)
}
case "email":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Email), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Email), strings.ToLower(q.Value.(string))) == int(operator)
}
case "notify_email":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.NotifyThroughEmail), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.NotifyThroughEmail), bool2int(q.Value.(bool))) == int(operator)
}
case "last_active":
return func(a *respUser) bool {
return value.(DateAttempt).Compare(a.LastActive) == int(operator)
return q.Value.(DateAttempt).Compare(a.LastActive) == int(operator)
}
case "admin":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.Admin), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.Admin), bool2int(q.Value.(bool))) == int(operator)
}
case "expiry":
return func(a *respUser) bool {
return value.(DateAttempt).Compare(a.Expiry) == int(operator)
return q.Value.(DateAttempt).Compare(a.Expiry) == int(operator)
}
case "disabled":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.Disabled), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.Disabled), bool2int(q.Value.(bool))) == int(operator)
}
case "telegram":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Telegram), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Telegram), strings.ToLower(q.Value.(string))) == int(operator)
}
case "notify_telegram":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.NotifyThroughTelegram), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.NotifyThroughTelegram), bool2int(q.Value.(bool))) == int(operator)
}
case "discord":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Discord), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Discord), strings.ToLower(q.Value.(string))) == int(operator)
}
case "discord_id":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.DiscordID), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.DiscordID), strings.ToLower(q.Value.(string))) == int(operator)
}
case "notify_discord":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.NotifyThroughDiscord), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.NotifyThroughDiscord), bool2int(q.Value.(bool))) == int(operator)
}
case "matrix":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Matrix), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Matrix), strings.ToLower(q.Value.(string))) == int(operator)
}
case "notify_matrix":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.NotifyThroughMatrix), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.NotifyThroughMatrix), bool2int(q.Value.(bool))) == int(operator)
}
case "label":
return func(a *respUser) bool {
return cmp.Compare(strings.ToLower(a.Label), strings.ToLower(value.(string))) == int(operator)
return cmp.Compare(strings.ToLower(a.Label), strings.ToLower(q.Value.(string))) == int(operator)
}
case "accounts_admin":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.AccountsAdmin), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.AccountsAdmin), bool2int(q.Value.(bool))) == int(operator)
}
case "referrals_enabled":
return func(a *respUser) bool {
return cmp.Compare(bool2int(a.ReferralsEnabled), bool2int(value.(bool))) == int(operator)
return cmp.Compare(bool2int(a.ReferralsEnabled), bool2int(q.Value.(bool))) == int(operator)
}
}
panic(fmt.Errorf("got invalid field %s", field))
return nil
panic(fmt.Errorf("got invalid q.Field %s", q.Field))
}
// MatchesSearch checks (case-insensitively) if any string field in respUser includes the term string.
func (ru *respUser) MatchesSearch(term string) bool {
return (strings.Contains(ru.ID, term) ||
strings.Contains(strings.ToLower(ru.Name), term) ||
@@ -356,6 +363,7 @@ func (ru *respUser) MatchesSearch(term string) bool {
strings.Contains(strings.ToLower(ru.Telegram), term))
}
// QueryClass is the class of a query (the datatype), i.e. bool, string or date.
type QueryClass string
const (
@@ -364,6 +372,7 @@ const (
DateQuery QueryClass = "date"
)
// QueryOperator is the operator used for comparison in a filter, i.e. <, = or >.
type QueryOperator string
const (
@@ -372,6 +381,7 @@ const (
GreaterOperator QueryOperator = ">"
)
// QueryDTO is the transport representation of a Query, sent from the web app.
type QueryDTO struct {
Class QueryClass `json:"class"`
Field string `json:"field"`
@@ -380,17 +390,20 @@ type QueryDTO struct {
Value any `json:"value"`
}
// ServerSearchReqDTO is a usual PaginatedReqDTO with added fields for searching and filtering.
type ServerSearchReqDTO struct {
PaginatedReqDTO
SearchTerms []string `json:"searchTerms"`
Queries []QueryDTO `json:"queries"`
}
// Filter by AND-ing all search terms and queries.
// Filter reduces the passed slice of *respUsers
// by searching for each term of terms[] with respUser.MatchesSearch,
// and by evaluating Queries with Query.AsFilter().
func (c *UserCache) Filter(users []*respUser, terms []string, queries []QueryDTO) []*respUser {
filters := make([]Filter, len(queries))
for i, q := range queries {
filters[i] = FilterUsersBy(q.Field, q.Operator, q.Value)
filters[i] = q.AsFilter()
}
// FIXME: Properly consider pre-allocation size
out := make([]*respUser, 0, len(users)/4)
@@ -418,6 +431,7 @@ func (c *UserCache) Filter(users []*respUser, terms []string, queries []QueryDTO
return out
}
// Sort sorts the given slice of of *respUsers in-place by the field name given, in ascending or descending order.
func (c *UserCache) Sort(users []*respUser, field string, ascending bool) {
slices.SortFunc(users, SortUsersBy(field))
if !ascending {