Monorepo for Tangled tangled.org
6

Configure Feed

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

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 NativeLatch interface { 86 IsNative(host string) bool 87 MarkNative(host string) 88} 89 90type latchBox struct{ l NativeLatch } 91 92type nativeGate struct { 93 memo sync.Map 94 negMemo sync.Map 95 latch atomic.Pointer[latchBox] 96 useOnce sync.Once 97 now func() time.Time 98} 99 100func (g *nativeGate) clock() time.Time { 101 if g.now != nil { 102 return g.now() 103 } 104 return time.Now() 105} 106 107func (g *nativeGate) use(l NativeLatch) { 108 g.useOnce.Do(func() { 109 g.latch.Store(&latchBox{l: l}) 110 }) 111} 112 113func (g *nativeGate) currentLatch() NativeLatch { 114 if b := g.latch.Load(); b != nil { 115 return b.l 116 } 117 return nil 118} 119 120func (g *nativeGate) isNative(host string, probe func() bool) bool { 121 if _, ok := g.memo.Load(host); ok { 122 return true 123 } 124 if until, ok := g.negMemo.Load(host); ok { 125 if g.clock().Before(until.(time.Time)) { 126 return false 127 } 128 g.negMemo.Delete(host) 129 } 130 if l := g.currentLatch(); l != nil && l.IsNative(host) { 131 g.memo.Store(host, struct{}{}) 132 return true 133 } 134 if !probe() { 135 g.negMemo.Store(host, g.clock().Add(versionProbeFresh)) 136 return false 137 } 138 g.memo.Store(host, struct{}{}) 139 if l := g.currentLatch(); l != nil { 140 l.MarkNative(host) 141 } 142 return true 143} 144 145var nativeProbeGate = &nativeGate{} 146 147func UseNativeLatch(l NativeLatch) { 148 nativeProbeGate.use(l) 149} 150 151func KnotSupports114(ctx context.Context, host string, dev bool) bool { 152 return knotSupportsVersion(ctx, host, dev, 1, 14, true) 153} 154 155func KnotHasCapability(ctx context.Context, host string, dev bool, capability consts.Capability) bool { 156 return nativeProbeGate.isNative(host, func() bool { 157 return knotDeclares(ctx, host, dev, capability) 158 }) 159} 160 161func knotDeclares(ctx context.Context, host string, dev bool, capability consts.Capability) bool { 162 scheme := "https" 163 if dev { 164 scheme = "http" 165 } 166 client := &indigoxrpc.Client{ 167 Host: fmt.Sprintf("%s://%s", scheme, host), 168 Client: &http.Client{Timeout: versionProbeTimeout}, 169 } 170 171 ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 172 defer cancel() 173 174 resp, err := tangled.KnotVersion(ctx, client) 175 if err != nil || resp == nil { 176 return false 177 } 178 return slices.Contains(resp.Capabilities, string(capability)) 179} 180 181func knotSupportsVersion(ctx context.Context, host string, dev bool, minMajor, minMinor int, failOpen bool) bool { 182 return probeCache.supports(time.Now(), host, minMajor, minMinor, failOpen, func() (string, bool) { 183 return probeVersion(ctx, host, dev) 184 }) 185} 186 187func probeVersion(ctx context.Context, host string, dev bool) (string, bool) { 188 scheme := "https" 189 if dev { 190 scheme = "http" 191 } 192 client := &indigoxrpc.Client{ 193 Host: fmt.Sprintf("%s://%s", scheme, host), 194 Client: &http.Client{Timeout: versionProbeTimeout}, 195 } 196 197 ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 198 defer cancel() 199 200 resp, err := tangled.KnotVersion(ctx, client) 201 if err != nil || resp == nil { 202 return "", false 203 } 204 return resp.Version, true 205} 206 207func atLeast(v string, minMajor, minMinor int) bool { 208 v = strings.TrimSpace(v) 209 v = strings.TrimPrefix(v, "v") 210 if strings.HasPrefix(v, "(devel)") { 211 return true 212 } 213 if v == "" { 214 return false 215 } 216 parts := strings.SplitN(v, ".", 3) 217 if len(parts) < 2 { 218 return false 219 } 220 major, err := strconv.Atoi(parts[0]) 221 if err != nil { 222 return false 223 } 224 minorRaw := strings.SplitN(parts[1], "-", 2)[0] 225 minor, err := strconv.Atoi(minorRaw) 226 if err != nil { 227 return false 228 } 229 return major > minMajor || (major == minMajor && minor >= minMinor) 230}