Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

138 lines
3.3 KiB
Go

package server
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/zzet/gortex/internal/graph"
)
// subgraphNodeCap bounds the neighbour ring returned by /v1/subgraph so a
// single hydration call can never pull an unbounded slice of the graph.
const subgraphNodeCap = 200
// subgraphMaxDepth caps the requested ring depth server-side.
const subgraphMaxDepth = 2
// SubGraphResponse is the /v1/subgraph payload: FULL nodes (Meta /
// QualName / EndLine intact, unlike the brief /v1/graph projection) for a
// node and its neighbour ring, used by cross-daemon proxy-edge proxy-node
// hydration.
type SubGraphResponse struct {
Root *graph.Node `json:"root"`
Nodes []*graph.Node `json:"nodes"`
Edges []*graph.Edge `json:"edges"`
Stats SubGraphMeta `json:"stats"`
}
// SubGraphMeta carries the freshness + truncation metadata the hydrator
// stamps onto proxy nodes.
type SubGraphMeta struct {
SchemaVersion int `json:"schema_version"`
FetchedAt time.Time `json:"fetched_at"`
Truncated bool `json:"truncated"`
}
// handleSubGraph serves GET /v1/subgraph?id=<id>&depth=<n>. Returns the
// requested node and its in/out neighbour ring out to depth (default 1,
// capped at subgraphMaxDepth), with FULL node bodies so a remote daemon
// can hydrate a proxy node's neighbours. Read-only; obeys the same auth
// rule as the rest of /v1 (enforced by the WithAuth wrapper).
func (h *Handler) handleSubGraph(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(r.URL.Query().Get("id"))
if id == "" {
WriteJSONError(w, http.StatusBadRequest, "id query parameter is required")
return
}
depth := 1
if d := strings.TrimSpace(r.URL.Query().Get("depth")); d != "" {
if v, err := strconv.Atoi(d); err == nil && v > 0 {
depth = v
}
}
if depth > subgraphMaxDepth {
depth = subgraphMaxDepth
}
root := h.graph.GetNode(id)
if root == nil {
WriteJSONError(w, http.StatusNotFound, fmt.Sprintf("node %q not found", id))
return
}
visited := map[string]bool{id: true}
seenEdge := map[string]bool{}
var nodes []*graph.Node
var edges []*graph.Edge
truncated := false
addEdge := func(e *graph.Edge) {
key := e.From + "\x00" + e.To + "\x00" + string(e.Kind)
if seenEdge[key] {
return
}
seenEdge[key] = true
edges = append(edges, e)
}
addNode := func(nid string) bool {
if visited[nid] {
return true
}
if len(nodes) >= subgraphNodeCap {
truncated = true
return false
}
visited[nid] = true
if n := h.graph.GetNode(nid); n != nil {
nodes = append(nodes, n)
}
return true
}
frontier := []string{id}
for d := 0; d < depth && !truncated; d++ {
var next []string
for _, nid := range frontier {
for _, e := range h.graph.GetOutEdges(nid) {
addEdge(e)
if !visited[e.To] {
if !addNode(e.To) {
break
}
next = append(next, e.To)
}
}
if truncated {
break
}
for _, e := range h.graph.GetInEdges(nid) {
addEdge(e)
if !visited[e.From] {
if !addNode(e.From) {
break
}
next = append(next, e.From)
}
}
if truncated {
break
}
}
frontier = next
}
WriteJSON(w, http.StatusOK, SubGraphResponse{
Root: root,
Nodes: nodes,
Edges: edges,
Stats: SubGraphMeta{
SchemaVersion: SchemaVersion,
FetchedAt: time.Now(),
Truncated: truncated,
},
})
}