tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
back to net/http
tree.fail
4 months ago
78f261f3
23cb86b8
+549
-549
4 changed files
expand all
collapse all
unified
split
cmd
plcbundle
main.go
server.go
go.mod
go.sum
+7
-6
cmd/plcbundle/main.go
···
4
4
"context"
5
5
"flag"
6
6
"fmt"
7
7
+
"net/http"
7
8
"os"
8
9
"os/signal"
9
10
"path/filepath"
···
1446
1447
go runSync(ctx, mgr, syncInterval, *verbose, *enableResolver)
1447
1448
}
1448
1449
1449
1449
-
// Create and run server
1450
1450
-
server := newServerHandler(mgr, *sync, *enableWebSocket, *enableResolver)
1450
1450
+
handler := newServerHandler(mgr, *sync, *enableWebSocket, *enableResolver)
1451
1451
+
server := &http.Server{
1452
1452
+
Addr: addr,
1453
1453
+
Handler: handler,
1454
1454
+
}
1451
1455
1452
1452
-
// Run server (blocks until error or shutdown)
1453
1453
-
if err := server.Run(addr); err != nil {
1456
1456
+
if err := server.ListenAndServe(); err != nil {
1454
1457
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
1455
1455
-
1456
1456
-
// Ensure cleanup on error
1457
1458
mgr.SaveMempool()
1458
1459
mgr.Close()
1459
1460
os.Exit(1)
+542
-534
cmd/plcbundle/server.go
···
12
12
"strings"
13
13
"time"
14
14
15
15
-
"git.urbach.dev/go/web"
16
15
"github.com/goccy/go-json"
17
16
"github.com/gorilla/websocket"
18
17
···
33
32
var verboseMode bool
34
33
var resolverEnabled bool
35
34
36
36
-
func newServerHandler(mgr *bundle.Manager, syncMode bool, wsEnabled bool, resolverEnabled bool) web.Server {
37
37
-
s := web.NewServer()
35
35
+
// newServerHandler creates HTTP handler with all routes
36
36
+
func newServerHandler(mgr *bundle.Manager, syncMode bool, wsEnabled bool, resolverEnabled bool) http.Handler {
37
37
+
mux := http.NewServeMux()
38
38
39
39
-
// CORS middleware
40
40
-
s.Use(corsMiddleware)
39
39
+
// Specific routes first (highest priority)
40
40
+
mux.HandleFunc("GET /index.json", handleIndexJSONNative(mgr))
41
41
+
mux.HandleFunc("GET /bundle/{number}", handleBundleNative(mgr))
42
42
+
mux.HandleFunc("GET /data/{number}", handleBundleDataNative(mgr))
43
43
+
mux.HandleFunc("GET /jsonl/{number}", handleBundleJSONLNative(mgr))
44
44
+
mux.HandleFunc("GET /status", handleStatusNative(mgr, syncMode, wsEnabled))
45
45
+
mux.HandleFunc("GET /debug/memory", handleDebugMemoryNative(mgr))
41
46
42
42
-
// Root endpoint
43
43
-
s.Get("/", func(ctx web.Context) error {
44
44
-
return handleRoot(ctx, mgr, syncMode, wsEnabled, resolverEnabled)
45
45
-
})
47
47
+
// WebSocket endpoint
48
48
+
if wsEnabled {
49
49
+
mux.HandleFunc("GET /ws", handleWebSocketNative(mgr))
50
50
+
}
46
51
47
47
-
// Bundle endpoints
48
48
-
s.Get("/index.json", func(ctx web.Context) error {
49
49
-
return handleIndexJSON(ctx, mgr)
50
50
-
})
52
52
+
// Sync mode endpoints
53
53
+
if syncMode {
54
54
+
mux.HandleFunc("GET /mempool", handleMempoolNative(mgr))
55
55
+
}
51
56
52
52
-
s.Get("/bundle/:number", func(ctx web.Context) error {
53
53
-
return handleBundle(ctx, mgr)
54
54
-
})
57
57
+
// Combined root and DID resolver handler
58
58
+
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
59
59
+
path := r.URL.Path
55
60
56
56
-
s.Get("/data/:number", func(ctx web.Context) error {
57
57
-
return handleBundleData(ctx, mgr)
58
58
-
})
61
61
+
// Handle exact root
62
62
+
if path == "/" {
63
63
+
handleRootNative(mgr, syncMode, wsEnabled, resolverEnabled)(w, r)
64
64
+
return
65
65
+
}
59
66
60
60
-
s.Get("/jsonl/:number", func(ctx web.Context) error {
61
61
-
return handleBundleJSONL(ctx, mgr)
62
62
-
})
67
67
+
// Handle DID routes if enabled
68
68
+
if resolverEnabled {
69
69
+
handleDIDRouting(w, r, mgr)
70
70
+
return
71
71
+
}
63
72
64
64
-
s.Get("/status", func(ctx web.Context) error {
65
65
-
return handleStatus(ctx, mgr, syncMode, wsEnabled)
73
73
+
// 404 for everything else
74
74
+
sendJSON(w, 404, map[string]string{"error": "not found"})
66
75
})
67
76
68
68
-
s.Get("/debug/memory", func(ctx web.Context) error {
69
69
-
return handleDebugMemory(ctx, mgr)
70
70
-
})
77
77
+
// Wrap with CORS middleware
78
78
+
return corsMiddleware(mux)
79
79
+
}
80
80
+
81
81
+
// handleDIDRouting routes DID-related requests
82
82
+
func handleDIDRouting(w http.ResponseWriter, r *http.Request, mgr *bundle.Manager) {
83
83
+
path := strings.TrimPrefix(r.URL.Path, "/")
84
84
+
85
85
+
// Parse DID and sub-path
86
86
+
parts := strings.SplitN(path, "/", 2)
87
87
+
did := parts[0]
71
88
72
72
-
// WebSocket endpoint - needs special handling
73
73
-
if wsEnabled {
74
74
-
s.Get("/ws", func(ctx web.Context) error {
75
75
-
// WebSocket needs raw ResponseWriter, get it from underlying request
76
76
-
handleWebSocketRaw(ctx, mgr)
77
77
-
return nil
78
78
-
})
89
89
+
// Validate it's a DID
90
90
+
if !strings.HasPrefix(did, "did:plc:") {
91
91
+
sendJSON(w, 404, map[string]string{"error": "not found"})
92
92
+
return
79
93
}
80
94
81
81
-
// Sync mode endpoints
82
82
-
if syncMode {
83
83
-
s.Get("/mempool", func(ctx web.Context) error {
84
84
-
return handleMempool(ctx, mgr)
85
85
-
})
95
95
+
// Route based on sub-path
96
96
+
if len(parts) == 1 {
97
97
+
// /did:plc:xxx -> DID document
98
98
+
handleDIDDocumentLatestNative(mgr, did)(w, r)
99
99
+
} else if parts[1] == "data" {
100
100
+
// /did:plc:xxx/data -> PLC state
101
101
+
handleDIDDataNative(mgr, did)(w, r)
102
102
+
} else if parts[1] == "log/audit" {
103
103
+
// /did:plc:xxx/log/audit -> Audit log
104
104
+
handleDIDAuditLogNative(mgr, did)(w, r)
105
105
+
} else {
106
106
+
sendJSON(w, 404, map[string]string{"error": "not found"})
86
107
}
108
108
+
}
87
109
88
88
-
// DID resolution endpoints (must be LAST to avoid conflicts)
89
89
-
if resolverEnabled {
90
90
-
// Single catch-all handler for DID routes
91
91
-
s.Get("/*path", func(ctx web.Context) error {
92
92
-
path := ctx.Request().Param("path")
110
110
+
// corsMiddleware adds CORS headers (skips WebSocket upgrade requests)
111
111
+
func corsMiddleware(next http.Handler) http.Handler {
112
112
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113
113
+
// Check if this is a WebSocket upgrade request
114
114
+
if r.Header.Get("Upgrade") == "websocket" {
115
115
+
// Skip CORS for WebSocket - pass through directly
116
116
+
next.ServeHTTP(w, r)
117
117
+
return
118
118
+
}
93
119
94
94
-
// Remove leading slash
95
95
-
path = strings.TrimPrefix(path, "/")
120
120
+
// Normal CORS handling for non-WebSocket requests
121
121
+
w.Header().Set("Access-Control-Allow-Origin", "*")
122
122
+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
96
123
97
97
-
// Parse DID and sub-path
98
98
-
parts := strings.SplitN(path, "/", 2)
99
99
-
did := parts[0]
124
124
+
if requestedHeaders := r.Header.Get("Access-Control-Request-Headers"); requestedHeaders != "" {
125
125
+
w.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
126
126
+
} else {
127
127
+
w.Header().Set("Access-Control-Allow-Headers", "*")
128
128
+
}
100
129
101
101
-
// Validate it's a DID
102
102
-
if !strings.HasPrefix(did, "did:plc:") {
103
103
-
return sendJSON(ctx, 404, map[string]string{"error": "not found"})
104
104
-
}
130
130
+
w.Header().Set("Access-Control-Max-Age", "86400")
105
131
106
106
-
// Route based on sub-path
107
107
-
if len(parts) == 1 {
108
108
-
// /did:plc:xxx -> DID document
109
109
-
return handleDIDDocumentLatest(ctx, mgr, did)
110
110
-
} else if parts[1] == "data" {
111
111
-
// /did:plc:xxx/data -> PLC state
112
112
-
return handleDIDData(ctx, mgr, did)
113
113
-
} else if parts[1] == "log/audit" {
114
114
-
// /did:plc:xxx/log/audit -> Audit log
115
115
-
return handleDIDAuditLog(ctx, mgr, did)
116
116
-
}
132
132
+
if r.Method == "OPTIONS" {
133
133
+
w.WriteHeader(204)
134
134
+
return
135
135
+
}
117
136
118
118
-
return sendJSON(ctx, 404, map[string]string{"error": "not found"})
119
119
-
})
120
120
-
}
121
121
-
122
122
-
return s
137
137
+
next.ServeHTTP(w, r)
138
138
+
})
123
139
}
124
140
125
125
-
// Helper to send JSON responses using goccy/go-json
126
126
-
func sendJSON(ctx web.Context, statusCode int, data interface{}) error {
127
127
-
ctx.Response().SetHeader("Content-Type", "application/json")
128
128
-
129
129
-
if statusCode != 200 {
130
130
-
ctx.Status(statusCode)
131
131
-
}
141
141
+
// sendJSON sends JSON response
142
142
+
func sendJSON(w http.ResponseWriter, statusCode int, data interface{}) {
143
143
+
w.Header().Set("Content-Type", "application/json")
132
144
133
145
jsonData, err := json.Marshal(data)
134
146
if err != nil {
135
135
-
return err
147
147
+
w.WriteHeader(500)
148
148
+
w.Write([]byte(`{"error":"failed to marshal JSON"}`))
149
149
+
return
136
150
}
137
151
138
138
-
_, err = ctx.Response().Write(jsonData)
139
139
-
return err
152
152
+
w.WriteHeader(statusCode)
153
153
+
w.Write(jsonData)
140
154
}
141
155
142
142
-
// CORS middleware
143
143
-
func corsMiddleware(ctx web.Context) error {
144
144
-
ctx.Response().SetHeader("Access-Control-Allow-Origin", "*")
145
145
-
ctx.Response().SetHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
156
156
+
// Handler implementations
146
157
147
147
-
if requestedHeaders := ctx.Request().Header("Access-Control-Request-Headers"); requestedHeaders != "" {
148
148
-
ctx.Response().SetHeader("Access-Control-Allow-Headers", requestedHeaders)
149
149
-
} else {
150
150
-
ctx.Response().SetHeader("Access-Control-Allow-Headers", "*")
151
151
-
}
158
158
+
func handleRootNative(mgr *bundle.Manager, syncMode bool, wsEnabled bool, resolverEnabled bool) http.HandlerFunc {
159
159
+
return func(w http.ResponseWriter, r *http.Request) {
160
160
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
152
161
153
153
-
ctx.Response().SetHeader("Access-Control-Max-Age", "86400")
154
154
-
155
155
-
if ctx.Request().Method() == "OPTIONS" {
156
156
-
return ctx.Status(204).String("")
157
157
-
}
158
158
-
159
159
-
return ctx.Next(ctx)
160
160
-
}
161
161
-
162
162
-
func handleRoot(ctx web.Context, mgr *bundle.Manager, syncMode bool, wsEnabled bool, resolverEnabled bool) error {
163
163
-
ctx.Response().SetHeader("Content-Type", "text/plain; charset=utf-8")
164
164
-
165
165
-
index := mgr.GetIndex()
166
166
-
stats := index.GetStats()
167
167
-
bundleCount := stats["bundle_count"].(int)
162
162
+
index := mgr.GetIndex()
163
163
+
stats := index.GetStats()
164
164
+
bundleCount := stats["bundle_count"].(int)
168
165
169
169
-
baseURL := getBaseURLFromContext(ctx)
170
170
-
wsURL := getWSURLFromContext(ctx)
166
166
+
baseURL := getBaseURL(r)
167
167
+
wsURL := getWSURL(r)
171
168
172
172
-
var sb strings.Builder
169
169
+
var sb strings.Builder
173
170
174
174
-
sb.WriteString(`
171
171
+
sb.WriteString(`
175
172
176
173
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⡀⠀⠀⠀⠀⠀⠀⢀⠀⠀⡀⠀⢀⠀⢀⡀⣤⡢⣤⡤⡀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
177
174
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡄⡄⠐⡀⠈⣀⠀⡠⡠⠀⣢⣆⢌⡾⢙⠺⢽⠾⡋⣻⡷⡫⢵⣭⢦⣴⠦⠀⢠⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
···
210
207
211
208
`)
212
209
213
213
-
sb.WriteString("\nplcbundle server\n\n")
214
214
-
sb.WriteString("What is PLC Bundle?\n")
215
215
-
sb.WriteString("━━━━━━━━━━━━━━━━━━━━\n")
216
216
-
sb.WriteString("plcbundle archives AT Protocol's DID PLC Directory operations into\n")
217
217
-
sb.WriteString("immutable, cryptographically-chained bundles of 10,000 operations.\n\n")
218
218
-
sb.WriteString("More info: https://tangled.org/@atscan.net/plcbundle\n\n")
210
210
+
sb.WriteString("\nplcbundle server\n\n")
211
211
+
sb.WriteString("What is PLC Bundle?\n")
212
212
+
sb.WriteString("━━━━━━━━━━━━━━━━━━━━\n")
213
213
+
sb.WriteString("plcbundle archives AT Protocol's DID PLC Directory operations into\n")
214
214
+
sb.WriteString("immutable, cryptographically-chained bundles of 10,000 operations.\n\n")
215
215
+
sb.WriteString("More info: https://tangled.org/@atscan.net/plcbundle\n\n")
219
216
220
220
-
if bundleCount > 0 {
221
221
-
sb.WriteString("Bundles\n")
222
222
-
sb.WriteString("━━━━━━━\n")
223
223
-
sb.WriteString(fmt.Sprintf(" Bundle count: %d\n", bundleCount))
217
217
+
if bundleCount > 0 {
218
218
+
sb.WriteString("Bundles\n")
219
219
+
sb.WriteString("━━━━━━━\n")
220
220
+
sb.WriteString(fmt.Sprintf(" Bundle count: %d\n", bundleCount))
224
221
225
225
-
firstBundle := stats["first_bundle"].(int)
226
226
-
lastBundle := stats["last_bundle"].(int)
227
227
-
totalSize := stats["total_size"].(int64)
228
228
-
totalUncompressed := stats["total_uncompressed_size"].(int64)
222
222
+
firstBundle := stats["first_bundle"].(int)
223
223
+
lastBundle := stats["last_bundle"].(int)
224
224
+
totalSize := stats["total_size"].(int64)
225
225
+
totalUncompressed := stats["total_uncompressed_size"].(int64)
229
226
230
230
-
sb.WriteString(fmt.Sprintf(" Last bundle: %d (%s)\n", lastBundle,
231
231
-
stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")))
232
232
-
sb.WriteString(fmt.Sprintf(" Range: %06d - %06d\n", firstBundle, lastBundle))
233
233
-
sb.WriteString(fmt.Sprintf(" Total size: %.2f MB\n", float64(totalSize)/(1000*1000)))
234
234
-
sb.WriteString(fmt.Sprintf(" Uncompressed: %.2f MB (%.2fx)\n",
235
235
-
float64(totalUncompressed)/(1000*1000),
236
236
-
float64(totalUncompressed)/float64(totalSize)))
227
227
+
sb.WriteString(fmt.Sprintf(" Last bundle: %d (%s)\n", lastBundle,
228
228
+
stats["updated_at"].(time.Time).Format("2006-01-02 15:04:05")))
229
229
+
sb.WriteString(fmt.Sprintf(" Range: %06d - %06d\n", firstBundle, lastBundle))
230
230
+
sb.WriteString(fmt.Sprintf(" Total size: %.2f MB\n", float64(totalSize)/(1000*1000)))
231
231
+
sb.WriteString(fmt.Sprintf(" Uncompressed: %.2f MB (%.2fx)\n",
232
232
+
float64(totalUncompressed)/(1000*1000),
233
233
+
float64(totalUncompressed)/float64(totalSize)))
237
234
238
238
-
if gaps, ok := stats["gaps"].(int); ok && gaps > 0 {
239
239
-
sb.WriteString(fmt.Sprintf(" ⚠ Gaps: %d missing bundles\n", gaps))
240
240
-
}
235
235
+
if gaps, ok := stats["gaps"].(int); ok && gaps > 0 {
236
236
+
sb.WriteString(fmt.Sprintf(" ⚠ Gaps: %d missing bundles\n", gaps))
237
237
+
}
241
238
242
242
-
firstMeta, err := index.GetBundle(firstBundle)
243
243
-
if err == nil {
244
244
-
sb.WriteString(fmt.Sprintf("\n Root: %s\n", firstMeta.Hash))
245
245
-
}
239
239
+
firstMeta, err := index.GetBundle(firstBundle)
240
240
+
if err == nil {
241
241
+
sb.WriteString(fmt.Sprintf("\n Root: %s\n", firstMeta.Hash))
242
242
+
}
246
243
247
247
-
lastMeta, err := index.GetBundle(lastBundle)
248
248
-
if err == nil {
249
249
-
sb.WriteString(fmt.Sprintf(" Head: %s\n", lastMeta.Hash))
244
244
+
lastMeta, err := index.GetBundle(lastBundle)
245
245
+
if err == nil {
246
246
+
sb.WriteString(fmt.Sprintf(" Head: %s\n", lastMeta.Hash))
247
247
+
}
250
248
}
251
251
-
}
252
249
253
253
-
if syncMode {
254
254
-
mempoolStats := mgr.GetMempoolStats()
255
255
-
count := mempoolStats["count"].(int)
256
256
-
targetBundle := mempoolStats["target_bundle"].(int)
257
257
-
canCreate := mempoolStats["can_create_bundle"].(bool)
250
250
+
if syncMode {
251
251
+
mempoolStats := mgr.GetMempoolStats()
252
252
+
count := mempoolStats["count"].(int)
253
253
+
targetBundle := mempoolStats["target_bundle"].(int)
254
254
+
canCreate := mempoolStats["can_create_bundle"].(bool)
258
255
259
259
-
sb.WriteString("\nMempool Stats\n")
260
260
-
sb.WriteString("━━━━━━━━━━━━━\n")
261
261
-
sb.WriteString(fmt.Sprintf(" Target bundle: %d\n", targetBundle))
262
262
-
sb.WriteString(fmt.Sprintf(" Operations: %d / %d\n", count, bundle.BUNDLE_SIZE))
263
263
-
sb.WriteString(fmt.Sprintf(" Can create bundle: %v\n", canCreate))
256
256
+
sb.WriteString("\nMempool Stats\n")
257
257
+
sb.WriteString("━━━━━━━━━━━━━\n")
258
258
+
sb.WriteString(fmt.Sprintf(" Target bundle: %d\n", targetBundle))
259
259
+
sb.WriteString(fmt.Sprintf(" Operations: %d / %d\n", count, bundle.BUNDLE_SIZE))
260
260
+
sb.WriteString(fmt.Sprintf(" Can create bundle: %v\n", canCreate))
264
261
265
265
-
if count > 0 {
266
266
-
progress := float64(count) / float64(bundle.BUNDLE_SIZE) * 100
267
267
-
sb.WriteString(fmt.Sprintf(" Progress: %.1f%%\n", progress))
262
262
+
if count > 0 {
263
263
+
progress := float64(count) / float64(bundle.BUNDLE_SIZE) * 100
264
264
+
sb.WriteString(fmt.Sprintf(" Progress: %.1f%%\n", progress))
268
265
269
269
-
barWidth := 50
270
270
-
filled := int(float64(barWidth) * float64(count) / float64(bundle.BUNDLE_SIZE))
271
271
-
if filled > barWidth {
272
272
-
filled = barWidth
273
273
-
}
274
274
-
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
275
275
-
sb.WriteString(fmt.Sprintf(" [%s]\n", bar))
266
266
+
barWidth := 50
267
267
+
filled := int(float64(barWidth) * float64(count) / float64(bundle.BUNDLE_SIZE))
268
268
+
if filled > barWidth {
269
269
+
filled = barWidth
270
270
+
}
271
271
+
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
272
272
+
sb.WriteString(fmt.Sprintf(" [%s]\n", bar))
276
273
277
277
-
if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
278
278
-
sb.WriteString(fmt.Sprintf(" First op: %s\n", firstTime.Format("2006-01-02 15:04:05")))
279
279
-
}
280
280
-
if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
281
281
-
sb.WriteString(fmt.Sprintf(" Last op: %s\n", lastTime.Format("2006-01-02 15:04:05")))
274
274
+
if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
275
275
+
sb.WriteString(fmt.Sprintf(" First op: %s\n", firstTime.Format("2006-01-02 15:04:05")))
276
276
+
}
277
277
+
if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
278
278
+
sb.WriteString(fmt.Sprintf(" Last op: %s\n", lastTime.Format("2006-01-02 15:04:05")))
279
279
+
}
280
280
+
} else {
281
281
+
sb.WriteString(" (empty)\n")
282
282
}
283
283
-
} else {
284
284
-
sb.WriteString(" (empty)\n")
285
283
}
286
286
-
}
287
284
288
288
-
if didStats := mgr.GetDIDIndexStats(); didStats["exists"].(bool) {
289
289
-
sb.WriteString("\nDID Index\n")
290
290
-
sb.WriteString("━━━━━━━━━\n")
291
291
-
sb.WriteString(" Status: enabled\n")
285
285
+
if didStats := mgr.GetDIDIndexStats(); didStats["exists"].(bool) {
286
286
+
sb.WriteString("\nDID Index\n")
287
287
+
sb.WriteString("━━━━━━━━━\n")
288
288
+
sb.WriteString(" Status: enabled\n")
289
289
+
290
290
+
indexedDIDs := didStats["indexed_dids"].(int64)
291
291
+
mempoolDIDs := didStats["mempool_dids"].(int64)
292
292
+
totalDIDs := didStats["total_dids"].(int64)
292
293
293
293
-
indexedDIDs := didStats["indexed_dids"].(int64)
294
294
-
mempoolDIDs := didStats["mempool_dids"].(int64)
295
295
-
totalDIDs := didStats["total_dids"].(int64)
294
294
+
if mempoolDIDs > 0 {
295
295
+
sb.WriteString(fmt.Sprintf(" Total DIDs: %s (%s indexed + %s mempool)\n",
296
296
+
formatNumber(int(totalDIDs)),
297
297
+
formatNumber(int(indexedDIDs)),
298
298
+
formatNumber(int(mempoolDIDs))))
299
299
+
} else {
300
300
+
sb.WriteString(fmt.Sprintf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))))
301
301
+
}
296
302
297
297
-
if mempoolDIDs > 0 {
298
298
-
sb.WriteString(fmt.Sprintf(" Total DIDs: %s (%s indexed + %s mempool)\n",
299
299
-
formatNumber(int(totalDIDs)),
300
300
-
formatNumber(int(indexedDIDs)),
301
301
-
formatNumber(int(mempoolDIDs))))
302
302
-
} else {
303
303
-
sb.WriteString(fmt.Sprintf(" Total DIDs: %s\n", formatNumber(int(totalDIDs))))
303
303
+
sb.WriteString(fmt.Sprintf(" Cached shards: %d / %d\n",
304
304
+
didStats["cached_shards"], didStats["cache_limit"]))
305
305
+
sb.WriteString("\n")
304
306
}
305
307
306
306
-
sb.WriteString(fmt.Sprintf(" Cached shards: %d / %d\n",
307
307
-
didStats["cached_shards"], didStats["cache_limit"]))
308
308
-
sb.WriteString("\n")
309
309
-
}
308
308
+
sb.WriteString("Server Stats\n")
309
309
+
sb.WriteString("━━━━━━━━━━━━\n")
310
310
+
sb.WriteString(fmt.Sprintf(" Version: %s\n", version))
311
311
+
if origin := mgr.GetPLCOrigin(); origin != "" {
312
312
+
sb.WriteString(fmt.Sprintf(" Origin: %s\n", origin))
313
313
+
}
314
314
+
sb.WriteString(fmt.Sprintf(" Sync mode: %v\n", syncMode))
315
315
+
sb.WriteString(fmt.Sprintf(" WebSocket: %v\n", wsEnabled))
316
316
+
sb.WriteString(fmt.Sprintf(" Resolver: %v\n", resolverEnabled))
317
317
+
sb.WriteString(fmt.Sprintf(" Uptime: %s\n", time.Since(serverStartTime).Round(time.Second)))
310
318
311
311
-
sb.WriteString("Server Stats\n")
312
312
-
sb.WriteString("━━━━━━━━━━━━\n")
313
313
-
sb.WriteString(fmt.Sprintf(" Version: %s\n", version))
314
314
-
if origin := mgr.GetPLCOrigin(); origin != "" {
315
315
-
sb.WriteString(fmt.Sprintf(" Origin: %s\n", origin))
316
316
-
}
317
317
-
sb.WriteString(fmt.Sprintf(" Sync mode: %v\n", syncMode))
318
318
-
sb.WriteString(fmt.Sprintf(" WebSocket: %v\n", wsEnabled))
319
319
-
sb.WriteString(fmt.Sprintf(" Resolver: %v\n", resolverEnabled))
320
320
-
sb.WriteString(fmt.Sprintf(" Uptime: %s\n", time.Since(serverStartTime).Round(time.Second)))
319
319
+
sb.WriteString("\n\nAPI Endpoints\n")
320
320
+
sb.WriteString("━━━━━━━━━━━━━\n")
321
321
+
sb.WriteString(" GET / This info page\n")
322
322
+
sb.WriteString(" GET /index.json Full bundle index\n")
323
323
+
sb.WriteString(" GET /bundle/:number Bundle metadata (JSON)\n")
324
324
+
sb.WriteString(" GET /data/:number Raw bundle (zstd compressed)\n")
325
325
+
sb.WriteString(" GET /jsonl/:number Decompressed JSONL stream\n")
326
326
+
sb.WriteString(" GET /status Server status\n")
327
327
+
sb.WriteString(" GET /mempool Mempool operations (JSONL)\n")
321
328
322
322
-
sb.WriteString("\n\nAPI Endpoints\n")
323
323
-
sb.WriteString("━━━━━━━━━━━━━\n")
324
324
-
sb.WriteString(" GET / This info page\n")
325
325
-
sb.WriteString(" GET /index.json Full bundle index\n")
326
326
-
sb.WriteString(" GET /bundle/:number Bundle metadata (JSON)\n")
327
327
-
sb.WriteString(" GET /data/:number Raw bundle (zstd compressed)\n")
328
328
-
sb.WriteString(" GET /jsonl/:number Decompressed JSONL stream\n")
329
329
-
sb.WriteString(" GET /status Server status\n")
330
330
-
sb.WriteString(" GET /mempool Mempool operations (JSONL)\n")
329
329
+
if resolverEnabled {
330
330
+
sb.WriteString("\nDID Resolution\n")
331
331
+
sb.WriteString("━━━━━━━━━━━━━━\n")
332
332
+
sb.WriteString(" GET /:did DID Document (W3C format)\n")
333
333
+
sb.WriteString(" GET /:did/data PLC State (raw format)\n")
334
334
+
sb.WriteString(" GET /:did/log/audit Operation history\n")
331
335
332
332
-
if resolverEnabled {
333
333
-
sb.WriteString("\nDID Resolution\n")
334
334
-
sb.WriteString("━━━━━━━━━━━━━━\n")
335
335
-
sb.WriteString(" GET /:did DID Document (W3C format)\n")
336
336
-
sb.WriteString(" GET /:did/data PLC State (raw format)\n")
337
337
-
sb.WriteString(" GET /:did/log/audit Operation history\n")
338
338
-
339
339
-
didStats := mgr.GetDIDIndexStats()
340
340
-
if didStats["exists"].(bool) {
341
341
-
sb.WriteString(fmt.Sprintf("\n Index: %s DIDs indexed\n",
342
342
-
formatNumber(int(didStats["total_dids"].(int64)))))
343
343
-
} else {
344
344
-
sb.WriteString("\n ⚠️ Index: not built (will use slow scan)\n")
336
336
+
didStats := mgr.GetDIDIndexStats()
337
337
+
if didStats["exists"].(bool) {
338
338
+
sb.WriteString(fmt.Sprintf("\n Index: %s DIDs indexed\n",
339
339
+
formatNumber(int(didStats["total_dids"].(int64)))))
340
340
+
} else {
341
341
+
sb.WriteString("\n ⚠️ Index: not built (will use slow scan)\n")
342
342
+
}
343
343
+
sb.WriteString("\n")
345
344
}
346
346
-
sb.WriteString("\n")
347
347
-
}
348
345
349
349
-
if wsEnabled {
350
350
-
sb.WriteString("\nWebSocket Endpoints\n")
351
351
-
sb.WriteString("━━━━━━━━━━━━━━━━━━━\n")
352
352
-
sb.WriteString(" WS /ws Live stream (new operations only)\n")
353
353
-
sb.WriteString(" WS /ws?cursor=0 Stream all from beginning\n")
354
354
-
sb.WriteString(" WS /ws?cursor=N Stream from cursor N\n\n")
355
355
-
sb.WriteString("Cursor Format:\n")
356
356
-
sb.WriteString(" Global record number: (bundleNumber × 10,000) + position\n")
357
357
-
sb.WriteString(" Example: 88410345 = bundle 8841, position 345\n")
358
358
-
sb.WriteString(" Default: starts from latest (skips all historical data)\n")
346
346
+
if wsEnabled {
347
347
+
sb.WriteString("\nWebSocket Endpoints\n")
348
348
+
sb.WriteString("━━━━━━━━━━━━━━━━━━━\n")
349
349
+
sb.WriteString(" WS /ws Live stream (new operations only)\n")
350
350
+
sb.WriteString(" WS /ws?cursor=0 Stream all from beginning\n")
351
351
+
sb.WriteString(" WS /ws?cursor=N Stream from cursor N\n\n")
352
352
+
sb.WriteString("Cursor Format:\n")
353
353
+
sb.WriteString(" Global record number: (bundleNumber × 10,000) + position\n")
354
354
+
sb.WriteString(" Example: 88410345 = bundle 8841, position 345\n")
355
355
+
sb.WriteString(" Default: starts from latest (skips all historical data)\n")
359
356
360
360
-
latestCursor := mgr.GetCurrentCursor()
361
361
-
bundledOps := len(index.GetBundles()) * bundle.BUNDLE_SIZE
362
362
-
mempoolOps := latestCursor - bundledOps
357
357
+
latestCursor := mgr.GetCurrentCursor()
358
358
+
bundledOps := len(index.GetBundles()) * bundle.BUNDLE_SIZE
359
359
+
mempoolOps := latestCursor - bundledOps
363
360
364
364
-
if syncMode && mempoolOps > 0 {
365
365
-
sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundled + %d mempool)\n",
366
366
-
latestCursor, bundledOps, mempoolOps))
367
367
-
} else {
368
368
-
sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundles)\n",
369
369
-
latestCursor, len(index.GetBundles())))
361
361
+
if syncMode && mempoolOps > 0 {
362
362
+
sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundled + %d mempool)\n",
363
363
+
latestCursor, bundledOps, mempoolOps))
364
364
+
} else {
365
365
+
sb.WriteString(fmt.Sprintf(" Current latest: %d (%d bundles)\n",
366
366
+
latestCursor, len(index.GetBundles())))
367
367
+
}
370
368
}
371
371
-
}
372
369
373
373
-
sb.WriteString("\nExamples\n")
374
374
-
sb.WriteString("━━━━━━━━\n")
375
375
-
sb.WriteString(fmt.Sprintf(" curl %s/bundle/1\n", baseURL))
376
376
-
sb.WriteString(fmt.Sprintf(" curl %s/data/42 -o 000042.jsonl.zst\n", baseURL))
377
377
-
sb.WriteString(fmt.Sprintf(" curl %s/jsonl/1\n", baseURL))
370
370
+
sb.WriteString("\nExamples\n")
371
371
+
sb.WriteString("━━━━━━━━\n")
372
372
+
sb.WriteString(fmt.Sprintf(" curl %s/bundle/1\n", baseURL))
373
373
+
sb.WriteString(fmt.Sprintf(" curl %s/data/42 -o 000042.jsonl.zst\n", baseURL))
374
374
+
sb.WriteString(fmt.Sprintf(" curl %s/jsonl/1\n", baseURL))
378
375
379
379
-
if wsEnabled {
380
380
-
sb.WriteString(fmt.Sprintf(" websocat %s/ws\n", wsURL))
381
381
-
sb.WriteString(fmt.Sprintf(" websocat '%s/ws?cursor=0'\n", wsURL))
382
382
-
}
376
376
+
if wsEnabled {
377
377
+
sb.WriteString(fmt.Sprintf(" websocat %s/ws\n", wsURL))
378
378
+
sb.WriteString(fmt.Sprintf(" websocat '%s/ws?cursor=0'\n", wsURL))
379
379
+
}
383
380
384
384
-
if syncMode {
385
385
-
sb.WriteString(fmt.Sprintf(" curl %s/status\n", baseURL))
386
386
-
sb.WriteString(fmt.Sprintf(" curl %s/mempool\n", baseURL))
387
387
-
}
381
381
+
if syncMode {
382
382
+
sb.WriteString(fmt.Sprintf(" curl %s/status\n", baseURL))
383
383
+
sb.WriteString(fmt.Sprintf(" curl %s/mempool\n", baseURL))
384
384
+
}
388
385
389
389
-
sb.WriteString("\n────────────────────────────────────────────────────────────────\n")
390
390
-
sb.WriteString("https://tangled.org/@atscan.net/plcbundle\n")
386
386
+
sb.WriteString("\n────────────────────────────────────────────────────────────────\n")
387
387
+
sb.WriteString("https://tangled.org/@atscan.net/plcbundle\n")
391
388
392
392
-
return ctx.String(sb.String())
389
389
+
w.Write([]byte(sb.String()))
390
390
+
}
393
391
}
394
392
395
395
-
func handleIndexJSON(ctx web.Context, mgr *bundle.Manager) error {
396
396
-
index := mgr.GetIndex()
397
397
-
return sendJSON(ctx, 200, index)
393
393
+
func handleIndexJSONNative(mgr *bundle.Manager) http.HandlerFunc {
394
394
+
return func(w http.ResponseWriter, r *http.Request) {
395
395
+
index := mgr.GetIndex()
396
396
+
sendJSON(w, 200, index)
397
397
+
}
398
398
}
399
399
400
400
-
func handleBundle(ctx web.Context, mgr *bundle.Manager) error {
401
401
-
bundleNum, err := strconv.Atoi(ctx.Request().Param("number"))
402
402
-
if err != nil {
403
403
-
return sendJSON(ctx, 400, map[string]string{"error": "Invalid bundle number"})
404
404
-
}
400
400
+
func handleBundleNative(mgr *bundle.Manager) http.HandlerFunc {
401
401
+
return func(w http.ResponseWriter, r *http.Request) {
402
402
+
bundleNum, err := strconv.Atoi(r.PathValue("number"))
403
403
+
if err != nil {
404
404
+
sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
405
405
+
return
406
406
+
}
405
407
406
406
-
meta, err := mgr.GetIndex().GetBundle(bundleNum)
407
407
-
if err != nil {
408
408
-
return sendJSON(ctx, 404, map[string]string{"error": "Bundle not found"})
409
409
-
}
408
408
+
meta, err := mgr.GetIndex().GetBundle(bundleNum)
409
409
+
if err != nil {
410
410
+
sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
411
411
+
return
412
412
+
}
410
413
411
411
-
return sendJSON(ctx, 200, meta)
414
414
+
sendJSON(w, 200, meta)
415
415
+
}
412
416
}
413
417
414
414
-
func handleBundleData(ctx web.Context, mgr *bundle.Manager) error {
415
415
-
bundleNum, err := strconv.Atoi(ctx.Request().Param("number"))
416
416
-
if err != nil {
417
417
-
return sendJSON(ctx, 400, map[string]string{"error": "Invalid bundle number"})
418
418
-
}
418
418
+
func handleBundleDataNative(mgr *bundle.Manager) http.HandlerFunc {
419
419
+
return func(w http.ResponseWriter, r *http.Request) {
420
420
+
bundleNum, err := strconv.Atoi(r.PathValue("number"))
421
421
+
if err != nil {
422
422
+
sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
423
423
+
return
424
424
+
}
419
425
420
420
-
reader, err := mgr.StreamBundleRaw(context.Background(), bundleNum)
421
421
-
if err != nil {
422
422
-
if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
423
423
-
return sendJSON(ctx, 400, map[string]string{"error": "Bundle not found"})
426
426
+
reader, err := mgr.StreamBundleRaw(context.Background(), bundleNum)
427
427
+
if err != nil {
428
428
+
if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
429
429
+
sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
430
430
+
} else {
431
431
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
432
432
+
}
433
433
+
return
424
434
}
425
425
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
426
426
-
}
427
427
-
defer reader.Close()
435
435
+
defer reader.Close()
428
436
429
429
-
ctx.Response().SetHeader("Content-Type", "application/zstd")
430
430
-
ctx.Response().SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl.zst", bundleNum))
437
437
+
w.Header().Set("Content-Type", "application/zstd")
438
438
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl.zst", bundleNum))
431
439
432
432
-
_, err = io.Copy(ctx.Response(), reader)
433
433
-
return err
440
440
+
io.Copy(w, reader)
441
441
+
}
434
442
}
435
443
436
436
-
func handleBundleJSONL(ctx web.Context, mgr *bundle.Manager) error {
437
437
-
bundleNum, err := strconv.Atoi(ctx.Request().Param("number"))
438
438
-
if err != nil {
439
439
-
return sendJSON(ctx, 400, map[string]string{"error": "Invalid bundle number"})
440
440
-
}
444
444
+
func handleBundleJSONLNative(mgr *bundle.Manager) http.HandlerFunc {
445
445
+
return func(w http.ResponseWriter, r *http.Request) {
446
446
+
bundleNum, err := strconv.Atoi(r.PathValue("number"))
447
447
+
if err != nil {
448
448
+
sendJSON(w, 400, map[string]string{"error": "Invalid bundle number"})
449
449
+
return
450
450
+
}
441
451
442
442
-
reader, err := mgr.StreamBundleDecompressed(context.Background(), bundleNum)
443
443
-
if err != nil {
444
444
-
if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
445
445
-
return sendJSON(ctx, 404, map[string]string{"error": "Bundle not found"})
452
452
+
reader, err := mgr.StreamBundleDecompressed(context.Background(), bundleNum)
453
453
+
if err != nil {
454
454
+
if strings.Contains(err.Error(), "not in index") || strings.Contains(err.Error(), "not found") {
455
455
+
sendJSON(w, 404, map[string]string{"error": "Bundle not found"})
456
456
+
} else {
457
457
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
458
458
+
}
459
459
+
return
446
460
}
447
447
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
448
448
-
}
449
449
-
defer reader.Close()
461
461
+
defer reader.Close()
450
462
451
451
-
ctx.Response().SetHeader("Content-Type", "application/x-ndjson")
452
452
-
ctx.Response().SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl", bundleNum))
463
463
+
w.Header().Set("Content-Type", "application/x-ndjson")
464
464
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%06d.jsonl", bundleNum))
453
465
454
454
-
_, err = io.Copy(ctx.Response(), reader)
455
455
-
return err
466
466
+
io.Copy(w, reader)
467
467
+
}
456
468
}
457
469
458
458
-
func handleStatus(ctx web.Context, mgr *bundle.Manager, syncMode bool, wsEnabled bool) error {
459
459
-
index := mgr.GetIndex()
460
460
-
indexStats := index.GetStats()
470
470
+
func handleStatusNative(mgr *bundle.Manager, syncMode bool, wsEnabled bool) http.HandlerFunc {
471
471
+
return func(w http.ResponseWriter, r *http.Request) {
472
472
+
index := mgr.GetIndex()
473
473
+
indexStats := index.GetStats()
461
474
462
462
-
response := StatusResponse{
463
463
-
Server: ServerStatus{
464
464
-
Version: version,
465
465
-
UptimeSeconds: int(time.Since(serverStartTime).Seconds()),
466
466
-
SyncMode: syncMode,
467
467
-
WebSocketEnabled: wsEnabled,
468
468
-
Origin: mgr.GetPLCOrigin(),
469
469
-
},
470
470
-
Bundles: BundleStatus{
471
471
-
Count: indexStats["bundle_count"].(int),
472
472
-
TotalSize: indexStats["total_size"].(int64),
473
473
-
UncompressedSize: indexStats["total_uncompressed_size"].(int64),
474
474
-
},
475
475
-
}
475
475
+
response := StatusResponse{
476
476
+
Server: ServerStatus{
477
477
+
Version: version,
478
478
+
UptimeSeconds: int(time.Since(serverStartTime).Seconds()),
479
479
+
SyncMode: syncMode,
480
480
+
WebSocketEnabled: wsEnabled,
481
481
+
Origin: mgr.GetPLCOrigin(),
482
482
+
},
483
483
+
Bundles: BundleStatus{
484
484
+
Count: indexStats["bundle_count"].(int),
485
485
+
TotalSize: indexStats["total_size"].(int64),
486
486
+
UncompressedSize: indexStats["total_uncompressed_size"].(int64),
487
487
+
},
488
488
+
}
476
489
477
477
-
if syncMode && syncInterval > 0 {
478
478
-
response.Server.SyncIntervalSeconds = int(syncInterval.Seconds())
479
479
-
}
490
490
+
if syncMode && syncInterval > 0 {
491
491
+
response.Server.SyncIntervalSeconds = int(syncInterval.Seconds())
492
492
+
}
480
493
481
481
-
if bundleCount := response.Bundles.Count; bundleCount > 0 {
482
482
-
firstBundle := indexStats["first_bundle"].(int)
483
483
-
lastBundle := indexStats["last_bundle"].(int)
494
494
+
if bundleCount := response.Bundles.Count; bundleCount > 0 {
495
495
+
firstBundle := indexStats["first_bundle"].(int)
496
496
+
lastBundle := indexStats["last_bundle"].(int)
484
497
485
485
-
response.Bundles.FirstBundle = firstBundle
486
486
-
response.Bundles.LastBundle = lastBundle
487
487
-
response.Bundles.StartTime = indexStats["start_time"].(time.Time)
488
488
-
response.Bundles.EndTime = indexStats["end_time"].(time.Time)
498
498
+
response.Bundles.FirstBundle = firstBundle
499
499
+
response.Bundles.LastBundle = lastBundle
500
500
+
response.Bundles.StartTime = indexStats["start_time"].(time.Time)
501
501
+
response.Bundles.EndTime = indexStats["end_time"].(time.Time)
489
502
490
490
-
if firstMeta, err := index.GetBundle(firstBundle); err == nil {
491
491
-
response.Bundles.RootHash = firstMeta.Hash
492
492
-
}
503
503
+
if firstMeta, err := index.GetBundle(firstBundle); err == nil {
504
504
+
response.Bundles.RootHash = firstMeta.Hash
505
505
+
}
493
506
494
494
-
if lastMeta, err := index.GetBundle(lastBundle); err == nil {
495
495
-
response.Bundles.HeadHash = lastMeta.Hash
496
496
-
response.Bundles.HeadAgeSeconds = int(time.Since(lastMeta.EndTime).Seconds())
497
497
-
}
507
507
+
if lastMeta, err := index.GetBundle(lastBundle); err == nil {
508
508
+
response.Bundles.HeadHash = lastMeta.Hash
509
509
+
response.Bundles.HeadAgeSeconds = int(time.Since(lastMeta.EndTime).Seconds())
510
510
+
}
498
511
499
499
-
if gaps, ok := indexStats["gaps"].(int); ok {
500
500
-
response.Bundles.Gaps = gaps
501
501
-
response.Bundles.HasGaps = gaps > 0
502
502
-
if gaps > 0 {
503
503
-
response.Bundles.GapNumbers = index.FindGaps()
512
512
+
if gaps, ok := indexStats["gaps"].(int); ok {
513
513
+
response.Bundles.Gaps = gaps
514
514
+
response.Bundles.HasGaps = gaps > 0
515
515
+
if gaps > 0 {
516
516
+
response.Bundles.GapNumbers = index.FindGaps()
517
517
+
}
504
518
}
505
505
-
}
506
519
507
507
-
totalOps := bundleCount * bundle.BUNDLE_SIZE
508
508
-
response.Bundles.TotalOperations = totalOps
520
520
+
totalOps := bundleCount * bundle.BUNDLE_SIZE
521
521
+
response.Bundles.TotalOperations = totalOps
509
522
510
510
-
duration := response.Bundles.EndTime.Sub(response.Bundles.StartTime)
511
511
-
if duration.Hours() > 0 {
512
512
-
response.Bundles.AvgOpsPerHour = int(float64(totalOps) / duration.Hours())
523
523
+
duration := response.Bundles.EndTime.Sub(response.Bundles.StartTime)
524
524
+
if duration.Hours() > 0 {
525
525
+
response.Bundles.AvgOpsPerHour = int(float64(totalOps) / duration.Hours())
526
526
+
}
513
527
}
514
514
-
}
515
528
516
516
-
if syncMode {
517
517
-
mempoolStats := mgr.GetMempoolStats()
529
529
+
if syncMode {
530
530
+
mempoolStats := mgr.GetMempoolStats()
518
531
519
519
-
if count, ok := mempoolStats["count"].(int); ok {
520
520
-
mempool := &MempoolStatus{
521
521
-
Count: count,
522
522
-
TargetBundle: mempoolStats["target_bundle"].(int),
523
523
-
CanCreateBundle: mempoolStats["can_create_bundle"].(bool),
524
524
-
MinTimestamp: mempoolStats["min_timestamp"].(time.Time),
525
525
-
Validated: mempoolStats["validated"].(bool),
526
526
-
ProgressPercent: float64(count) / float64(bundle.BUNDLE_SIZE) * 100,
527
527
-
BundleSize: bundle.BUNDLE_SIZE,
528
528
-
OperationsNeeded: bundle.BUNDLE_SIZE - count,
529
529
-
}
532
532
+
if count, ok := mempoolStats["count"].(int); ok {
533
533
+
mempool := &MempoolStatus{
534
534
+
Count: count,
535
535
+
TargetBundle: mempoolStats["target_bundle"].(int),
536
536
+
CanCreateBundle: mempoolStats["can_create_bundle"].(bool),
537
537
+
MinTimestamp: mempoolStats["min_timestamp"].(time.Time),
538
538
+
Validated: mempoolStats["validated"].(bool),
539
539
+
ProgressPercent: float64(count) / float64(bundle.BUNDLE_SIZE) * 100,
540
540
+
BundleSize: bundle.BUNDLE_SIZE,
541
541
+
OperationsNeeded: bundle.BUNDLE_SIZE - count,
542
542
+
}
530
543
531
531
-
if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
532
532
-
mempool.FirstTime = firstTime
533
533
-
mempool.TimespanSeconds = int(time.Since(firstTime).Seconds())
534
534
-
}
535
535
-
if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
536
536
-
mempool.LastTime = lastTime
537
537
-
mempool.LastOpAgeSeconds = int(time.Since(lastTime).Seconds())
538
538
-
}
544
544
+
if firstTime, ok := mempoolStats["first_time"].(time.Time); ok {
545
545
+
mempool.FirstTime = firstTime
546
546
+
mempool.TimespanSeconds = int(time.Since(firstTime).Seconds())
547
547
+
}
548
548
+
if lastTime, ok := mempoolStats["last_time"].(time.Time); ok {
549
549
+
mempool.LastTime = lastTime
550
550
+
mempool.LastOpAgeSeconds = int(time.Since(lastTime).Seconds())
551
551
+
}
539
552
540
540
-
if count > 100 && count < bundle.BUNDLE_SIZE {
541
541
-
if !mempool.FirstTime.IsZero() && !mempool.LastTime.IsZero() {
542
542
-
timespan := mempool.LastTime.Sub(mempool.FirstTime)
543
543
-
if timespan.Seconds() > 0 {
544
544
-
opsPerSec := float64(count) / timespan.Seconds()
545
545
-
remaining := bundle.BUNDLE_SIZE - count
546
546
-
mempool.EtaNextBundleSeconds = int(float64(remaining) / opsPerSec)
553
553
+
if count > 100 && count < bundle.BUNDLE_SIZE {
554
554
+
if !mempool.FirstTime.IsZero() && !mempool.LastTime.IsZero() {
555
555
+
timespan := mempool.LastTime.Sub(mempool.FirstTime)
556
556
+
if timespan.Seconds() > 0 {
557
557
+
opsPerSec := float64(count) / timespan.Seconds()
558
558
+
remaining := bundle.BUNDLE_SIZE - count
559
559
+
mempool.EtaNextBundleSeconds = int(float64(remaining) / opsPerSec)
560
560
+
}
547
561
}
548
562
}
563
563
+
564
564
+
response.Mempool = mempool
549
565
}
566
566
+
}
550
567
551
551
-
response.Mempool = mempool
552
552
-
}
568
568
+
sendJSON(w, 200, response)
553
569
}
554
554
-
555
555
-
return sendJSON(ctx, 200, response)
556
570
}
557
571
558
558
-
func handleMempool(ctx web.Context, mgr *bundle.Manager) error {
559
559
-
ops, err := mgr.GetMempoolOperations()
560
560
-
if err != nil {
561
561
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
562
562
-
}
572
572
+
func handleMempoolNative(mgr *bundle.Manager) http.HandlerFunc {
573
573
+
return func(w http.ResponseWriter, r *http.Request) {
574
574
+
ops, err := mgr.GetMempoolOperations()
575
575
+
if err != nil {
576
576
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
577
577
+
return
578
578
+
}
563
579
564
564
-
ctx.Response().SetHeader("Content-Type", "application/x-ndjson")
580
580
+
w.Header().Set("Content-Type", "application/x-ndjson")
565
581
566
566
-
if len(ops) == 0 {
567
567
-
return nil
568
568
-
}
582
582
+
if len(ops) == 0 {
583
583
+
return
584
584
+
}
569
585
570
570
-
for _, op := range ops {
571
571
-
if len(op.RawJSON) > 0 {
572
572
-
ctx.Response().Write(op.RawJSON)
573
573
-
} else {
574
574
-
data, _ := json.Marshal(op)
575
575
-
ctx.Response().Write(data)
586
586
+
for _, op := range ops {
587
587
+
if len(op.RawJSON) > 0 {
588
588
+
w.Write(op.RawJSON)
589
589
+
} else {
590
590
+
data, _ := json.Marshal(op)
591
591
+
w.Write(data)
592
592
+
}
593
593
+
w.Write([]byte("\n"))
576
594
}
577
577
-
ctx.Response().Write([]byte("\n"))
578
595
}
579
579
-
580
580
-
return nil
581
596
}
582
597
583
583
-
func handleDebugMemory(ctx web.Context, mgr *bundle.Manager) error {
584
584
-
var m runtime.MemStats
585
585
-
runtime.ReadMemStats(&m)
598
598
+
func handleDebugMemoryNative(mgr *bundle.Manager) http.HandlerFunc {
599
599
+
return func(w http.ResponseWriter, r *http.Request) {
600
600
+
var m runtime.MemStats
601
601
+
runtime.ReadMemStats(&m)
586
602
587
587
-
didStats := mgr.GetDIDIndexStats()
603
603
+
didStats := mgr.GetDIDIndexStats()
588
604
589
589
-
beforeAlloc := m.Alloc / 1024 / 1024
605
605
+
beforeAlloc := m.Alloc / 1024 / 1024
590
606
591
591
-
runtime.GC()
592
592
-
runtime.ReadMemStats(&m)
593
593
-
afterAlloc := m.Alloc / 1024 / 1024
607
607
+
runtime.GC()
608
608
+
runtime.ReadMemStats(&m)
609
609
+
afterAlloc := m.Alloc / 1024 / 1024
594
610
595
595
-
return ctx.String(fmt.Sprintf(`Memory Stats:
611
611
+
response := fmt.Sprintf(`Memory Stats:
596
612
Alloc: %d MB
597
613
TotalAlloc: %d MB
598
614
Sys: %d MB
···
604
620
After GC:
605
621
Alloc: %d MB
606
622
`,
607
607
-
beforeAlloc,
608
608
-
m.TotalAlloc/1024/1024,
609
609
-
m.Sys/1024/1024,
610
610
-
m.NumGC,
611
611
-
didStats["cached_shards"],
612
612
-
didStats["cache_limit"],
613
613
-
afterAlloc))
614
614
-
}
615
615
-
616
616
-
func handleDIDDocumentLatest(ctx web.Context, mgr *bundle.Manager, did string) error {
617
617
-
op, err := mgr.GetLatestDIDOperation(context.Background(), did)
618
618
-
if err != nil {
619
619
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
620
620
-
}
623
623
+
beforeAlloc,
624
624
+
m.TotalAlloc/1024/1024,
625
625
+
m.Sys/1024/1024,
626
626
+
m.NumGC,
627
627
+
didStats["cached_shards"],
628
628
+
didStats["cache_limit"],
629
629
+
afterAlloc)
621
630
622
622
-
doc, err := plc.ResolveDIDDocument(did, []plc.PLCOperation{*op})
623
623
-
if err != nil {
624
624
-
if strings.Contains(err.Error(), "deactivated") {
625
625
-
return sendJSON(ctx, 410, map[string]string{"error": "DID has been deactivated"})
626
626
-
}
627
627
-
return sendJSON(ctx, 500, map[string]string{"error": fmt.Sprintf("Resolution failed: %v", err)})
631
631
+
w.Header().Set("Content-Type", "text/plain")
632
632
+
w.Write([]byte(response))
628
633
}
629
629
-
ctx.Response().SetHeader("Content-Type", "application/did+ld+json")
630
630
-
return sendJSON(ctx, 200, doc)
631
634
}
632
635
633
633
-
func handleDIDData(ctx web.Context, mgr *bundle.Manager, did string) error {
634
634
-
if err := plc.ValidateDIDFormat(did); err != nil {
635
635
-
return sendJSON(ctx, 400, map[string]string{"error": "Invalid DID format"})
636
636
-
}
636
636
+
func handleWebSocketNative(mgr *bundle.Manager) http.HandlerFunc {
637
637
+
return func(w http.ResponseWriter, r *http.Request) {
638
638
+
cursorStr := r.URL.Query().Get("cursor")
639
639
+
var cursor int
637
640
638
638
-
operations, err := mgr.GetDIDOperations(context.Background(), did, false)
639
639
-
if err != nil {
640
640
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
641
641
-
}
641
641
+
if cursorStr == "" {
642
642
+
cursor = mgr.GetCurrentCursor()
643
643
+
} else {
644
644
+
var err error
645
645
+
cursor, err = strconv.Atoi(cursorStr)
646
646
+
if err != nil || cursor < 0 {
647
647
+
http.Error(w, "Invalid cursor: must be non-negative integer", 400)
648
648
+
return
649
649
+
}
650
650
+
}
642
651
643
643
-
if len(operations) == 0 {
644
644
-
return sendJSON(ctx, 404, map[string]string{"error": "DID not found"})
645
645
-
}
646
646
-
647
647
-
state, err := plc.BuildDIDState(did, operations)
648
648
-
if err != nil {
649
649
-
if strings.Contains(err.Error(), "deactivated") {
650
650
-
return sendJSON(ctx, 410, map[string]string{"error": "DID has been deactivated"})
652
652
+
conn, err := upgrader.Upgrade(w, r, nil)
653
653
+
if err != nil {
654
654
+
fmt.Fprintf(os.Stderr, "WebSocket upgrade failed: %v\n", err)
655
655
+
return
651
656
}
652
652
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
653
653
-
}
657
657
+
defer conn.Close()
658
658
+
659
659
+
conn.SetPongHandler(func(string) error {
660
660
+
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
661
661
+
return nil
662
662
+
})
654
663
655
655
-
return sendJSON(ctx, 200, state)
656
656
-
}
664
664
+
done := make(chan struct{})
657
665
658
658
-
func handleDIDAuditLog(ctx web.Context, mgr *bundle.Manager, did string) error {
659
659
-
if err := plc.ValidateDIDFormat(did); err != nil {
660
660
-
return sendJSON(ctx, 400, map[string]string{"error": "Invalid DID format"})
661
661
-
}
666
666
+
go func() {
667
667
+
defer close(done)
668
668
+
for {
669
669
+
_, _, err := conn.ReadMessage()
670
670
+
if err != nil {
671
671
+
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
672
672
+
fmt.Fprintf(os.Stderr, "WebSocket: client closed connection\n")
673
673
+
}
674
674
+
return
675
675
+
}
676
676
+
}
677
677
+
}()
662
678
663
663
-
operations, err := mgr.GetDIDOperations(context.Background(), did, false)
664
664
-
if err != nil {
665
665
-
return sendJSON(ctx, 500, map[string]string{"error": err.Error()})
666
666
-
}
679
679
+
bgCtx := context.Background()
667
680
668
668
-
if len(operations) == 0 {
669
669
-
return sendJSON(ctx, 404, map[string]string{"error": "DID not found"})
681
681
+
if err := streamLive(bgCtx, conn, mgr, cursor, done); err != nil {
682
682
+
fmt.Fprintf(os.Stderr, "WebSocket stream error: %v\n", err)
683
683
+
}
670
684
}
671
671
-
672
672
-
auditLog := plc.FormatAuditLog(operations)
673
673
-
return sendJSON(ctx, 200, auditLog)
674
685
}
675
686
676
676
-
// WebSocket handler wrapper for web framework
677
677
-
func handleWebSocketRaw(ctx web.Context, mgr *bundle.Manager) {
678
678
-
// The web framework doesn't expose ResponseWriter directly
679
679
-
// We need to use reflection or type assertion to get it
680
680
-
// For now, we'll implement a workaround by getting the underlying HTTP objects
687
687
+
func handleDIDDocumentLatestNative(mgr *bundle.Manager, did string) http.HandlerFunc {
688
688
+
return func(w http.ResponseWriter, r *http.Request) {
689
689
+
op, err := mgr.GetLatestDIDOperation(context.Background(), did)
690
690
+
if err != nil {
691
691
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
692
692
+
return
693
693
+
}
681
694
682
682
-
cursorStr := ctx.Request().Query().Param("cursor")
683
683
-
var cursor int
684
684
-
685
685
-
if cursorStr == "" {
686
686
-
cursor = mgr.GetCurrentCursor()
687
687
-
} else {
688
688
-
var err error
689
689
-
cursor, err = strconv.Atoi(cursorStr)
690
690
-
if err != nil || cursor < 0 {
691
691
-
ctx.Status(400).String("Invalid cursor: must be non-negative integer")
695
695
+
doc, err := plc.ResolveDIDDocument(did, []plc.PLCOperation{*op})
696
696
+
if err != nil {
697
697
+
if strings.Contains(err.Error(), "deactivated") {
698
698
+
sendJSON(w, 410, map[string]string{"error": "DID has been deactivated"})
699
699
+
} else {
700
700
+
sendJSON(w, 500, map[string]string{"error": fmt.Sprintf("Resolution failed: %v", err)})
701
701
+
}
692
702
return
693
703
}
694
694
-
}
695
704
696
696
-
// Access underlying ResponseWriter through interface assertion
697
697
-
type ResponseWriterGetter interface {
698
698
-
ResponseWriter() http.ResponseWriter
705
705
+
w.Header().Set("Content-Type", "application/did+ld+json")
706
706
+
sendJSON(w, 200, doc)
699
707
}
708
708
+
}
700
709
701
701
-
type RequestGetter interface {
702
702
-
HTTPRequest() *http.Request
703
703
-
}
704
704
-
705
705
-
var w http.ResponseWriter
706
706
-
var r *http.Request
710
710
+
func handleDIDDataNative(mgr *bundle.Manager, did string) http.HandlerFunc {
711
711
+
return func(w http.ResponseWriter, r *http.Request) {
712
712
+
if err := plc.ValidateDIDFormat(did); err != nil {
713
713
+
sendJSON(w, 400, map[string]string{"error": "Invalid DID format"})
714
714
+
return
715
715
+
}
707
716
708
708
-
// Try to get ResponseWriter (framework-specific)
709
709
-
if rwg, ok := ctx.(ResponseWriterGetter); ok {
710
710
-
w = rwg.ResponseWriter()
711
711
-
}
717
717
+
operations, err := mgr.GetDIDOperations(context.Background(), did, false)
718
718
+
if err != nil {
719
719
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
720
720
+
return
721
721
+
}
712
722
713
713
-
if rg, ok := ctx.(RequestGetter); ok {
714
714
-
r = rg.HTTPRequest()
715
715
-
}
723
723
+
if len(operations) == 0 {
724
724
+
sendJSON(w, 404, map[string]string{"error": "DID not found"})
725
725
+
return
726
726
+
}
716
727
717
717
-
// If we can't get them, we need to upgrade manually
718
718
-
// This is a limitation - WebSocket needs direct access
719
719
-
if w == nil || r == nil {
720
720
-
ctx.Status(500).String("WebSocket not supported")
721
721
-
return
722
722
-
}
728
728
+
state, err := plc.BuildDIDState(did, operations)
729
729
+
if err != nil {
730
730
+
if strings.Contains(err.Error(), "deactivated") {
731
731
+
sendJSON(w, 410, map[string]string{"error": "DID has been deactivated"})
732
732
+
} else {
733
733
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
734
734
+
}
735
735
+
return
736
736
+
}
723
737
724
724
-
conn, err := upgrader.Upgrade(w, r, nil)
725
725
-
if err != nil {
726
726
-
fmt.Fprintf(os.Stderr, "WebSocket upgrade failed: %v\n", err)
727
727
-
return
738
738
+
sendJSON(w, 200, state)
728
739
}
729
729
-
defer conn.Close()
740
740
+
}
730
741
731
731
-
conn.SetPongHandler(func(string) error {
732
732
-
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
733
733
-
return nil
734
734
-
})
742
742
+
func handleDIDAuditLogNative(mgr *bundle.Manager, did string) http.HandlerFunc {
743
743
+
return func(w http.ResponseWriter, r *http.Request) {
744
744
+
if err := plc.ValidateDIDFormat(did); err != nil {
745
745
+
sendJSON(w, 400, map[string]string{"error": "Invalid DID format"})
746
746
+
return
747
747
+
}
735
748
736
736
-
done := make(chan struct{})
737
737
-
738
738
-
go func() {
739
739
-
defer close(done)
740
740
-
for {
741
741
-
_, _, err := conn.ReadMessage()
742
742
-
if err != nil {
743
743
-
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
744
744
-
fmt.Fprintf(os.Stderr, "WebSocket: client closed connection\n")
745
745
-
}
746
746
-
return
747
747
-
}
749
749
+
operations, err := mgr.GetDIDOperations(context.Background(), did, false)
750
750
+
if err != nil {
751
751
+
sendJSON(w, 500, map[string]string{"error": err.Error()})
752
752
+
return
748
753
}
749
749
-
}()
750
754
751
751
-
bgCtx := context.Background()
755
755
+
if len(operations) == 0 {
756
756
+
sendJSON(w, 404, map[string]string{"error": "DID not found"})
757
757
+
return
758
758
+
}
752
759
753
753
-
if err := streamLive(bgCtx, conn, mgr, cursor, done); err != nil {
754
754
-
fmt.Fprintf(os.Stderr, "WebSocket stream error: %v\n", err)
760
760
+
auditLog := plc.FormatAuditLog(operations)
761
761
+
sendJSON(w, 200, auditLog)
755
762
}
756
763
}
757
764
758
758
-
// streamLive and other WebSocket functions remain unchanged
765
765
+
// WebSocket streaming functions (unchanged from your original)
766
766
+
759
767
func streamLive(ctx context.Context, conn *websocket.Conn, mgr *bundle.Manager, startCursor int, done chan struct{}) error {
760
768
index := mgr.GetIndex()
761
769
bundles := index.GetBundles()
···
966
974
return "ws"
967
975
}
968
976
969
969
-
func getBaseURLFromContext(ctx web.Context) string {
970
970
-
// Get host from request
971
971
-
host := ctx.Request().Header("Host")
972
972
-
// Assume http since we're behind reverse proxy
973
973
-
return fmt.Sprintf("http://%s", host)
977
977
+
func getBaseURL(r *http.Request) string {
978
978
+
scheme := getScheme(r)
979
979
+
host := r.Host
980
980
+
return fmt.Sprintf("%s://%s", scheme, host)
974
981
}
975
982
976
976
-
func getWSURLFromContext(ctx web.Context) string {
977
977
-
host := ctx.Request().Header("Host")
978
978
-
return fmt.Sprintf("ws://%s", host)
983
983
+
func getWSURL(r *http.Request) string {
984
984
+
scheme := getWSScheme(r)
985
985
+
host := r.Host
986
986
+
return fmt.Sprintf("%s://%s", scheme, host)
979
987
}
980
988
981
981
-
// Response types
989
989
+
// Response types (unchanged)
982
990
983
991
type StatusResponse struct {
984
992
Bundles BundleStatus `json:"bundles"`
···
1031
1039
EtaNextBundleSeconds int `json:"eta_next_bundle_seconds,omitempty"`
1032
1040
}
1033
1041
1034
1034
-
// Background sync
1042
1042
+
// Background sync (unchanged)
1035
1043
1036
1044
func runSync(ctx context.Context, mgr *bundle.Manager, interval time.Duration, verbose bool, resolverEnabled bool) {
1037
1045
syncBundles(ctx, mgr, verbose, resolverEnabled)
-3
go.mod
···
3
3
go 1.25
4
4
5
5
require (
6
6
-
git.urbach.dev/go/web v0.0.0-20250827103423-e50f220853ff
7
6
github.com/DataDog/zstd v1.5.7
8
7
github.com/goccy/go-json v0.10.5
9
8
github.com/gorilla/websocket v1.5.3
10
9
)
11
11
-
12
12
-
require git.urbach.dev/go/router v0.0.0-20250721083733-8d04266bc544 // indirect
-6
go.sum
···
1
1
-
git.urbach.dev/go/assert v0.0.0-20250606150337-559d3d3afcda h1:VN6ZQwtwLOm2xTms+v8IIeeNjvs55qyEBNArv3dPq9g=
2
2
-
git.urbach.dev/go/assert v0.0.0-20250606150337-559d3d3afcda/go.mod h1:PNI/NSBOqvoeU58/7eBsIR09Yoq2S/qtSRiTrctkiq0=
3
3
-
git.urbach.dev/go/router v0.0.0-20250721083733-8d04266bc544 h1:ChhCFmPVTDzj5rdzYsbQFJrDwzUldnZQMt2Bgc0gcwM=
4
4
-
git.urbach.dev/go/router v0.0.0-20250721083733-8d04266bc544/go.mod h1:seUQ5raGaj6fDeZP6d7JdgnWQys8oTrtFdvBhAp1IZA=
5
5
-
git.urbach.dev/go/web v0.0.0-20250827103423-e50f220853ff h1:bB+YedSwmEjgAFe9W6WMwKFi1T/b78z7072HyVPAcCw=
6
6
-
git.urbach.dev/go/web v0.0.0-20250827103423-e50f220853ff/go.mod h1:ON84DswRfsjIgAloDGjbt9PWWhDcMVEek3LsCHuYnOg=
7
1
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
8
2
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
9
3
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=