Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
6.8 kB · 280 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281package knotcompat
import ( "context" "fmt" "net/http" "slices" "strconv" "strings" "sync" "sync/atomic" "time"
"github.com/bluesky-social/indigo/atproto/syntax" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "tangled.org/core/api/tangled" "tangled.org/core/consts" "tangled.org/core/repoident")
const ( versionProbeTimeout = 5 * time.Second versionProbeFresh = 5 * time.Minute versionProbeTrust = time.Hour versionProbeCacheMax = 4096)
type knotIdentity struct { version string capabilities []string}
type versionProbeEntry struct { identity knotIdentity probedAt time.Time}
type versionProbeCache struct { mu sync.Mutex entries map[string]versionProbeEntry}
func (c *versionProbeCache) get(host string) (versionProbeEntry, bool) { c.mu.Lock() defer c.mu.Unlock() e, ok := c.entries[host] return e, ok}
func (c *versionProbeCache) put(host string, identity knotIdentity, at time.Time) { c.mu.Lock() defer c.mu.Unlock() if _, exists := c.entries[host]; !exists && len(c.entries) >= versionProbeCacheMax { c.evictLocked(at) } c.entries[host] = versionProbeEntry{identity: identity, probedAt: at}}
func (c *versionProbeCache) evictLocked(now time.Time) { oldestHost := "" var oldestAt time.Time for h, e := range c.entries { if now.Sub(e.probedAt) >= versionProbeTrust { delete(c.entries, h) continue } if oldestHost == "" || e.probedAt.Before(oldestAt) { oldestHost, oldestAt = h, e.probedAt } } if len(c.entries) >= versionProbeCacheMax && oldestHost != "" { delete(c.entries, oldestHost) }}
func (c *versionProbeCache) resolve(now time.Time, host string, probe func() (knotIdentity, bool)) (knotIdentity, bool) { if e, ok := c.get(host); ok && now.Sub(e.probedAt) < versionProbeFresh { return e.identity, true } if identity, ok := probe(); ok { c.put(host, identity, now) return identity, true } if e, ok := c.get(host); ok && now.Sub(e.probedAt) < versionProbeTrust { return e.identity, true } return knotIdentity{}, false}
func (c *versionProbeCache) supports(now time.Time, host string, minMajor, minMinor int, failOpen bool, probe func() (knotIdentity, bool)) bool { identity, ok := c.resolve(now, host, probe) if !ok { return failOpen } return atLeast(identity.version, minMajor, minMinor)}
var probeCache = &versionProbeCache{entries: map[string]versionProbeEntry{}}
type CapStatus int
const ( CapUnknown CapStatus = iota CapAbsent CapPresent)
type negEntry struct { status CapStatus until time.Time}
type NativeLatch interface { IsNative(host string) bool MarkNative(host string)}
type latchBox struct{ l NativeLatch }
type nativeGate struct { memo sync.Map negMemo sync.Map latch atomic.Pointer[latchBox] useOnce sync.Once now func() time.Time}
func (g *nativeGate) clock() time.Time { if g.now != nil { return g.now() } return time.Now()}
func (g *nativeGate) use(l NativeLatch) { g.useOnce.Do(func() { g.latch.Store(&latchBox{l: l}) })}
func (g *nativeGate) currentLatch() NativeLatch { if b := g.latch.Load(); b != nil { return b.l } return nil}
func (g *nativeGate) status(host string, probe func() CapStatus) CapStatus { if _, ok := g.memo.Load(host); ok { return CapPresent } if v, ok := g.negMemo.Load(host); ok { e := v.(negEntry) if g.clock().Before(e.until) { return e.status } g.negMemo.Delete(host) } if l := g.currentLatch(); l != nil && l.IsNative(host) { g.memo.Store(host, struct{}{}) return CapPresent } if s := probe(); s != CapPresent { g.negMemo.Store(host, negEntry{status: s, until: g.clock().Add(versionProbeFresh)}) return s } g.memo.Store(host, struct{}{}) if l := g.currentLatch(); l != nil { l.MarkNative(host) } return CapPresent}
var nativeProbeGate = &nativeGate{}
func UseNativeLatch(l NativeLatch) { nativeProbeGate.use(l)}
func KnotSupports114(ctx context.Context, host string, dev bool) bool { return knotSupportsVersion(ctx, host, dev, 1, 14, true)}
func RepoArg(ctx context.Context, host string, dev bool, repoDid string, legacyAtUri syntax.ATURI) string { rd, err := repoident.NewRepoDid(repoDid) if err != nil { return legacyAtUri.String() } identity, ok := probeCache.resolve(time.Now(), host, func() (knotIdentity, bool) { return probeIdentity(ctx, host, dev) }) if !ok || !slices.Contains(identity.capabilities, string(consts.CapRepoDidInput)) { return legacyAtUri.String() } return rd.String()}
func KnotCapability(ctx context.Context, host string, dev bool, capability consts.Capability) CapStatus { return nativeProbeGate.status(host, func() CapStatus { return knotDeclares(ctx, host, dev, capability) })}
func KnotHasCapability(ctx context.Context, host string, dev bool, capability consts.Capability) bool { return KnotCapability(ctx, host, dev, capability) == CapPresent}
func knotDeclares(ctx context.Context, host string, dev bool, capability consts.Capability) CapStatus { scheme := "https" if dev { scheme = "http" } client := &indigoxrpc.Client{ Host: fmt.Sprintf("%s://%s", scheme, host), Client: &http.Client{Timeout: versionProbeTimeout}, }
ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) defer cancel()
resp, err := tangled.KnotVersion(ctx, client) if err != nil || resp == nil { return CapUnknown } if slices.Contains(resp.Capabilities, string(capability)) { return CapPresent } return CapAbsent}
func knotSupportsVersion(ctx context.Context, host string, dev bool, minMajor, minMinor int, failOpen bool) bool { return probeCache.supports(time.Now(), host, minMajor, minMinor, failOpen, func() (knotIdentity, bool) { return probeIdentity(ctx, host, dev) })}
func probeIdentity(ctx context.Context, host string, dev bool) (knotIdentity, bool) { scheme := "https" if dev { scheme = "http" } client := &indigoxrpc.Client{ Host: fmt.Sprintf("%s://%s", scheme, host), Client: &http.Client{Timeout: versionProbeTimeout}, }
ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) defer cancel()
resp, err := tangled.KnotVersion(ctx, client) if err != nil || resp == nil { return knotIdentity{}, false } return knotIdentity{version: resp.Version, capabilities: resp.Capabilities}, true}
func atLeast(v string, minMajor, minMinor int) bool { v = strings.TrimSpace(v) v = strings.TrimPrefix(v, "v") if strings.HasPrefix(v, "(devel)") { return true } if v == "" { return false } parts := strings.SplitN(v, ".", 3) if len(parts) < 2 { return false } major, err := strconv.Atoi(parts[0]) if err != nil { return false } minorRaw := strings.SplitN(parts[1], "-", 2)[0] minor, err := strconv.Atoi(minorRaw) if err != nil { return false } return major > minMajor || (major == minMajor && minor >= minMinor)}