Monorepo for Tangled tangled.org
3

Configure Feed

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

at icy/lqyotq 5.9 kB View raw
1package knotcompat 2 3import ( 4 "context" 5 "fmt" 6 "net/http" 7 "slices" 8 "strconv" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 "tangled.org/core/api/tangled" 16 "tangled.org/core/consts" 17) 18 19const ( 20 versionProbeTimeout = 5 * time.Second 21 versionProbeFresh = 5 * time.Minute 22 versionProbeTrust = time.Hour 23 versionProbeCacheMax = 4096 24) 25 26type versionProbeEntry struct { 27 version string 28 probedAt time.Time 29} 30 31type versionProbeCache struct { 32 mu sync.Mutex 33 entries map[string]versionProbeEntry 34} 35 36func (c *versionProbeCache) get(host string) (versionProbeEntry, bool) { 37 c.mu.Lock() 38 defer c.mu.Unlock() 39 e, ok := c.entries[host] 40 return e, ok 41} 42 43func (c *versionProbeCache) put(host, version string, at time.Time) { 44 c.mu.Lock() 45 defer c.mu.Unlock() 46 if _, exists := c.entries[host]; !exists && len(c.entries) >= versionProbeCacheMax { 47 c.evictLocked(at) 48 } 49 c.entries[host] = versionProbeEntry{version: version, probedAt: at} 50} 51 52func (c *versionProbeCache) evictLocked(now time.Time) { 53 oldestHost := "" 54 var oldestAt time.Time 55 for h, e := range c.entries { 56 if now.Sub(e.probedAt) >= versionProbeTrust { 57 delete(c.entries, h) 58 continue 59 } 60 if oldestHost == "" || e.probedAt.Before(oldestAt) { 61 oldestHost, oldestAt = h, e.probedAt 62 } 63 } 64 if len(c.entries) >= versionProbeCacheMax && oldestHost != "" { 65 delete(c.entries, oldestHost) 66 } 67} 68 69func (c *versionProbeCache) supports(now time.Time, host string, minMajor, minMinor int, failOpen bool, probe func() (string, bool)) bool { 70 if e, ok := c.get(host); ok && now.Sub(e.probedAt) < versionProbeFresh { 71 return atLeast(e.version, minMajor, minMinor) 72 } 73 if version, ok := probe(); ok { 74 c.put(host, version, now) 75 return atLeast(version, minMajor, minMinor) 76 } 77 if e, ok := c.get(host); ok && now.Sub(e.probedAt) < versionProbeTrust { 78 return atLeast(e.version, minMajor, minMinor) 79 } 80 return failOpen 81} 82 83var probeCache = &versionProbeCache{entries: map[string]versionProbeEntry{}} 84 85type CapStatus int 86 87const ( 88 CapUnknown CapStatus = iota 89 CapAbsent 90 CapPresent 91) 92 93type negEntry struct { 94 status CapStatus 95 until time.Time 96} 97 98type NativeLatch interface { 99 IsNative(host string) bool 100 MarkNative(host string) 101} 102 103type latchBox struct{ l NativeLatch } 104 105type nativeGate struct { 106 memo sync.Map 107 negMemo sync.Map 108 latch atomic.Pointer[latchBox] 109 useOnce sync.Once 110 now func() time.Time 111} 112 113func (g *nativeGate) clock() time.Time { 114 if g.now != nil { 115 return g.now() 116 } 117 return time.Now() 118} 119 120func (g *nativeGate) use(l NativeLatch) { 121 g.useOnce.Do(func() { 122 g.latch.Store(&latchBox{l: l}) 123 }) 124} 125 126func (g *nativeGate) currentLatch() NativeLatch { 127 if b := g.latch.Load(); b != nil { 128 return b.l 129 } 130 return nil 131} 132 133func (g *nativeGate) status(host string, probe func() CapStatus) CapStatus { 134 if _, ok := g.memo.Load(host); ok { 135 return CapPresent 136 } 137 if v, ok := g.negMemo.Load(host); ok { 138 e := v.(negEntry) 139 if g.clock().Before(e.until) { 140 return e.status 141 } 142 g.negMemo.Delete(host) 143 } 144 if l := g.currentLatch(); l != nil && l.IsNative(host) { 145 g.memo.Store(host, struct{}{}) 146 return CapPresent 147 } 148 if s := probe(); s != CapPresent { 149 g.negMemo.Store(host, negEntry{status: s, until: g.clock().Add(versionProbeFresh)}) 150 return s 151 } 152 g.memo.Store(host, struct{}{}) 153 if l := g.currentLatch(); l != nil { 154 l.MarkNative(host) 155 } 156 return CapPresent 157} 158 159var nativeProbeGate = &nativeGate{} 160 161func UseNativeLatch(l NativeLatch) { 162 nativeProbeGate.use(l) 163} 164 165func KnotSupports114(ctx context.Context, host string, dev bool) bool { 166 return knotSupportsVersion(ctx, host, dev, 1, 14, true) 167} 168 169func KnotCapability(ctx context.Context, host string, dev bool, capability consts.Capability) CapStatus { 170 return nativeProbeGate.status(host, func() CapStatus { 171 return knotDeclares(ctx, host, dev, capability) 172 }) 173} 174 175func KnotHasCapability(ctx context.Context, host string, dev bool, capability consts.Capability) bool { 176 return KnotCapability(ctx, host, dev, capability) == CapPresent 177} 178 179func knotDeclares(ctx context.Context, host string, dev bool, capability consts.Capability) CapStatus { 180 scheme := "https" 181 if dev { 182 scheme = "http" 183 } 184 client := &indigoxrpc.Client{ 185 Host: fmt.Sprintf("%s://%s", scheme, host), 186 Client: &http.Client{Timeout: versionProbeTimeout}, 187 } 188 189 ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 190 defer cancel() 191 192 resp, err := tangled.KnotVersion(ctx, client) 193 if err != nil || resp == nil { 194 return CapUnknown 195 } 196 if slices.Contains(resp.Capabilities, string(capability)) { 197 return CapPresent 198 } 199 return CapAbsent 200} 201 202func knotSupportsVersion(ctx context.Context, host string, dev bool, minMajor, minMinor int, failOpen bool) bool { 203 return probeCache.supports(time.Now(), host, minMajor, minMinor, failOpen, func() (string, bool) { 204 return probeVersion(ctx, host, dev) 205 }) 206} 207 208func probeVersion(ctx context.Context, host string, dev bool) (string, bool) { 209 scheme := "https" 210 if dev { 211 scheme = "http" 212 } 213 client := &indigoxrpc.Client{ 214 Host: fmt.Sprintf("%s://%s", scheme, host), 215 Client: &http.Client{Timeout: versionProbeTimeout}, 216 } 217 218 ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 219 defer cancel() 220 221 resp, err := tangled.KnotVersion(ctx, client) 222 if err != nil || resp == nil { 223 return "", false 224 } 225 return resp.Version, true 226} 227 228func atLeast(v string, minMajor, minMinor int) bool { 229 v = strings.TrimSpace(v) 230 v = strings.TrimPrefix(v, "v") 231 if strings.HasPrefix(v, "(devel)") { 232 return true 233 } 234 if v == "" { 235 return false 236 } 237 parts := strings.SplitN(v, ".", 3) 238 if len(parts) < 2 { 239 return false 240 } 241 major, err := strconv.Atoi(parts[0]) 242 if err != nil { 243 return false 244 } 245 minorRaw := strings.SplitN(parts[1], "-", 2)[0] 246 minor, err := strconv.Atoi(minorRaw) 247 if err != nil { 248 return false 249 } 250 return major > minMajor || (major == minMajor && minor >= minMinor) 251}