A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory

update ws

+89 -26
+28 -1
cmd/plcbundle/main.go
··· 7 7 "net/http" 8 8 "os" 9 9 "path/filepath" 10 + "runtime/debug" 10 11 "sort" 11 12 "strings" 12 13 "time" ··· 15 16 "github.com/atscan/plcbundle/plc" 16 17 ) 17 18 18 - // Version information (injected at build time via ldflags) 19 + // Version information (injected at build time via ldflags or read from build info) 19 20 var ( 20 21 version = "dev" 21 22 gitCommit = "unknown" 22 23 buildDate = "unknown" 23 24 ) 25 + 26 + func init() { 27 + // Try to get version from build info (works with go install) 28 + if info, ok := debug.ReadBuildInfo(); ok { 29 + if info.Main.Version != "" && info.Main.Version != "(devel)" { 30 + version = info.Main.Version 31 + } 32 + 33 + // Extract git commit and build time from build settings 34 + for _, setting := range info.Settings { 35 + switch setting.Key { 36 + case "vcs.revision": 37 + if setting.Value != "" { 38 + gitCommit = setting.Value 39 + if len(gitCommit) > 7 { 40 + gitCommit = gitCommit[:7] // Short hash 41 + } 42 + } 43 + case "vcs.time": 44 + if setting.Value != "" { 45 + buildDate = setting.Value 46 + } 47 + } 48 + } 49 + } 50 + } 24 51 25 52 func main() { 26 53 if len(os.Args) < 2 {
+61 -25
cmd/plcbundle/server.go
··· 12 12 "time" 13 13 14 14 "github.com/atscan/plcbundle/bundle" 15 + "github.com/atscan/plcbundle/plc" 15 16 "github.com/gorilla/websocket" 16 17 ) 17 18 ··· 143 144 index := mgr.GetIndex() 144 145 bundles := index.GetBundles() 145 146 146 - if len(bundles) == 0 { 147 - return 148 - } 149 - 150 147 currentRecord := 0 151 148 152 149 // Stream all operations from all bundles ··· 166 163 continue 167 164 } 168 165 169 - // Send raw JSON if available, otherwise marshal the operation 170 - var data []byte 171 - if len(op.RawJSON) > 0 { 172 - data = op.RawJSON 173 - } else { 174 - data, err = json.Marshal(op) 175 - if err != nil { 176 - fmt.Fprintf(os.Stderr, "Failed to marshal operation: %v\n", err) 177 - currentRecord++ 178 - continue 179 - } 180 - } 181 - 182 - // Send raw JSON as text message 183 - if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { 184 - // Connection closed or error 185 - fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err) 166 + // Send raw JSON 167 + if err := sendOperation(conn, op); err != nil { 186 168 return 187 169 } 188 170 189 171 currentRecord++ 190 172 191 - // Optional: Send ping periodically to keep connection alive 173 + // Send ping periodically to keep connection alive 192 174 if currentRecord%1000 == 0 { 193 175 if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 194 176 return ··· 196 178 } 197 179 } 198 180 } 181 + 182 + // Stream mempool operations (seamlessly after bundles) 183 + mempoolOps, err := mgr.GetMempoolOperations() 184 + if err != nil { 185 + fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err) 186 + return 187 + } 188 + 189 + for _, op := range mempoolOps { 190 + // Skip records before cursor 191 + if currentRecord < cursor { 192 + currentRecord++ 193 + continue 194 + } 195 + 196 + // Send raw JSON 197 + if err := sendOperation(conn, op); err != nil { 198 + return 199 + } 200 + 201 + currentRecord++ 202 + 203 + // Send ping periodically 204 + if currentRecord%1000 == 0 { 205 + if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 206 + return 207 + } 208 + } 209 + } 210 + } 211 + 212 + // sendOperation sends a single operation over WebSocket as raw JSON 213 + func sendOperation(conn *websocket.Conn, op plc.PLCOperation) error { 214 + var data []byte 215 + var err error 216 + 217 + // Use raw JSON if available, otherwise marshal 218 + if len(op.RawJSON) > 0 { 219 + data = op.RawJSON 220 + } else { 221 + data, err = json.Marshal(op) 222 + if err != nil { 223 + fmt.Fprintf(os.Stderr, "Failed to marshal operation: %v\n", err) 224 + return nil // Skip this operation but continue 225 + } 226 + } 227 + 228 + // Send as text message 229 + if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { 230 + fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err) 231 + return err 232 + } 233 + 234 + return nil 199 235 } 200 236 201 237 func handleRoot(w http.ResponseWriter, r *http.Request, mgr *bundle.Manager, syncMode bool, wsEnabled bool) { ··· 315 351 fmt.Fprintf(w, "━━━━━━━━\n") 316 352 fmt.Fprintf(w, " # Get bundle metadata\n") 317 353 fmt.Fprintf(w, " curl %s/bundle/1\n\n", baseURL) 318 - fmt.Fprintf(w, " # Download compressed bundle\n") 319 - fmt.Fprintf(w, " curl %s/data/1 -o 000001.jsonl.zst\n\n", baseURL) 320 - fmt.Fprintf(w, " # Stream decompressed operations\n") 354 + fmt.Fprintf(w, " # Download compressed bundle 42\n") 355 + fmt.Fprintf(w, " curl %s/data/42 -o 000042.jsonl.zst\n\n", baseURL) 356 + fmt.Fprintf(w, " # Stream decompressed operations from bundle 42\n") 321 357 fmt.Fprintf(w, " curl %s/jsonl/1\n\n", baseURL) 322 358 323 359 if wsEnabled {