···128128 pdsCache := &sync.Map{}
129129130130 visited := sync.Map{}
131131- didCh := make(chan string, *workers*4)
131131+ didCh := make(chan string, 10000)
132132+ var inFlight sync.WaitGroup
132133134134+ // seed initial DIDs
133135 for _, did := range cleanSeeds {
134136 visited.Store(did, true)
137137+ inFlight.Add(1)
135138 didCh <- did
136139 }
137140···139142 var totalVouches atomic.Int64
140143 var totalFollows atomic.Int64
141144 var totalErrors atomic.Int64
145145+ var startTime = time.Now()
146146+147147+ // periodic progress ticker
148148+ go func() {
149149+ ticker := time.NewTicker(5 * time.Second)
150150+ defer ticker.Stop()
151151+ for {
152152+ select {
153153+ case <-ctx.Done():
154154+ return
155155+ case <-ticker.C:
156156+ visited := totalVisited.Load()
157157+ elapsed := time.Since(startTime).Round(time.Second)
158158+ rate := float64(visited) / elapsed.Seconds()
159159+ slog.Info("progress",
160160+ "visited", visited,
161161+ "vouches", totalVouches.Load(),
162162+ "follows", totalFollows.Load(),
163163+ "errors", totalErrors.Load(),
164164+ "queue", len(didCh),
165165+ "rate", fmt.Sprintf("%.1f/s", rate),
166166+ "elapsed", elapsed.String(),
167167+ )
168168+ }
169169+ }
170170+ }()
171171+172172+ // closer: wait for all in-flight work to finish, then close the channel
173173+ go func() {
174174+ inFlight.Wait()
175175+ close(didCh)
176176+ }()
142177143178 var wg sync.WaitGroup
144179 for i := 0; i < *workers; i++ {
···148183 for did := range didCh {
149184 select {
150185 case <-ctx.Done():
186186+ inFlight.Done()
151187 return
152188 default:
153189 }
154190155155- n := totalVisited.Add(1)
156156- if n%20 == 0 {
157157- slog.Info("progress",
158158- "visited", n,
159159- "vouches", totalVouches.Load(),
160160- "follows", totalFollows.Load(),
161161- "errors", totalErrors.Load(),
162162- "queue", len(didCh),
163163- )
164164- }
191191+ totalVisited.Add(1)
165192166193 newDIDs, err := backfillDID(ctx, store, pdsCache, did, &totalVouches, &totalFollows)
167194 if err != nil {
168195 totalErrors.Add(1)
169196 slog.Warn("backfill failed", "did", did, "error", err)
197197+ inFlight.Done()
170198 continue
171199 }
172200···175203 continue
176204 }
177205 if _, loaded := visited.LoadOrStore(newDID, true); !loaded {
206206+ inFlight.Add(1)
178207 select {
179208 case didCh <- newDID:
180209 case <-ctx.Done():
181181- return
210210+ inFlight.Done()
182211 }
183212 }
184213 }
214214+ inFlight.Done()
185215 }
186216 }(i)
187217 }
188218189189- close(didCh)
190219 wg.Wait()
191220221221+ elapsed := time.Since(startTime).Round(time.Second)
222222+ rate := float64(totalVisited.Load()) / elapsed.Seconds()
192223 slog.Info("backfill complete",
193224 "visited", totalVisited.Load(),
194225 "vouches", totalVouches.Load(),
195226 "follows", totalFollows.Load(),
196227 "errors", totalErrors.Load(),
228228+ "elapsed", elapsed.String(),
229229+ "rate", fmt.Sprintf("%.1f/s", rate),
197230 )
198231199232 slog.Info("enriching profiles...")
+3
internal/db/db.go
···255255 AND d.did NOT IN (
256256 SELECT did FROM profiles WHERE handle != '' AND avatar_url != ''
257257 )
258258+ AND d.did NOT IN (
259259+ SELECT did FROM profiles WHERE handle = '!'
260260+ )
258261 LIMIT ?
259262 `, limit)
260263 if err != nil {
+51
internal/resolve/resolve.go
···141141 // PLC fallback for handle only
142142 h, err := ResolveHandle(ctx, did)
143143 if err != nil {
144144+ // tombstone: mark as unresolvable so we don't retry
145145+ store.UpsertProfile(db.Profile{DID: did, Handle: "!", UpdatedAt: time.Now()})
144146 return fmt.Errorf("resolve handle: %w", err)
145147 }
146148 handle = h
···199201 handle, err := ResolveHandle(ctx, did)
200202 if err != nil {
201203 slog.Warn("handle resolution failed", "did", did, "error", err)
204204+ // tombstone: mark as unresolvable so we don't retry
205205+ store.UpsertProfile(db.Profile{DID: did, Handle: "!", UpdatedAt: time.Now()})
202206 } else {
203207 p.Handle = handle
204208 }
···265269 return total, nil
266270 }
267271 }
272272+}
273273+274274+func ResolveDIDFromHandle(ctx context.Context, handle string) (string, error) {
275275+ // 1. Try DNS TXT _atproto.<handle>
276276+ req, err := http.NewRequestWithContext(ctx, "GET",
277277+ fmt.Sprintf("https://dns.google/resolve?name=_atproto.%s&type=TXT", url.QueryEscape(handle)), nil)
278278+ if err == nil {
279279+ resp, err := httpClient.Do(req)
280280+ if err == nil {
281281+ defer resp.Body.Close()
282282+ if resp.StatusCode == 200 {
283283+ var dnsResult struct {
284284+ Answer []struct {
285285+ Data string `json:"data"`
286286+ } `json:"Answer"`
287287+ }
288288+ if json.NewDecoder(resp.Body).Decode(&dnsResult) == nil {
289289+ for _, a := range dnsResult.Answer {
290290+ did := strings.TrimSpace(strings.Trim(a.Data, `"`))
291291+ if isValidDID(did) {
292292+ return did, nil
293293+ }
294294+ }
295295+ }
296296+ }
297297+ }
298298+ }
299299+300300+ // 2. Try HTTPS well-known
301301+ wkReq, err := http.NewRequestWithContext(ctx, "GET",
302302+ fmt.Sprintf("https://%s/.well-known/atproto.json", handle), nil)
303303+ if err == nil {
304304+ resp, err := httpClient.Do(wkReq)
305305+ if err == nil {
306306+ defer resp.Body.Close()
307307+ if resp.StatusCode == 200 {
308308+ var wkDoc struct {
309309+ DID string `json:"did"`
310310+ }
311311+ if json.NewDecoder(resp.Body).Decode(&wkDoc) == nil && isValidDID(wkDoc.DID) {
312312+ return wkDoc.DID, nil
313313+ }
314314+ }
315315+ }
316316+ }
317317+318318+ return "", fmt.Errorf("could not resolve handle %q to DID", handle)
268319}
269320270321func truncate(s string, n int) string {
+59
internal/web/server.go
···77 "io"
88 "log/slog"
99 "net/http"
1010+ "net/url"
1011 "os"
1212+ "strings"
1113 "time"
12141315 "dunkirk.sh/tangle-of-trust/internal/db"
···62646365func (s *Server) routes() {
6466 s.mux.HandleFunc("/api/proxy/avatar", s.handleAvatarProxy)
6767+ s.mux.HandleFunc("/api/search", s.handleSearch)
6568 s.mux.HandleFunc("/api/graph", s.handleGraph)
6669 s.mux.HandleFunc("/api/stats", s.handleStats)
6770 s.mux.HandleFunc("/api/resolve", s.handleResolve)
···110113 w.Header().Set("Cache-Control", "public, max-age=86400")
111114 w.Header().Set("Access-Control-Allow-Origin", "*")
112115 io.Copy(w, resp.Body)
116116+}
117117+118118+func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
119119+ q := r.URL.Query().Get("q")
120120+ if q == "" {
121121+ writeJSON(w, map[string]interface{}{"actors": nil})
122122+ return
123123+ }
124124+125125+ ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
126126+ defer cancel()
127127+128128+ // Always try Bluesky search first
129129+ type SearchActor struct {
130130+ DID string `json:"did"`
131131+ Handle string `json:"handle"`
132132+ Avatar string `json:"avatar"`
133133+ }
134134+ var actors []SearchActor
135135+136136+ u := fmt.Sprintf("%s/xrpc/app.bsky.actor.searchActors?q=%s&limit=6", resolve.BskyPublicAPI, url.QueryEscape(q))
137137+ req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
138138+ if err == nil {
139139+ resp, err := http.DefaultClient.Do(req)
140140+ if err == nil && resp.StatusCode == 200 {
141141+ defer resp.Body.Close()
142142+ var result struct {
143143+ Actors []SearchActor `json:"actors"`
144144+ }
145145+ if json.NewDecoder(resp.Body).Decode(&result) == nil {
146146+ actors = result.Actors
147147+ }
148148+ } else if resp != nil {
149149+ resp.Body.Close()
150150+ }
151151+ }
152152+153153+ // If Bluesky found nothing and query looks like a handle (contains a dot), resolve via ATProto
154154+ if len(actors) == 0 && strings.Contains(q, ".") {
155155+ did, err := resolve.ResolveDIDFromHandle(ctx, q)
156156+ if err == nil && did != "" {
157157+ // try to get avatar from Bluesky
158158+ avatar := ""
159159+ handle := q
160160+ profiles, perr := resolve.BatchProfiles(ctx, []string{did})
161161+ if perr == nil && len(profiles) > 0 {
162162+ if profiles[0].Handle != "" {
163163+ handle = profiles[0].Handle
164164+ }
165165+ avatar = profiles[0].Avatar
166166+ }
167167+ actors = append(actors, SearchActor{DID: did, Handle: handle, Avatar: avatar})
168168+ }
169169+ }
170170+171171+ writeJSON(w, map[string]interface{}{"actors": actors})
113172}
114173115174func (s *Server) handleGraph(w http.ResponseWriter, r *http.Request) {