Monorepo for Tangled
tangled.org
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}