Low-latency AT Protocol interaction indexer.
20

Configure Feed

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

api: add server and implement getBacklinksCount

Aly Raffauf (Jul 2, 2026, 9:48 PM EDT) e9b89600 a0169962

+106
+9
cmd/asterism/main.go
··· 9 9 10 10 "github.com/gorilla/websocket" 11 11 12 + "github.com/alyraffauf/asterism/internal/api" 12 13 "github.com/alyraffauf/asterism/internal/firehose" 13 14 "github.com/alyraffauf/asterism/internal/store" 14 15 ) ··· 49 50 if err != nil { 50 51 panic(err) 51 52 } 53 + 54 + server := &api.Server{Store: linkStore} 55 + go func() { 56 + if err := server.Run(":8081"); err != nil { 57 + panic(err) 58 + } 59 + }() 60 + defer conn.Close() 52 61 53 62 consumer := &firehose.Consumer{ 54 63 WantedCollections: parseCollections(*collectionsFlag),
+32
internal/api/backlinks.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "log" 6 + "net/http" 7 + ) 8 + 9 + type backlinksCountResponse struct { 10 + Total uint64 `json:"total"` 11 + } 12 + 13 + func (s *Server) GetBacklinksCount(w http.ResponseWriter, r *http.Request) { 14 + subject := r.URL.Query().Get("subject") 15 + source := r.URL.Query().Get("source") 16 + 17 + collection, path, err := parseSource(source) 18 + if err != nil { 19 + http.Error(w, err.Error(), http.StatusBadRequest) 20 + return 21 + } 22 + 23 + total, err := s.Store.CountBacklinks(r.Context(), subject, collection, path) 24 + if err != nil { 25 + log.Println("count backlinks:", err) 26 + http.Error(w, "internal error", http.StatusInternalServerError) 27 + return 28 + } 29 + 30 + w.Header().Set("Content-Type", "application/json") 31 + json.NewEncoder(w).Encode(backlinksCountResponse{Total: total}) 32 + }
+18
internal/api/server.go
··· 1 + package api 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/alyraffauf/asterism/internal/store" 7 + ) 8 + 9 + type Server struct { 10 + Store *store.Store 11 + } 12 + 13 + func (s *Server) Run(addr string) error { 14 + mux := http.NewServeMux() 15 + mux.HandleFunc("GET /xrpc/blue.microcosm.links.getBacklinksCount", s.GetBacklinksCount) 16 + 17 + return http.ListenAndServe(addr, mux) 18 + }
+27
internal/api/source.go
··· 1 + package api 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + ) 7 + 8 + func parseSource(raw string) (collection string, path string, err error) { 9 + collection, rawPath, ok := strings.Cut(raw, ":") 10 + if !ok { 11 + return "", "", fmt.Errorf("source must contain a ':' separator") 12 + } 13 + if collection == "" { 14 + return "", "", fmt.Errorf("source is missing a collection before ':'") 15 + } 16 + 17 + switch { 18 + case rawPath == "": 19 + return "", "", fmt.Errorf("source is missing a path after ':'") 20 + case rawPath == ".": 21 + return collection, ".", nil 22 + case strings.HasPrefix(rawPath, "."): 23 + return "", "", fmt.Errorf("source path must not start with '.'") 24 + default: 25 + return collection, "." + rawPath, nil 26 + } 27 + }
+20
internal/store/query.go
··· 1 + package store 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + ) 7 + 8 + func (s *Store) CountBacklinks(ctx context.Context, target, collection, fieldPath string) (uint64, error) { 9 + var total uint64 10 + 11 + err := s.db.QueryRowContext(ctx, 12 + `SELECT COUNT(*) FROM links WHERE target = ? AND collection = ? AND field_path = ?`, 13 + target, collection, fieldPath, 14 + ).Scan(&total) 15 + if err != nil { 16 + return 0, fmt.Errorf("count backlinks: %w", err) 17 + } 18 + 19 + return total, nil 20 + }