Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

at icy/kyxspm 4.2 kB View raw
1package knotacl 2 3import ( 4 "context" 5 "maps" 6 "net/http" 7 "slices" 8 "sync" 9 "time" 10 11 "golang.org/x/sync/singleflight" 12) 13 14const ( 15 cacheTTL = 15 * time.Second 16 cacheMaxEntries = 4096 17) 18 19type lister interface { 20 GetKnotMembers(ctx context.Context, host string) ([]string, error) 21 GetRepoCollaborators(ctx context.Context, host, repoDid string) ([]string, error) 22} 23 24type cacheEntry struct { 25 subjects []string 26 storedAt time.Time 27} 28 29type cache struct { 30 inner lister 31 ttl time.Duration 32 now func() time.Time 33 34 mu sync.Mutex 35 entries map[string]cacheEntry 36 group singleflight.Group 37} 38 39func newCache(inner lister, ttl time.Duration, now func() time.Time) *cache { 40 if now == nil { 41 now = time.Now 42 } 43 return &cache{inner: inner, ttl: ttl, now: now, entries: map[string]cacheEntry{}} 44} 45 46func (c *cache) GetKnotMembers(ctx context.Context, host string) ([]string, error) { 47 return c.fetch(ctx, memberCacheKey(host), func() ([]string, error) { 48 return c.inner.GetKnotMembers(ctx, host) 49 }) 50} 51 52func (c *cache) GetRepoCollaborators(ctx context.Context, host, repoDid string) ([]string, error) { 53 return c.fetch(ctx, collabCacheKey(host, repoDid), func() ([]string, error) { 54 return c.inner.GetRepoCollaborators(ctx, host, repoDid) 55 }) 56} 57 58func memberCacheKey(host string) string { return "m\x00" + host } 59 60func collabCacheKey(host, repoDid string) string { return "c\x00" + host + "\x00" + repoDid } 61 62func (c *cache) InvalidateMembers(host string) { 63 c.forget(memberCacheKey(host)) 64} 65 66func (c *cache) InvalidateCollaborators(host, repoDid string) { 67 c.forget(collabCacheKey(host, repoDid)) 68} 69 70func (c *cache) forget(key string) { 71 c.mu.Lock() 72 defer c.mu.Unlock() 73 delete(c.entries, key) 74} 75 76func (c *cache) fetch(ctx context.Context, key string, load func() ([]string, error)) ([]string, error) { 77 if memo := memoFrom(ctx); memo != nil { 78 if v, ok := memo.get(key); ok { 79 return slices.Clone(v), nil 80 } 81 } 82 v, err := c.load(key, load) 83 if err != nil { 84 return nil, err 85 } 86 if memo := memoFrom(ctx); memo != nil { 87 memo.put(key, v) 88 } 89 return slices.Clone(v), nil 90} 91 92func (c *cache) load(key string, load func() ([]string, error)) ([]string, error) { 93 if v, ok := c.lookup(key); ok { 94 return v, nil 95 } 96 v, err, _ := c.group.Do(key, func() (any, error) { 97 if v, ok := c.lookup(key); ok { 98 return v, nil 99 } 100 fresh, err := load() 101 if err != nil { 102 return nil, err 103 } 104 c.store(key, fresh) 105 return fresh, nil 106 }) 107 if err != nil { 108 return nil, err 109 } 110 return v.([]string), nil 111} 112 113func (c *cache) lookup(key string) ([]string, bool) { 114 c.mu.Lock() 115 defer c.mu.Unlock() 116 e, ok := c.entries[key] 117 if !ok || c.now().Sub(e.storedAt) >= c.ttl { 118 return nil, false 119 } 120 return e.subjects, true 121} 122 123func (c *cache) store(key string, subjects []string) { 124 c.mu.Lock() 125 defer c.mu.Unlock() 126 if _, exists := c.entries[key]; !exists && len(c.entries) >= cacheMaxEntries { 127 maps.DeleteFunc(c.entries, func(_ string, e cacheEntry) bool { 128 return c.now().Sub(e.storedAt) >= c.ttl 129 }) 130 c.evictOldestLocked() 131 } 132 c.entries[key] = cacheEntry{subjects: subjects, storedAt: c.now()} 133} 134 135func (c *cache) evictOldestLocked() { 136 oldestKey := "" 137 var oldestAt time.Time 138 for k, e := range c.entries { 139 if oldestKey == "" || e.storedAt.Before(oldestAt) { 140 oldestKey, oldestAt = k, e.storedAt 141 } 142 } 143 if len(c.entries) >= cacheMaxEntries && oldestKey != "" { 144 delete(c.entries, oldestKey) 145 } 146} 147 148type requestMemo struct { 149 mu sync.Mutex 150 entries map[string][]string 151} 152 153type memoCtxKey struct{} 154 155func WithMemo(ctx context.Context) context.Context { 156 return context.WithValue(ctx, memoCtxKey{}, &requestMemo{entries: map[string][]string{}}) 157} 158 159func memoFrom(ctx context.Context) *requestMemo { 160 memo, _ := ctx.Value(memoCtxKey{}).(*requestMemo) 161 return memo 162} 163 164func (m *requestMemo) get(key string) ([]string, bool) { 165 m.mu.Lock() 166 defer m.mu.Unlock() 167 v, ok := m.entries[key] 168 return v, ok 169} 170 171func (m *requestMemo) put(key string, subjects []string) { 172 m.mu.Lock() 173 defer m.mu.Unlock() 174 m.entries[key] = subjects 175} 176 177func MemoMiddleware(next http.Handler) http.Handler { 178 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 179 next.ServeHTTP(w, r.WithContext(WithMemo(r.Context()))) 180 }) 181}