Monorepo for Tangled tangled.org
2

Configure Feed

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

appview/knotcompat: knot capability detection

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 8, 2026, 4:18 PM +0300) commit db265537 parent d588ea08 change-id xvnlplur
+232 -97
+1 -1
appview/compat113/compat.go appview/knotcompat/compat.go
··· 1 - package compat113 1 + package knotcompat 2 2 3 3 import ( 4 4 "encoding/json"
+1 -1
appview/compat113/compat_test.go appview/knotcompat/compat_test.go
··· 1 - package compat113 1 + package knotcompat 2 2 3 3 import ( 4 4 "encoding/json"
-60
appview/compat113/version.go
··· 1 - package compat113 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - "strconv" 8 - "strings" 9 - "time" 10 - 11 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 - "tangled.org/core/api/tangled" 13 - ) 14 - 15 - const versionProbeTimeout = 5 * time.Second 16 - 17 - func KnotSupports114(ctx context.Context, host string, dev bool) bool { 18 - scheme := "https" 19 - if dev { 20 - scheme = "http" 21 - } 22 - client := &indigoxrpc.Client{ 23 - Host: fmt.Sprintf("%s://%s", scheme, host), 24 - Client: &http.Client{Timeout: versionProbeTimeout}, 25 - } 26 - 27 - ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 28 - defer cancel() 29 - 30 - resp, err := tangled.KnotVersion(ctx, client) 31 - if err != nil || resp == nil { 32 - return true 33 - } 34 - return atLeast114(resp.Version) 35 - } 36 - 37 - func atLeast114(v string) bool { 38 - v = strings.TrimSpace(v) 39 - v = strings.TrimPrefix(v, "v") 40 - if strings.HasPrefix(v, "(devel)") { 41 - return true 42 - } 43 - if v == "" { 44 - return false 45 - } 46 - parts := strings.SplitN(v, ".", 3) 47 - if len(parts) < 2 { 48 - return false 49 - } 50 - major, err := strconv.Atoi(parts[0]) 51 - if err != nil { 52 - return false 53 - } 54 - minorRaw := strings.SplitN(parts[1], "-", 2)[0] 55 - minor, err := strconv.Atoi(minorRaw) 56 - if err != nil { 57 - return false 58 - } 59 - return major > 1 || (major == 1 && minor >= 14) 60 - }
-35
appview/compat113/version_test.go
··· 1 - package compat113 2 - 3 - import "testing" 4 - 5 - func TestAtLeast114(t *testing.T) { 6 - cases := []struct { 7 - in string 8 - want bool 9 - }{ 10 - {"v1.14.0", true}, 11 - {"v1.14.0-alpha", true}, 12 - {"v1.14.5", true}, 13 - {"v1.13.0", false}, 14 - {"v1.13.0-alpha", false}, 15 - {"v1.0.0", false}, 16 - {"v2.0.0", true}, 17 - {"1.14.0", true}, 18 - {"1.13.99", false}, 19 - {"(devel)", true}, 20 - {"", false}, 21 - {"garbagio-furioso", false}, 22 - {"v1", false}, 23 - {"vX.Y.Z", false}, 24 - {"unknown", false}, 25 - {"unknown-abc1234", false}, 26 - {"unknown-abc1234-modified", false}, 27 - } 28 - for _, c := range cases { 29 - t.Run(c.in, func(t *testing.T) { 30 - if got := atLeast114(c.in); got != c.want { 31 - t.Errorf("atLeast114(%q) = %v, want %v", c.in, got, c.want) 32 - } 33 - }) 34 - } 35 - }
+230
appview/knotcompat/version.go
··· 1 + package knotcompat 2 + 3 + import ( 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 + 19 + const ( 20 + versionProbeTimeout = 5 * time.Second 21 + versionProbeFresh = 5 * time.Minute 22 + versionProbeTrust = time.Hour 23 + versionProbeCacheMax = 4096 24 + ) 25 + 26 + type versionProbeEntry struct { 27 + version string 28 + probedAt time.Time 29 + } 30 + 31 + type versionProbeCache struct { 32 + mu sync.Mutex 33 + entries map[string]versionProbeEntry 34 + } 35 + 36 + func (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 + 43 + func (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 + 52 + func (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 + 69 + func (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 + 83 + var probeCache = &versionProbeCache{entries: map[string]versionProbeEntry{}} 84 + 85 + type NativeLatch interface { 86 + IsNative(host string) bool 87 + MarkNative(host string) 88 + } 89 + 90 + type latchBox struct{ l NativeLatch } 91 + 92 + type 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 + 100 + func (g *nativeGate) clock() time.Time { 101 + if g.now != nil { 102 + return g.now() 103 + } 104 + return time.Now() 105 + } 106 + 107 + func (g *nativeGate) use(l NativeLatch) { 108 + g.useOnce.Do(func() { 109 + g.latch.Store(&latchBox{l: l}) 110 + }) 111 + } 112 + 113 + func (g *nativeGate) currentLatch() NativeLatch { 114 + if b := g.latch.Load(); b != nil { 115 + return b.l 116 + } 117 + return nil 118 + } 119 + 120 + func (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 + 145 + var nativeProbeGate = &nativeGate{} 146 + 147 + func UseNativeLatch(l NativeLatch) { 148 + nativeProbeGate.use(l) 149 + } 150 + 151 + func KnotSupports114(ctx context.Context, host string, dev bool) bool { 152 + return knotSupportsVersion(ctx, host, dev, 1, 14, true) 153 + } 154 + 155 + func 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 + 161 + func 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 + 181 + func 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 + 187 + func 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 + 207 + func 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 + }