[DEPRECATED] Go implementation of plcbundle
1package server
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "runtime"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/goccy/go-json"
14 "tangled.org/atscan.net/plcbundle-go/internal/plcclient"
15 "tangled.org/atscan.net/plcbundle-go/internal/types"
16)
17
18func (s *Server) handleRoot() http.HandlerFunc {
19 return func(w http.ResponseWriter, r *http.Request) {
20 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
21
22 index := s.manager.GetIndex()
23 stats := index.GetStats()
24 bundleCount := stats["bundle_count"].(int)
25
26 baseURL := getBaseURL(r)
27 wsURL := getWSURL(r)
28
29 var sb strings.Builder
30
31 sb.WriteString(`
32
33 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⡀⠀⠀⠀⠀⠀⠀⢀⠀⠀⡀⠀⢀⠀⢀⡀⣤⡢⣤⡤⡀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
34⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡄⡄⠐⡀⠈⣀⠀⡠⡠⠀⣢⣆⢌⡾⢙⠺⢽⠾⡋⣻⡷⡫⢵⣭⢦⣴⠦⠀⢠⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
35⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣽⣥⡈⠧⣂⢧⢾⠕⠞⠡⠊⠁⣐⠉⠀⠉⢍⠀⠉⠌⡉⠀⠂⠁⠱⠉⠁⢝⠻⠎⣬⢌⡌⣬⣡⣀⣢⣄⡄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀
36⠀⠀⠀⠀⠀⠀⠀⢀⢸⣿⣿⢿⣾⣯⣑⢄⡂⠀⠄⠂⠀⠀⢀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠄⠐⠀⠀⠀⠀⣄⠭⠂⠈⠜⣩⣿⢝⠃⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀
37⠀⠀⠀⠀⠀⠀⠀⢀⣻⡟⠏⠀⠚⠈⠚⡉⡝⢶⣱⢤⣅⠈⠀⠄⠀⠀⠀⠀⠀⠠⠀⠀⡂⠐⣤⢕⡪⢼⣈⡹⡇⠏⠏⠋⠅⢃⣪⡏⡇⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀
38⠀⠀⠀⠀⠀⠀⠀⠀⠺⣻⡄⠀⠀⠀⢠⠌⠃⠐⠉⢡⠱⠧⠝⡯⣮⢶⣴⣤⡆⢐⣣⢅⣮⡟⠦⠍⠉⠀⠁⠐⠀⠀⠀⠄⠐⠡⣽⡸⣎⢁⠀⠀⠀⠀⠀⠀⠀⠀⠀
39⠀⠀⠀⠀⠀⠀⠀⢈⡻⣧⠀⠁⠐⠀⠀⠀⠀⠀⠀⠊⠀⠕⢀⡉⠈⡫⠽⡿⡟⠿⠟⠁⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠬⠥⣋⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
40⠀⠀⠀⠀⠀⠀⠀⡀⣾⡍⠕⡀⠀⠀⠀⠄⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠥⣤⢌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠄⢀⠀⢝⢞⣫⡆⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀
41⠀⠀⠀⠀⠀⠀⠀⠀⣽⡶⡄⠐⡀⠀⠀⠀⠀⠀⠀⢀⠀⠄⠀⠀⠀⠄⠁⠇⣷⡆⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡸⢝⣮⠍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
42⠀⠀⠀⠀⠀⠀⢀⠀⢾⣷⠀⠠⡀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠁⡁⠀⠀⣾⡥⠖⠀⠀⠀⠂⠀⠀⠀⠀⠀⠁⠀⡀⠁⠀⠀⠻⢳⣻⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
43⠀⠀⠀⠀⠀⠀⠀⠀⣞⡙⠨⣀⠠⠄⠀⠂⠀⠀⠀⠈⢀⠀⠀⠀⠀⠀⠤⢚⢢⣟⠀⠀⠀⠀⡐⠀⠀⡀⠀⠀⠀⠀⠁⠈⠌⠊⣯⣮⡏⠡⠂⠀⠀⠀⠀⠀⠀⠀⠀
44⠀⠀⠀⠀⠀⠀⠀⠀⣻⡟⡄⡡⣄⠀⠠⠀⠀⡅⠀⠐⠀⡀⠀⡀⠀⠄⠈⠃⠳⠪⠤⠀⠀⠀⠀⡀⠀⠂⠀⠀⠀⠁⠈⢠⣠⠒⠻⣻⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
45⠀⠀⠀⠀⠀⠀⠀⠀⠪⡎⠠⢌⠑⡀⠂⠀⠄⠠⠀⠠⠀⠁⡀⠠⠠⡀⣀⠜⢏⡅⠀⠀⡀⠁⠀⠀⠁⠁⠐⠄⡀⢀⠂⠀⠄⢑⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
46⠀⠀⠀⠀⠀⠀⠀⠀⠼⣻⠧⣣⣀⠐⠨⠁⠕⢈⢀⢀⡁⠀⠈⠠⢀⠀⠐⠜⣽⡗⡤⠀⠂⠀⠠⠀⢂⠠⠀⠁⠄⠀⠔⠀⠑⣨⣿⢯⠋⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀
47⠀⠀⠀⠀⠀⠀⠀⠀⡚⣷⣭⠎⢃⡗⠄⡄⢀⠁⠀⠅⢀⢅⡀⠠⠀⢠⡀⡩⠷⢇⠀⡀⠄⡠⠤⠆⣀⡀⠄⠉⣠⠃⠴⠀⠈⢁⣿⡛⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
48⠀⠀⠀⠀⠀⠀⠀⠘⡬⡿⣿⡏⡻⡯⠌⢁⢛⠠⠓⠐⠐⠐⠌⠃⠋⠂⡢⢰⣈⢏⣰⠂⠈⠀⠠⠒⠡⠌⠫⠭⠩⠢⡬⠆⠿⢷⢿⡽⡧⠉⠊⠀⠀⠀⠀⠀⠀⠀⠀
49⠀⠀⠀⠀⠀⠀⠀⠀⠺⣷⣺⣗⣿⡶⡎⡅⣣⢎⠠⡅⣢⡖⠴⠬⡈⠂⡨⢡⠾⣣⣢⠀⠀⡹⠄⡄⠄⡇⣰⡖⡊⠔⢹⣄⣿⣭⣵⣿⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
50⠀⠀⠀⠀⠀⠀⠀⠀⠩⣿⣿⣲⣿⣷⣟⣼⠟⣬⢉⡠⣪⢜⣂⣁⠥⠓⠚⡁⢶⣷⣠⠂⡄⡢⣀⡐⠧⢆⣒⡲⡳⡫⢟⡃⢪⡧⣟⡟⣯⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀
51⠀⠀⠀⠀⠀⠀⠀⠀⢺⠟⢿⢟⢻⡗⡮⡿⣲⢷⣆⣏⣇⡧⣄⢖⠾⡷⣿⣤⢳⢷⣣⣦⡜⠗⣭⢂⠩⣹⢿⡲⢎⡧⣕⣖⣓⣽⡿⡖⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
52⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠂⠂⠏⠿⢻⣥⡪⢽⣳⣳⣥⡶⣫⣍⢐⣥⣻⣾⡻⣅⢭⡴⢭⣿⠕⣧⡭⣞⣻⣣⣻⢿⠟⠛⠙⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
53⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠋⠫⠯⣍⢻⣿⣿⣷⣕⣵⣹⣽⣿⣷⣇⡏⣿⡿⣍⡝⠵⠯⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
54⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠠⠁⠋⢣⠓⡍⣫⠹⣿⣿⣷⡿⠯⠺⠁⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
55⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠋⢀⠋⢈⡿⠿⠁⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
56
57 plcbundle server
58
59*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
60| ⚠️ Preview Version – Do Not Use In Production! |
61*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*
62| This project and plcbundle specification is currently |
63| unstable and under heavy development. Things can break at |
64| any time. Do not use this for production systems. |
65| Please wait for the 1.0 release. |
66|________________________________________________________________|
67
68`)
69
70 sb.WriteString("\nplcbundle server\n\n")
71 sb.WriteString("What is PLC Bundle?\n")
72 sb.WriteString("━━━━━━━━━━━━━━━━━━━━\n")
73 sb.WriteString("plcbundle archives AT Protocol's DID PLC Directory operations into\n")
74 sb.WriteString("immutable, cryptographically-chained bundles of 10,000 operations.\n\n")
75 sb.WriteString("More info: https://tangled.org/@atscan.net/plcbundle\n\n")
76
77 origin := s.manager.GetPLCOrigin()
78
79 if bundleCount > 0 {
80 sb.WriteString("Bundles\n")
81 sb.WriteString("━━━━━━━\n")
82 sb.WriteString(fmt.Sprintf(" Origin: %s\n", origin))
83 sb.WriteString(fmt.Sprintf(" Bundle count: %d\n", bundleCount))
84
85 firstBundle := stats["first_bundle"].(int)
86 lastBundle := stats["last_bundle"].(int)
87 totalSize := stats["total_size"].(int64)
88 totalUncompressed := stats["total_uncompressed_size"].(int64)
89
90 sb.WriteString(fmt.Sprintf(" Last bundle: %d (%s)\n", lastBundle,
91 stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")))
92 sb.WriteString(fmt.Sprintf(" Range: %06d - %06d\n", firstBundle, lastBundle))
93 sb.WriteString(fmt.Sprintf(" Total size: %.2f MB\n", float64(totalSize)/(1000*1000)))
94 sb.WriteString(fmt.Sprintf(" Uncompressed: %.2f MB (%.2fx)\n",
95 float64(totalUncompressed)/(1000*1000),
96 float64(totalUncompressed)/float64(totalSize)))
97
98 if gaps, ok := stats["gaps"].(int); ok && gaps > 0 {
99 sb.WriteString(fmt.Sprintf(" ⚠ Gaps: %d missing bundles\n", gaps))
100 }
101
102 firstMeta, err := index.GetBundle(firstBundle)
103 if err == nil {
104 sb.WriteString(fmt.Sprintf("\n Root: %s\n", firstMeta.Hash))
105 }
106
107 lastMeta, err := index.GetBundle(lastBundle)
108 if err == nil {
109 sb.WriteString(fmt.Sprintf(" Head: %s\n", lastMeta.Hash))
110 }
111 }
112
113 if s.config.SyncMode {
114 mempoolStats := s.manager.GetMempoolStats()
115 count := mempoolStats["count"].(int)
116 targetBundle := mempoolStats["target_bundle"].(int)
117
118 sb.WriteString("\nMempool\n")
119 sb.WriteString("━━━━━━━\n")
120 sb.WriteString(fmt.Sprintf(" Target bundle: %d\n", targetBundle))
121 sb.WriteString(fmt.Sprintf(" Operations: %d / %d\n", count, types.BUNDLE_SIZE))
122
123 if count > 0 {
124 progress := float64(count) / float64(types.BUNDLE_SIZE) * 100
125 sb.WriteString(fmt.Sprintf(" Progress: %.1f%%\n", progress))
126
127 barWidth := 50
128 filled := int(float64(barWidth) * float64(count) / float64(types.BUNDLE_SIZE))
129 if filled > barWidth {
130 filled = barWidth
131 }
132 bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
133 sb.WriteString(fmt.Sprintf(" [%s]\n", bar))
134
135 if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
136 sb.WriteString(fmt.Sprintf(" First op: %s\n", firstTime.Format("2006-01-02 15:04:05")))
137 }
138 if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
139 sb.WriteString(fmt.Sprintf(" Last op: %s\n", lastTime.Format("2006-01-02 15:04:05")))
140 }
141 } else {
142 sb.WriteString(" (empty)\n")
143 }
144 }
145
146 if s.config.EnableResolver {
147
148 sb.WriteString("\nResolver\n")
149 sb.WriteString("━━━━━━━━\n")
150 sb.WriteString(" Status: enabled\n")
151
152 if didStats := s.manager.GetDIDIndexStats(); didStats["exists"].(bool) {
153 indexedDIDs := didStats["indexed_dids"].(int64)
154 mempoolDIDs := didStats["mempool_dids"].(int64)
155 totalDIDs := didStats["total_dids"].(int64)
156
157 if mempoolDIDs > 0 {
158 sb.WriteString(fmt.Sprintf(" Total DIDs: %s (%s indexed + %s mempool)\n",
159 formatNumber(int(totalDIDs)),
160 formatNumber(int(indexedDIDs)),
161 formatNumber(int(mempoolDIDs))))
162 } else {
163 sb.WriteString(fmt.Sprintf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))))
164 }
165 }
166 sb.WriteString("\n")
167 }
168
169 sb.WriteString("Server Stats\n")
170 sb.WriteString("━━━━━━━━━━━━\n")
171 sb.WriteString(fmt.Sprintf(" Version: %s\n", s.config.Version))
172 sb.WriteString(fmt.Sprintf(" Sync mode: %v\n", s.config.SyncMode))
173 sb.WriteString(fmt.Sprintf(" WebSocket: %v\n", s.config.EnableWebSocket))
174 sb.WriteString(fmt.Sprintf(" Handle Resolver: %v\n", s.manager.GetHandleResolver().GetBaseURL()))
175 sb.WriteString(fmt.Sprintf(" Uptime: %s\n", time.Since(s.startTime).Round(time.Second)))
176
177 sb.WriteString("\n\nAPI Endpoints\n")
178 sb.WriteString("━━━━━━━━━━━━━\n")
179 sb.WriteString(" GET / This info page\n")
180 sb.WriteString(" GET /index.json Full bundle index\n")
181 sb.WriteString(" GET /bundle/:number Bundle metadata (JSON)\n")
182 sb.WriteString(" GET /data/:number Raw bundle (zstd compressed)\n")
183 sb.WriteString(" GET /jsonl/:number Decompressed JSONL stream\n")
184 sb.WriteString(" GET /op/:pointer Get single operation\n")
185 sb.WriteString(" GET /status Server status\n")
186 sb.WriteString(" GET /mempool Mempool operations (JSONL)\n")
187
188 if s.config.EnableResolver {
189 sb.WriteString("\nDID Resolution\n")
190 sb.WriteString("━━━━━━━━━━━━━━\n")
191 sb.WriteString(" GET /:did DID Document (W3C format)\n")
192 sb.WriteString(" GET /:did/data PLC State (raw format)\n")
193 sb.WriteString(" GET /:did/log/audit Operation history\n")
194 }
195
196 if s.config.EnableWebSocket {
197 sb.WriteString("\nWebSocket Endpoints\n")
198 sb.WriteString("━━━━━━━━━━━━━━━━━━━\n")
199 sb.WriteString(" WS /ws Live stream (new operations only)\n")
200 sb.WriteString(" WS /ws?cursor=0 Stream all from beginning\n")
201 sb.WriteString(" WS /ws?cursor=N Stream from cursor N\n\n")
202 sb.WriteString("Cursor Format:\n")
203 sb.WriteString(" Global record number: (bundleNumber × 10,000) + position\n")
204 sb.WriteString(" Example: 88410345 = bundle 8841, position 345\n")
205 sb.WriteString(" Default: starts from latest (skips all historical data)\n")
206
207 latestCursor := s.manager.GetCurrentCursor()
208 bundledOps := len(index.GetBundles()) * types.BUNDLE_SIZE
209 mempoolOps := latestCursor - bundledOps
210
211 if s.config.SyncMode && mempoolOps > 0 {
212 sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundled + %d mempool)\n",
213 latestCursor, bundledOps, mempoolOps))
214 } else {
215 sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundles)\n",
216 latestCursor, len(index.GetBundles())))
217 }
218 }
219
220 sb.WriteString("\nExamples\n")
221 sb.WriteString("━━━━━━━━\n")
222 sb.WriteString(fmt.Sprintf(" curl %s/bundle/1\n", baseURL))
223 sb.WriteString(fmt.Sprintf(" curl %s/data/42 -o 000042.jsonl.zst\n", baseURL))
224 sb.WriteString(fmt.Sprintf(" curl %s/jsonl/1\n", baseURL))
225 sb.WriteString(fmt.Sprintf(" curl %s/op/0\n", baseURL))
226
227 if s.config.EnableWebSocket {
228 sb.WriteString(fmt.Sprintf(" websocat %s/ws\n", wsURL))
229 sb.WriteString(fmt.Sprintf(" websocat '%s/ws?cursor=0'\n", wsURL))
230 }
231
232 if s.config.SyncMode {
233 sb.WriteString(fmt.Sprintf(" curl %s/status\n", baseURL))
234 sb.WriteString(fmt.Sprintf(" curl %s/mempool\n", baseURL))
235 }
236
237 sb.WriteString("\n────────────────────────────────────────────────────────────────\n")
238 sb.WriteString("https://tangled.org/@atscan.net/plcbundle\n")
239
240 w.Write([]byte(sb.String()))
241 }
242}
243
244func (s *Server) handleIndexJSON() http.HandlerFunc {
245 return func(w http.ResponseWriter, r *http.Request) {
246 index := s.manager.GetIndex()
247 sendJSON(w, 200, index)
248 }
249}
250
251func (s *Server) handleBundle() http.HandlerFunc {
252 return func(w http.ResponseWriter, r *http.Request) {
253 bundleNum, err := strconv.Atoi(r.PathValue("number"))
254 if err != nil {
255 sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
256 return
257 }
258
259 meta, err := s.manager.GetIndex().GetBundle(bundleNum)
260 if err != nil {
261 sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
262 return
263 }
264
265 sendJSON(w, 200, meta)
266 }
267}
268
269func (s *Server) handleBundleData() http.HandlerFunc {
270 return func(w http.ResponseWriter, r *http.Request) {
271 bundleNum, err := strconv.Atoi(r.PathValue("number"))
272 if err != nil {
273 sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
274 return
275 }
276
277 reader, err := s.manager.StreamBundleRaw(context.Background(), bundleNum)
278 if err != nil {
279 if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
280 sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
281 } else {
282 sendJSON(w, 500, map[string]string{"error": err.Error()})
283 }
284 return
285 }
286 defer reader.Close()
287
288 w.Header().Set("Content-Type", "application/zstd")
289 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl.zst", bundleNum))
290
291 io.Copy(w, reader)
292 }
293}
294
295func (s *Server) handleBundleJSONL() http.HandlerFunc {
296 return func(w http.ResponseWriter, r *http.Request) {
297 bundleNum, err := strconv.Atoi(r.PathValue("number"))
298 if err != nil {
299 sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
300 return
301 }
302
303 reader, err := s.manager.StreamBundleDecompressed(context.Background(), bundleNum)
304 if err != nil {
305 if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
306 sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
307 } else {
308 sendJSON(w, 500, map[string]string{"error": err.Error()})
309 }
310 return
311 }
312 defer reader.Close()
313
314 w.Header().Set("Content-Type", "application/x-ndjson")
315 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl", bundleNum))
316
317 io.Copy(w, reader)
318 }
319}
320
321func (s *Server) handleStatus() http.HandlerFunc {
322 return func(w http.ResponseWriter, r *http.Request) {
323 index := s.manager.GetIndex()
324 indexStats := index.GetStats()
325
326 response := StatusResponse{
327 Server: ServerStatus{
328 Version: s.config.Version,
329 UptimeSeconds: int(time.Since(s.startTime).Seconds()),
330 SyncMode: s.config.SyncMode,
331 WebSocketEnabled: s.config.EnableWebSocket,
332 ResolverEnabled: s.config.EnableResolver,
333 Origin: s.manager.GetPLCOrigin(),
334 },
335 Bundles: BundleStatus{
336 Count: indexStats["bundle_count"].(int),
337 TotalSize: indexStats["total_size"].(int64),
338 UncompressedSize: indexStats["total_uncompressed_size"].(int64),
339 UpdatedAt: indexStats["updated_at"].(time.Time),
340 },
341 }
342
343 if resolver := s.manager.GetHandleResolver(); resolver != nil {
344 response.Server.HandleResolver = resolver.GetBaseURL()
345 }
346
347 if s.config.SyncMode && s.config.SyncInterval > 0 {
348 response.Server.SyncIntervalSeconds = int(s.config.SyncInterval.Seconds())
349 }
350
351 if bundleCount := response.Bundles.Count; bundleCount > 0 {
352 firstBundle := indexStats["first_bundle"].(int)
353 lastBundle := indexStats["last_bundle"].(int)
354
355 response.Bundles.FirstBundle = firstBundle
356 response.Bundles.LastBundle = lastBundle
357 response.Bundles.StartTime = indexStats["start_time"].(time.Time)
358 response.Bundles.EndTime = indexStats["end_time"].(time.Time)
359
360 if firstMeta, err := index.GetBundle(firstBundle); err == nil {
361 response.Bundles.RootHash = firstMeta.Hash
362 }
363
364 if lastMeta, err := index.GetBundle(lastBundle); err == nil {
365 response.Bundles.HeadHash = lastMeta.Hash
366 response.Bundles.HeadAgeSeconds = int(time.Since(lastMeta.EndTime).Seconds())
367 }
368
369 if gaps, ok := indexStats["gaps"].(int); ok {
370 response.Bundles.Gaps = gaps
371 response.Bundles.HasGaps = gaps > 0
372 if gaps > 0 {
373 response.Bundles.GapNumbers = index.FindGaps()
374 }
375 }
376
377 totalOps := bundleCount * types.BUNDLE_SIZE
378 response.Bundles.TotalOperations = totalOps
379
380 duration := response.Bundles.EndTime.Sub(response.Bundles.StartTime)
381 if duration.Hours() > 0 {
382 response.Bundles.AvgOpsPerHour = int(float64(totalOps) / duration.Hours())
383 }
384 }
385
386 if s.config.SyncMode {
387 mempoolStats := s.manager.GetMempoolStats()
388
389 if count, ok := mempoolStats["count"].(int); ok {
390 mempool := &MempoolStatus{
391 Count: count,
392 TargetBundle: mempoolStats["target_bundle"].(int),
393 CanCreateBundle: mempoolStats["can_create_bundle"].(bool),
394 MinTimestamp: mempoolStats["min_timestamp"].(time.Time),
395 Validated: mempoolStats["validated"].(bool),
396 ProgressPercent: float64(count) / float64(types.BUNDLE_SIZE) * 100,
397 BundleSize: types.BUNDLE_SIZE,
398 OperationsNeeded: types.BUNDLE_SIZE - count,
399 }
400
401 if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
402 mempool.FirstTime = firstTime
403 mempool.TimespanSeconds = int(time.Since(firstTime).Seconds())
404 }
405 if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
406 mempool.LastTime = lastTime
407 mempool.LastOpAgeSeconds = int(time.Since(lastTime).Seconds())
408 }
409
410 if count > 100 && count < types.BUNDLE_SIZE {
411 if !mempool.FirstTime.IsZero() && !mempool.LastTime.IsZero() {
412 timespan := mempool.LastTime.Sub(mempool.FirstTime)
413 if timespan.Seconds() > 0 {
414 opsPerSec := float64(count) / timespan.Seconds()
415 remaining := types.BUNDLE_SIZE - count
416 mempool.EtaNextBundleSeconds = int(float64(remaining) / opsPerSec)
417 }
418 }
419 }
420
421 response.Mempool = mempool
422 }
423 }
424
425 // DID Index stats
426 didStats := s.manager.GetDIDIndexStats()
427 if didStats["enabled"].(bool) {
428 didIndex := &DIDIndexStatus{
429 Enabled: true,
430 Exists: didStats["exists"].(bool),
431 TotalDIDs: didStats["total_dids"].(int64),
432 IndexedDIDs: didStats["indexed_dids"].(int64),
433 LastBundle: didStats["last_bundle"].(int),
434 ShardCount: didStats["shard_count"].(int),
435 CachedShards: didStats["cached_shards"].(int),
436 CacheLimit: didStats["cache_limit"].(int),
437 UpdatedAt: didStats["updated_at"].(time.Time),
438 }
439
440 // Mempool DIDs
441 if mempoolDIDs, ok := didStats["mempool_dids"].(int64); ok && mempoolDIDs > 0 {
442 didIndex.MempoolDIDs = mempoolDIDs
443 }
444
445 // Version and format
446 if s.manager.GetDIDIndex() != nil {
447 config := s.manager.GetDIDIndex().GetConfig()
448 didIndex.Version = config.Version
449 didIndex.Format = config.Format
450 }
451
452 // Hot shards
453 if cacheOrder, ok := didStats["cache_order"].([]int); ok && len(cacheOrder) > 0 {
454 maxShards := 10
455 if len(cacheOrder) < maxShards {
456 maxShards = len(cacheOrder)
457 }
458 didIndex.HotShards = cacheOrder[:maxShards]
459 }
460
461 // Cache performance
462 if cacheHitRate, ok := didStats["cache_hit_rate"].(float64); ok {
463 didIndex.CacheHitRate = cacheHitRate
464 }
465 if cacheHits, ok := didStats["cache_hits"].(int64); ok {
466 didIndex.CacheHits = cacheHits
467 }
468 if cacheMisses, ok := didStats["cache_misses"].(int64); ok {
469 didIndex.CacheMisses = cacheMisses
470 }
471 if totalLookups, ok := didStats["total_lookups"].(int64); ok {
472 didIndex.TotalLookups = totalLookups
473 }
474
475 // Lookup performance metrics
476 if avgTime, ok := didStats["avg_lookup_time_ms"].(float64); ok {
477 didIndex.AvgLookupTimeMs = avgTime
478 }
479 if recentAvg, ok := didStats["recent_avg_lookup_time_ms"].(float64); ok {
480 didIndex.RecentAvgLookupTimeMs = recentAvg
481 }
482 if minTime, ok := didStats["min_lookup_time_ms"].(float64); ok {
483 didIndex.MinLookupTimeMs = minTime
484 }
485 if maxTime, ok := didStats["max_lookup_time_ms"].(float64); ok {
486 didIndex.MaxLookupTimeMs = maxTime
487 }
488 if p50, ok := didStats["p50_lookup_time_ms"].(float64); ok {
489 didIndex.P50LookupTimeMs = p50
490 }
491 if p95, ok := didStats["p95_lookup_time_ms"].(float64); ok {
492 didIndex.P95LookupTimeMs = p95
493 }
494 if p99, ok := didStats["p99_lookup_time_ms"].(float64); ok {
495 didIndex.P99LookupTimeMs = p99
496 }
497 if sampleSize, ok := didStats["recent_sample_size"].(int); ok {
498 didIndex.RecentSampleSize = sampleSize
499 }
500
501 response.DIDIndex = didIndex
502 }
503
504 // Resolver performance stats
505 if s.config.EnableResolver {
506 resolverStats := s.manager.GetResolverStats()
507
508 if totalRes, ok := resolverStats["total_resolutions"].(int64); ok && totalRes > 0 {
509 resolver := &ResolverStatus{
510 Enabled: true,
511 TotalResolutions: totalRes,
512 }
513
514 // Handle resolver URL
515 if hr := s.manager.GetHandleResolver(); hr != nil {
516 resolver.HandleResolver = hr.GetBaseURL()
517 }
518
519 // Counts
520 if v, ok := resolverStats["mempool_hits"].(int64); ok {
521 resolver.MempoolHits = v
522 }
523 if v, ok := resolverStats["bundle_hits"].(int64); ok {
524 resolver.BundleHits = v
525 }
526 if v, ok := resolverStats["errors"].(int64); ok {
527 resolver.Errors = v
528 }
529 if v, ok := resolverStats["success_rate"].(float64); ok {
530 resolver.SuccessRate = v
531 }
532 if v, ok := resolverStats["mempool_hit_rate"].(float64); ok {
533 resolver.MempoolHitRate = v
534 }
535
536 // Overall averages
537 if v, ok := resolverStats["avg_total_time_ms"].(float64); ok {
538 resolver.AvgTotalTimeMs = v
539 }
540 if v, ok := resolverStats["avg_mempool_time_ms"].(float64); ok {
541 resolver.AvgMempoolTimeMs = v
542 }
543 if v, ok := resolverStats["avg_index_time_ms"].(float64); ok {
544 resolver.AvgIndexTimeMs = v
545 }
546 if v, ok := resolverStats["avg_load_op_time_ms"].(float64); ok {
547 resolver.AvgLoadOpTimeMs = v
548 }
549
550 // Recent averages
551 if v, ok := resolverStats["recent_avg_total_time_ms"].(float64); ok {
552 resolver.RecentAvgTotalTimeMs = v
553 }
554 if v, ok := resolverStats["recent_avg_mempool_time_ms"].(float64); ok {
555 resolver.RecentAvgMempoolTimeMs = v
556 }
557 if v, ok := resolverStats["recent_avg_index_time_ms"].(float64); ok {
558 resolver.RecentAvgIndexTimeMs = v
559 }
560 if v, ok := resolverStats["recent_avg_load_time_ms"].(float64); ok {
561 resolver.RecentAvgLoadTimeMs = v
562 }
563 if v, ok := resolverStats["recent_sample_size"].(int); ok {
564 resolver.RecentSampleSize = v
565 }
566
567 // Percentiles
568 if v, ok := resolverStats["min_total_time_ms"].(float64); ok {
569 resolver.MinTotalTimeMs = v
570 }
571 if v, ok := resolverStats["max_total_time_ms"].(float64); ok {
572 resolver.MaxTotalTimeMs = v
573 }
574 if v, ok := resolverStats["p50_total_time_ms"].(float64); ok {
575 resolver.P50TotalTimeMs = v
576 }
577 if v, ok := resolverStats["p95_total_time_ms"].(float64); ok {
578 resolver.P95TotalTimeMs = v
579 }
580 if v, ok := resolverStats["p99_total_time_ms"].(float64); ok {
581 resolver.P99TotalTimeMs = v
582 }
583 if v, ok := resolverStats["p95_index_time_ms"].(float64); ok {
584 resolver.P95IndexTimeMs = v
585 }
586 if v, ok := resolverStats["p95_load_op_time_ms"].(float64); ok {
587 resolver.P95LoadOpTimeMs = v
588 }
589
590 response.Resolver = resolver
591 } else {
592 // No resolutions yet, but resolver is enabled
593 response.Resolver = &ResolverStatus{
594 Enabled: true,
595 TotalResolutions: 0,
596 }
597
598 if hr := s.manager.GetHandleResolver(); hr != nil {
599 response.Resolver.HandleResolver = hr.GetBaseURL()
600 }
601 }
602 }
603
604 sendJSON(w, 200, response)
605 }
606}
607
608func (s *Server) handleMempool() http.HandlerFunc {
609 return func(w http.ResponseWriter, r *http.Request) {
610 ops, err := s.manager.GetMempoolOperations()
611 if err != nil {
612 sendJSON(w, 500, map[string]string{"error": err.Error()})
613 return
614 }
615
616 w.Header().Set("Content-Type", "application/x-ndjson")
617
618 if len(ops) == 0 {
619 return
620 }
621
622 for _, op := range ops {
623 if len(op.RawJSON) > 0 {
624 w.Write(op.RawJSON)
625 } else {
626 data, _ := json.Marshal(op)
627 w.Write(data)
628 }
629 w.Write([]byte("\n"))
630 }
631 }
632}
633
634func (s *Server) handleDebugMemory() http.HandlerFunc {
635 return func(w http.ResponseWriter, r *http.Request) {
636 var m runtime.MemStats
637 runtime.ReadMemStats(&m)
638
639 didStats := s.manager.GetDIDIndexStats()
640
641 beforeAlloc := m.Alloc / 1024 / 1024
642
643 runtime.GC()
644 runtime.ReadMemStats(&m)
645 afterAlloc := m.Alloc / 1024 / 1024
646
647 response := fmt.Sprintf(`Memory Stats:
648 Alloc: %d MB
649 TotalAlloc: %d MB
650 Sys: %d MB
651 NumGC: %d
652
653DID Index:
654 Cached shards: %d/%d
655
656After GC:
657 Alloc: %d MB
658`,
659 beforeAlloc,
660 m.TotalAlloc/1024/1024,
661 m.Sys/1024/1024,
662 m.NumGC,
663 didStats["cached_shards"],
664 didStats["cache_limit"],
665 afterAlloc)
666
667 w.Header().Set("Content-Type", "text/plain")
668 w.Write([]byte(response))
669 }
670}
671
672func (s *Server) handleDIDRouting(w http.ResponseWriter, r *http.Request) {
673 path := strings.TrimPrefix(r.URL.Path, "/")
674
675 parts := strings.SplitN(path, "/", 2)
676 input := parts[0]
677
678 // Ignore common browser files before any validation
679 if isCommonBrowserFile(input) {
680 w.WriteHeader(http.StatusNotFound)
681 return
682 }
683
684 // Quick validation: must be either a DID or a valid handle format
685 if !isValidDIDOrHandle(input) {
686 sendJSON(w, 404, map[string]string{"error": "not found"})
687 return
688 }
689
690 // Route to appropriate handler
691 if len(parts) == 1 {
692 s.handleDIDDocument(input)(w, r)
693 } else if parts[1] == "data" {
694 s.handleDIDData(input)(w, r)
695 } else if parts[1] == "log/audit" {
696 s.handleDIDAuditLog(input)(w, r)
697 } else {
698 sendJSON(w, 404, map[string]string{"error": "not found"})
699 }
700}
701
702func isCommonBrowserFile(path string) bool {
703 // Common files browsers request automatically
704 commonFiles := []string{
705 "favicon.ico",
706 "robots.txt",
707 "sitemap.xml",
708 "apple-touch-icon.png",
709 "apple-touch-icon-precomposed.png",
710 ".well-known",
711 }
712
713 for _, file := range commonFiles {
714 if path == file || strings.HasPrefix(path, file) {
715 return true
716 }
717 }
718
719 // Common file extensions that are NOT DIDs/handles
720 commonExtensions := []string{
721 ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg",
722 ".css", ".js", ".woff", ".woff2", ".ttf", ".eot",
723 ".xml", ".txt", ".html", ".webmanifest",
724 }
725
726 for _, ext := range commonExtensions {
727 if strings.HasSuffix(path, ext) {
728 return true
729 }
730 }
731
732 return false
733}
734
735// isValidDIDOrHandle does quick format check before expensive resolution
736func isValidDIDOrHandle(input string) bool {
737 // Empty input
738 if input == "" {
739 return false
740 }
741
742 // If it's a DID
743 if strings.HasPrefix(input, "did:") {
744 // Only accept did:plc: method (reject other methods at routing level)
745 if !strings.HasPrefix(input, "did:plc:") {
746 return false // Returns 404 for did:web:, did:key:, did:invalid:, etc
747 }
748
749 // Accept any did:plc:* - let handler validate exact format
750 // This allows invalid formats to reach handler and get proper 400 errors
751 return true
752 }
753
754 // Not a DID - validate as handle
755 // Must have at least one dot (domain.tld)
756 if !strings.Contains(input, ".") {
757 return false
758 }
759
760 // Must not have invalid characters for a domain
761 // Simple check: alphanumeric, dots, hyphens only
762 for _, c := range input {
763 if !((c >= 'a' && c <= 'z') ||
764 (c >= 'A' && c <= 'Z') ||
765 (c >= '0' && c <= '9') ||
766 c == '.' || c == '-') {
767 return false
768 }
769 }
770
771 // Basic length check (DNS max is 253)
772 if len(input) > 253 {
773 return false
774 }
775
776 // Must not start or end with dot or hyphen
777 if strings.HasPrefix(input, ".") || strings.HasSuffix(input, ".") ||
778 strings.HasPrefix(input, "-") || strings.HasSuffix(input, "-") {
779 return false
780 }
781
782 return true
783}
784
785func (s *Server) handleDIDDocument(input string) http.HandlerFunc {
786 return func(w http.ResponseWriter, r *http.Request) {
787 if r.Method == "OPTIONS" {
788 return
789 }
790
791 // Resolve handle to DID
792 did, handleResolveTime, err := s.manager.ResolveHandleOrDID(r.Context(), input)
793 if err != nil {
794 if strings.Contains(err.Error(), "appears to be a handle") {
795 sendJSON(w, 400, map[string]string{
796 "error": "Handle resolver not configured",
797 "hint": "Start server with --handle-resolver flag",
798 })
799 } else {
800 sendJSON(w, 400, map[string]string{"error": err.Error()})
801 }
802 return
803 }
804
805 resolvedHandle := ""
806 if handleResolveTime > 0 {
807 resolvedHandle = input
808 }
809
810 // Single call gets both document AND operation metadata
811 result, err := s.manager.ResolveDID(r.Context(), did)
812 if err != nil {
813 if strings.Contains(err.Error(), "deactivated") {
814 sendJSON(w, 410, map[string]string{"error": "DID has been deactivated"})
815 } else if strings.Contains(err.Error(), "not found") {
816 sendJSON(w, 404, map[string]string{"error": "DID not found"})
817 } else {
818 sendJSON(w, 500, map[string]string{"error": err.Error()})
819 }
820 return
821 }
822
823 // Early ETag check - operation is already in result.LatestOperation
824 if result.LatestOperation != nil {
825 etag := fmt.Sprintf(`"%s"`, result.LatestOperation.CID)
826
827 if match := r.Header.Get("If-None-Match"); match != "" {
828 // Strip quotes if present
829 matchClean := strings.Trim(match, `"`)
830 if matchClean == result.LatestOperation.CID {
831 // Set minimal headers for 304 response
832 w.Header().Set("ETag", etag)
833 w.Header().Set("Cache-Control", "public, max-age=300")
834 w.WriteHeader(http.StatusNotModified)
835 return
836 }
837 }
838 }
839
840 // Set all headers (now with result.LatestOperation available)
841 setDIDDocumentHeaders(w, r, did, resolvedHandle, result, handleResolveTime)
842
843 w.Header().Set("Content-Type", "application/did+ld+json")
844 sendJSON(w, 200, result.Document)
845 }
846}
847
848func (s *Server) handleDIDData(input string) http.HandlerFunc {
849 return func(w http.ResponseWriter, r *http.Request) {
850 // Resolve handle to DID
851 did, _, err := s.manager.ResolveHandleOrDID(r.Context(), input)
852 if err != nil {
853 sendJSON(w, 400, map[string]string{"error": err.Error()})
854 return
855 }
856
857 operations, _, err := s.manager.GetDIDOperations(context.Background(), did, false)
858 if err != nil {
859 sendJSON(w, 500, map[string]string{"error": err.Error()})
860 return
861 }
862
863 if len(operations) == 0 {
864 sendJSON(w, 404, map[string]string{"error": "DID not found"})
865 return
866 }
867
868 state, err := plcclient.BuildDIDState(did, operations)
869 if err != nil {
870 if strings.Contains(err.Error(), "deactivated") {
871 sendJSON(w, 410, map[string]string{"error": "DID has been deactivated"})
872 } else {
873 sendJSON(w, 500, map[string]string{"error": err.Error()})
874 }
875 return
876 }
877
878 sendJSON(w, 200, state)
879 }
880}
881
882func (s *Server) handleDIDAuditLog(input string) http.HandlerFunc {
883 return func(w http.ResponseWriter, r *http.Request) {
884 did, _, err := s.manager.ResolveHandleOrDID(r.Context(), input)
885 if err != nil {
886 sendJSON(w, 400, map[string]string{"error": err.Error()})
887 return
888 }
889
890 operations, _, err := s.manager.GetDIDOperations(context.Background(), did, false)
891 if err != nil {
892 sendJSON(w, 500, map[string]string{"error": err.Error()})
893 return
894 }
895
896 if len(operations) == 0 {
897 sendJSON(w, 404, map[string]string{"error": "DID not found"})
898 return
899 }
900
901 auditLog := plcclient.FormatAuditLog(operations)
902 sendJSON(w, 200, auditLog)
903 }
904}
905
906// handleOperation gets a single operation with detailed timing headers
907func (s *Server) handleOperation() http.HandlerFunc {
908 return func(w http.ResponseWriter, r *http.Request) {
909 pointer := r.PathValue("pointer")
910
911 // Parse pointer format: "bundle:position" or global position
912 bundleNum, position, err := parseOperationPointer(pointer)
913 if err != nil {
914 sendJSON(w, 400, map[string]string{"error": err.Error()})
915 return
916 }
917
918 // Validate position range
919 if position < 0 || position >= types.BUNDLE_SIZE {
920 sendJSON(w, 400, map[string]string{
921 "error": fmt.Sprintf("Position must be 0-%d", types.BUNDLE_SIZE-1),
922 })
923 return
924 }
925
926 // Time the entire request
927 totalStart := time.Now()
928
929 // Time the operation load
930 loadStart := time.Now()
931 op, err := s.manager.LoadOperation(r.Context(), bundleNum, position)
932 loadDuration := time.Since(loadStart)
933
934 if err != nil {
935 if strings.Contains(err.Error(), "not in index") ||
936 strings.Contains(err.Error(), "not found") {
937 sendJSON(w, 404, map[string]string{"error": "Operation not found"})
938 } else {
939 sendJSON(w, 500, map[string]string{"error": err.Error()})
940 }
941 return
942 }
943
944 totalDuration := time.Since(totalStart)
945
946 // Calculate global position
947 globalPos := (bundleNum * types.BUNDLE_SIZE) + position
948
949 // Calculate operation age
950 opAge := time.Since(op.CreatedAt)
951
952 // Set response headers with useful metadata
953 setOperationHeaders(w, op, bundleNum, position, globalPos, loadDuration, totalDuration, opAge)
954
955 // Send raw JSON if available (faster, preserves exact format)
956 if len(op.RawJSON) > 0 {
957 w.Header().Set("Content-Type", "application/json")
958 w.Write(op.RawJSON)
959 } else {
960 sendJSON(w, 200, op)
961 }
962 }
963}
964
965// parseOperationPointer parses pointer in format "bundle:position" or global position
966func parseOperationPointer(pointer string) (bundleNum, position int, err error) {
967 // Check if it's the "bundle:position" format
968 if strings.Contains(pointer, ":") {
969 parts := strings.Split(pointer, ":")
970 if len(parts) != 2 {
971 return 0, 0, fmt.Errorf("invalid pointer format: use 'bundle:position' or global position")
972 }
973
974 bundleNum, err = strconv.Atoi(parts[0])
975 if err != nil {
976 return 0, 0, fmt.Errorf("invalid bundle number: %w", err)
977 }
978
979 position, err = strconv.Atoi(parts[1])
980 if err != nil {
981 return 0, 0, fmt.Errorf("invalid position: %w", err)
982 }
983
984 if bundleNum < 1 {
985 return 0, 0, fmt.Errorf("bundle number must be >= 1")
986 }
987
988 return bundleNum, position, nil
989 }
990
991 // Parse as global position
992 globalPos, err := strconv.Atoi(pointer)
993 if err != nil {
994 return 0, 0, fmt.Errorf("invalid position: must be number or 'bundle:position' format")
995 }
996
997 if globalPos < 0 {
998 return 0, 0, fmt.Errorf("global position must be >= 0")
999 }
1000
1001 // Handle small numbers as shorthand for bundle 1
1002 if globalPos < types.BUNDLE_SIZE {
1003 return 1, globalPos, nil
1004 }
1005
1006 // Convert global position to bundle + position
1007 bundleNum = globalPos / types.BUNDLE_SIZE
1008 position = globalPos % types.BUNDLE_SIZE
1009
1010 // Minimum bundle number is 1
1011 if bundleNum < 1 {
1012 bundleNum = 1
1013 }
1014
1015 return bundleNum, position, nil
1016}
1017
1018// setOperationHeaders sets useful response headers
1019func setOperationHeaders(
1020 w http.ResponseWriter,
1021 op *plcclient.PLCOperation,
1022 bundleNum, position, globalPos int,
1023 loadDuration, totalDuration, opAge time.Duration,
1024) {
1025 // === Location Information ===
1026 w.Header().Set("X-Bundle-Number", fmt.Sprintf("%d", bundleNum))
1027 w.Header().Set("X-Position", fmt.Sprintf("%d", position))
1028 w.Header().Set("X-Global-Position", fmt.Sprintf("%d", globalPos))
1029 w.Header().Set("X-Pointer", fmt.Sprintf("%d:%d", bundleNum, position))
1030
1031 // === Operation Metadata ===
1032 w.Header().Set("X-Operation-DID", op.DID)
1033 w.Header().Set("X-Operation-CID", op.CID)
1034 w.Header().Set("X-Operation-Created", op.CreatedAt.Format(time.RFC3339))
1035 w.Header().Set("X-Operation-Age-Seconds", fmt.Sprintf("%d", int(opAge.Seconds())))
1036
1037 // Nullification status
1038 if op.IsNullified() {
1039 w.Header().Set("X-Operation-Nullified", "true")
1040 if nullCID := op.GetNullifyingCID(); nullCID != "" {
1041 w.Header().Set("X-Operation-Nullified-By", nullCID)
1042 }
1043 } else {
1044 w.Header().Set("X-Operation-Nullified", "false")
1045 }
1046
1047 // === Size Information ===
1048 if len(op.RawJSON) > 0 {
1049 w.Header().Set("X-Operation-Size", fmt.Sprintf("%d", len(op.RawJSON)))
1050 }
1051
1052 // === Performance Metrics (in milliseconds with precision) ===
1053 w.Header().Set("X-Load-Time-Ms", fmt.Sprintf("%.3f", float64(loadDuration.Microseconds())/1000.0))
1054 w.Header().Set("X-Total-Time-Ms", fmt.Sprintf("%.3f", float64(totalDuration.Microseconds())/1000.0))
1055
1056 // === Caching Hints ===
1057 // Set cache control (operations are immutable once bundled)
1058 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1059 w.Header().Set("ETag", op.CID) // CID is perfect for ETag
1060}
1061
1062// handleDIDIndexStats returns detailed DID index performance metrics
1063func (s *Server) handleDebugDIDIndex() http.HandlerFunc {
1064 return func(w http.ResponseWriter, r *http.Request) {
1065 didStats := s.manager.GetDIDIndexStats()
1066
1067 if !didStats["enabled"].(bool) || !didStats["exists"].(bool) {
1068 sendJSON(w, 404, map[string]string{
1069 "error": "DID index not available",
1070 })
1071 return
1072 }
1073
1074 // Return all stats (more detailed than /status)
1075 sendJSON(w, 200, didStats)
1076 }
1077}
1078
1079func (s *Server) handleDebugResolver() http.HandlerFunc {
1080 return func(w http.ResponseWriter, r *http.Request) {
1081 resolverStats := s.manager.GetResolverStats()
1082
1083 if resolverStats == nil {
1084 sendJSON(w, 404, map[string]string{
1085 "error": "Resolver not enabled",
1086 })
1087 return
1088 }
1089
1090 // Return all stats (more detailed than /status)
1091 sendJSON(w, 200, resolverStats)
1092 }
1093}