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

better info, ws update

+512 -95
+1 -1
bundle/bundle_test.go
··· 311 tmpDir := t.TempDir() 312 logger := &testLogger{t: t} 313 314 - ops, err := bundle.NewOperations(bundle.CompressionBetter, logger) 315 if err != nil { 316 t.Fatalf("NewOperations failed: %v", err) 317 }
··· 311 tmpDir := t.TempDir() 312 logger := &testLogger{t: t} 313 314 + ops, err := bundle.NewOperations(logger) 315 if err != nil { 316 t.Fatalf("NewOperations failed: %v", err) 317 }
+1 -2
bundle/manager.go
··· 53 } 54 55 // Initialize operations handler 56 - ops, err := NewOperations(config.CompressionLevel, config.Logger) 57 if err != nil { 58 return nil, fmt.Errorf("failed to initialize operations: %w", err) 59 } ··· 640 stats := m.index.GetStats() 641 stats["bundle_dir"] = m.config.BundleDir 642 stats["index_path"] = m.indexPath 643 - stats["compression_level"] = m.config.CompressionLevel 644 stats["verify_on_load"] = m.config.VerifyOnLoad 645 return stats 646 }
··· 53 } 54 55 // Initialize operations handler 56 + ops, err := NewOperations(config.Logger) 57 if err != nil { 58 return nil, fmt.Errorf("failed to initialize operations: %w", err) 59 } ··· 640 stats := m.index.GetStats() 641 stats["bundle_dir"] = m.config.BundleDir 642 stats["index_path"] = m.indexPath 643 stats["verify_on_load"] = m.config.VerifyOnLoad 644 return stats 645 }
+4 -17
bundle/operations.go
··· 22 logger Logger 23 } 24 25 - // NewOperations creates a new Operations handler 26 - func NewOperations(compressionLevel CompressionLevel, logger Logger) (*Operations, error) { 27 - var zstdLevel zstd.EncoderLevel 28 - switch compressionLevel { 29 - case CompressionFastest: 30 - zstdLevel = zstd.SpeedFastest 31 - case CompressionDefault: 32 - zstdLevel = zstd.SpeedDefault 33 - case CompressionBetter: 34 - zstdLevel = zstd.SpeedBetterCompression 35 - case CompressionBest: 36 - zstdLevel = zstd.SpeedBestCompression 37 - default: 38 - zstdLevel = zstd.SpeedBetterCompression 39 - } 40 - 41 - encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstdLevel)) 42 if err != nil { 43 return nil, fmt.Errorf("failed to create zstd encoder: %w", err) 44 }
··· 22 logger Logger 23 } 24 25 + // NewOperations creates a new Operations handler with default compression 26 + func NewOperations(logger Logger) (*Operations, error) { 27 + // Always use default compression (level 3 - good balance) 28 + encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedDefault)) 29 if err != nil { 30 return nil, fmt.Errorf("failed to create zstd encoder: %w", err) 31 }
+6 -21
bundle/types.go
··· 11 const ( 12 // BUNDLE_SIZE is the standard number of operations per bundle 13 BUNDLE_SIZE = 10000 14 - 15 - // DefaultCompression is the default compression level 16 - DefaultCompression = CompressionBetter 17 - ) 18 - 19 - // CompressionLevel represents zstd compression levels 20 - type CompressionLevel int 21 - 22 - const ( 23 - CompressionFastest CompressionLevel = 1 24 - CompressionDefault CompressionLevel = 3 25 - CompressionBetter CompressionLevel = 5 26 - CompressionBest CompressionLevel = 9 27 ) 28 29 // Bundle represents a PLC bundle ··· 171 172 // Config holds configuration for bundle operations 173 type Config struct { 174 - BundleDir string 175 - CompressionLevel CompressionLevel 176 - VerifyOnLoad bool 177 - Logger Logger 178 } 179 180 // DefaultConfig returns default configuration 181 func DefaultConfig(bundleDir string) *Config { 182 return &Config{ 183 - BundleDir: bundleDir, 184 - CompressionLevel: CompressionBetter, 185 - VerifyOnLoad: true, 186 - Logger: nil, // Will use defaultLogger in manager 187 } 188 }
··· 11 const ( 12 // BUNDLE_SIZE is the standard number of operations per bundle 13 BUNDLE_SIZE = 10000 14 ) 15 16 // Bundle represents a PLC bundle ··· 158 159 // Config holds configuration for bundle operations 160 type Config struct { 161 + BundleDir string 162 + VerifyOnLoad bool 163 + Logger Logger 164 } 165 166 // DefaultConfig returns default configuration 167 func DefaultConfig(bundleDir string) *Config { 168 return &Config{ 169 + BundleDir: bundleDir, 170 + VerifyOnLoad: true, 171 + Logger: nil, // Will use defaultLogger in manager 172 } 173 }
+334
cmd/plcbundle/info.go
···
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "sort" 9 + "strings" 10 + "time" 11 + 12 + "github.com/atscan/plcbundle/bundle" 13 + ) 14 + 15 + func showGeneralInfo(mgr *bundle.Manager, dir string, verbose bool, showBundles bool, verify bool, showTimeline bool) { 16 + index := mgr.GetIndex() 17 + info := mgr.GetInfo() 18 + stats := index.GetStats() 19 + bundleCount := stats["bundle_count"].(int) 20 + 21 + fmt.Printf("\n") 22 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 23 + fmt.Printf(" PLC Bundle Repository Overview\n") 24 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 25 + fmt.Printf("\n") 26 + 27 + // Location 28 + fmt.Printf("📁 Location\n") 29 + fmt.Printf(" Directory: %s\n", dir) 30 + fmt.Printf(" Index: %s\n", filepath.Base(info["index_path"].(string))) 31 + fmt.Printf("\n") 32 + 33 + if bundleCount == 0 { 34 + fmt.Printf("⚠️ No bundles found\n") 35 + fmt.Printf("\n") 36 + fmt.Printf("Get started:\n") 37 + fmt.Printf(" plcbundle fetch # Fetch bundles from PLC\n") 38 + fmt.Printf(" plcbundle scan # Scan existing bundle files\n") 39 + fmt.Printf("\n") 40 + return 41 + } 42 + 43 + firstBundle := stats["first_bundle"].(int) 44 + lastBundle := stats["last_bundle"].(int) 45 + totalSize := stats["total_size"].(int64) 46 + startTime := stats["start_time"].(time.Time) 47 + endTime := stats["end_time"].(time.Time) 48 + updatedAt := stats["updated_at"].(time.Time) 49 + 50 + // Summary 51 + fmt.Printf("📊 Summary\n") 52 + fmt.Printf(" Bundles: %s\n", formatNumber(bundleCount)) 53 + fmt.Printf(" Range: %06d → %06d\n", firstBundle, lastBundle) 54 + fmt.Printf(" Total Size: %s\n", formatBytes(totalSize)) 55 + fmt.Printf(" Avg/Bundle: %s\n", formatBytes(totalSize/int64(bundleCount))) 56 + fmt.Printf("\n") 57 + 58 + // Timeline 59 + duration := endTime.Sub(startTime) 60 + fmt.Printf("📅 Timeline\n") 61 + fmt.Printf(" First Op: %s\n", startTime.Format("2006-01-02 15:04:05 MST")) 62 + fmt.Printf(" Last Op: %s\n", endTime.Format("2006-01-02 15:04:05 MST")) 63 + fmt.Printf(" Timespan: %s\n", formatDuration(duration)) 64 + fmt.Printf(" Last Updated: %s\n", updatedAt.Format("2006-01-02 15:04:05 MST")) 65 + fmt.Printf(" Age: %s ago\n", formatDuration(time.Since(updatedAt))) 66 + fmt.Printf("\n") 67 + 68 + // Operations estimate 69 + totalOps := bundleCount * bundle.BUNDLE_SIZE 70 + fmt.Printf("🔢 Operations (estimate)\n") 71 + fmt.Printf(" Total: %s\n", formatNumber(totalOps)) 72 + if duration.Hours() > 0 { 73 + opsPerHour := float64(totalOps) / duration.Hours() 74 + fmt.Printf(" Rate: %.0f ops/hour\n", opsPerHour) 75 + } 76 + fmt.Printf("\n") 77 + 78 + // Gaps 79 + gaps := index.FindGaps() 80 + if len(gaps) > 0 { 81 + fmt.Printf("⚠️ Missing Bundles: %d\n", len(gaps)) 82 + if len(gaps) <= 10 || verbose { 83 + fmt.Printf(" ") 84 + for i, gap := range gaps { 85 + if i > 0 { 86 + fmt.Printf(", ") 87 + } 88 + if i > 0 && i%10 == 0 { 89 + fmt.Printf("\n ") 90 + } 91 + fmt.Printf("%06d", gap) 92 + } 93 + fmt.Printf("\n") 94 + } else { 95 + fmt.Printf(" First few: ") 96 + for i := 0; i < 5; i++ { 97 + if i > 0 { 98 + fmt.Printf(", ") 99 + } 100 + fmt.Printf("%06d", gaps[i]) 101 + } 102 + fmt.Printf(" ... (use -v to show all)\n") 103 + } 104 + fmt.Printf("\n") 105 + } 106 + 107 + // Mempool 108 + mempoolStats := mgr.GetMempoolStats() 109 + mempoolCount := mempoolStats["count"].(int) 110 + if mempoolCount > 0 { 111 + targetBundle := mempoolStats["target_bundle"].(int) 112 + canCreate := mempoolStats["can_create_bundle"].(bool) 113 + progress := float64(mempoolCount) / float64(bundle.BUNDLE_SIZE) * 100 114 + 115 + fmt.Printf("🔄 Mempool (next bundle: %06d)\n", targetBundle) 116 + fmt.Printf(" Operations: %s / %s\n", formatNumber(mempoolCount), formatNumber(bundle.BUNDLE_SIZE)) 117 + fmt.Printf(" Progress: %.1f%%\n", progress) 118 + 119 + // Progress bar 120 + barWidth := 40 121 + filled := int(float64(barWidth) * float64(mempoolCount) / float64(bundle.BUNDLE_SIZE)) 122 + if filled > barWidth { 123 + filled = barWidth 124 + } 125 + bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled) 126 + fmt.Printf(" [%s]\n", bar) 127 + 128 + if canCreate { 129 + fmt.Printf(" ✓ Ready to create bundle\n") 130 + } else { 131 + remaining := bundle.BUNDLE_SIZE - mempoolCount 132 + fmt.Printf(" Need %s more operations\n", formatNumber(remaining)) 133 + } 134 + fmt.Printf("\n") 135 + } 136 + 137 + // Chain verification 138 + if verify { 139 + fmt.Printf("🔐 Chain Verification\n") 140 + fmt.Printf(" Verifying %d bundles...\n", bundleCount) 141 + 142 + ctx := context.Background() 143 + result, err := mgr.VerifyChain(ctx) 144 + if err != nil { 145 + fmt.Printf(" ✗ Verification failed: %v\n", err) 146 + } else if result.Valid { 147 + fmt.Printf(" ✓ Chain is valid\n") 148 + fmt.Printf(" ✓ All %d bundles verified\n", len(result.VerifiedBundles)) 149 + 150 + // Show head hash 151 + lastMeta, _ := index.GetBundle(lastBundle) 152 + if lastMeta != nil { 153 + fmt.Printf(" Head: %s\n", lastMeta.Hash[:16]+"...") 154 + } 155 + } else { 156 + fmt.Printf(" ✗ Chain is broken\n") 157 + fmt.Printf(" Verified: %d/%d bundles\n", len(result.VerifiedBundles), bundleCount) 158 + fmt.Printf(" Broken at: %06d\n", result.BrokenAt) 159 + fmt.Printf(" Error: %s\n", result.Error) 160 + } 161 + fmt.Printf("\n") 162 + } 163 + 164 + // Timeline visualization 165 + if showTimeline { 166 + fmt.Printf("📈 Timeline Visualization\n") 167 + visualizeTimeline(index, verbose) 168 + fmt.Printf("\n") 169 + } 170 + 171 + // Bundle list 172 + if showBundles { 173 + bundles := index.GetBundles() 174 + fmt.Printf("📚 Bundle List (%d total)\n", len(bundles)) 175 + fmt.Printf("\n") 176 + fmt.Printf(" Number | Start Time | End Time | Ops | DIDs | Size\n") 177 + fmt.Printf(" ---------|---------------------|---------------------|--------|--------|--------\n") 178 + 179 + for _, meta := range bundles { 180 + fmt.Printf(" %06d | %s | %s | %6d | %6d | %7s\n", 181 + meta.BundleNumber, 182 + meta.StartTime.Format("2006-01-02 15:04"), 183 + meta.EndTime.Format("2006-01-02 15:04"), 184 + meta.OperationCount, 185 + meta.DIDCount, 186 + formatBytes(meta.CompressedSize)) 187 + } 188 + fmt.Printf("\n") 189 + } else if bundleCount > 0 { 190 + fmt.Printf("💡 Tip: Use --bundles to see detailed bundle list\n") 191 + fmt.Printf(" Use --timeline to see timeline visualization\n") 192 + fmt.Printf(" Use --verify to verify chain integrity\n") 193 + fmt.Printf("\n") 194 + } 195 + 196 + // File system stats (verbose) 197 + if verbose { 198 + fmt.Printf("💾 File System\n") 199 + 200 + // Calculate average compression ratio 201 + bundles := index.GetBundles() 202 + var totalCompressed, totalUncompressed int64 203 + for _, meta := range bundles { 204 + totalCompressed += meta.CompressedSize 205 + totalUncompressed += meta.UncompressedSize 206 + } 207 + if totalCompressed > 0 { 208 + avgRatio := float64(totalUncompressed) / float64(totalCompressed) 209 + savings := (1 - float64(totalCompressed)/float64(totalUncompressed)) * 100 210 + fmt.Printf(" Compression: %.2fx average ratio\n", avgRatio) 211 + fmt.Printf(" Space Saved: %.1f%% (%s)\n", savings, formatBytes(totalUncompressed-totalCompressed)) 212 + } 213 + 214 + // Index size 215 + indexPath := info["index_path"].(string) 216 + if indexInfo, err := os.Stat(indexPath); err == nil { 217 + fmt.Printf(" Index Size: %s\n", formatBytes(indexInfo.Size())) 218 + } 219 + 220 + fmt.Printf("\n") 221 + } 222 + } 223 + 224 + func visualizeTimeline(index *bundle.Index, verbose bool) { 225 + bundles := index.GetBundles() 226 + if len(bundles) == 0 { 227 + return 228 + } 229 + 230 + // Group bundles by date 231 + type dateGroup struct { 232 + date string 233 + count int 234 + first int 235 + last int 236 + } 237 + 238 + dateMap := make(map[string]*dateGroup) 239 + for _, meta := range bundles { 240 + dateStr := meta.StartTime.Format("2006-01-02") 241 + if group, exists := dateMap[dateStr]; exists { 242 + group.count++ 243 + group.last = meta.BundleNumber 244 + } else { 245 + dateMap[dateStr] = &dateGroup{ 246 + date: dateStr, 247 + count: 1, 248 + first: meta.BundleNumber, 249 + last: meta.BundleNumber, 250 + } 251 + } 252 + } 253 + 254 + // Sort dates 255 + var dates []string 256 + for date := range dateMap { 257 + dates = append(dates, date) 258 + } 259 + sort.Strings(dates) 260 + 261 + // Find max count for scaling 262 + maxCount := 0 263 + for _, group := range dateMap { 264 + if group.count > maxCount { 265 + maxCount = group.count 266 + } 267 + } 268 + 269 + // Display 270 + fmt.Printf("\n") 271 + barWidth := 40 272 + for _, date := range dates { 273 + group := dateMap[date] 274 + barLen := int(float64(barWidth) * float64(group.count) / float64(maxCount)) 275 + if barLen == 0 && group.count > 0 { 276 + barLen = 1 277 + } 278 + 279 + bar := strings.Repeat("█", barLen) 280 + fmt.Printf(" %s | %-40s | %3d bundles", group.date, bar, group.count) 281 + if verbose { 282 + fmt.Printf(" (%06d-%06d)", group.first, group.last) 283 + } 284 + fmt.Printf("\n") 285 + } 286 + } 287 + 288 + // Helper formatting functions 289 + 290 + func formatNumber(n int) string { 291 + s := fmt.Sprintf("%d", n) 292 + // Add thousand separators 293 + var result []byte 294 + for i, c := range s { 295 + if i > 0 && (len(s)-i)%3 == 0 { 296 + result = append(result, ',') 297 + } 298 + result = append(result, byte(c)) 299 + } 300 + return string(result) 301 + } 302 + 303 + func formatBytes(bytes int64) string { 304 + const unit = 1024 305 + if bytes < unit { 306 + return fmt.Sprintf("%d B", bytes) 307 + } 308 + div, exp := int64(unit), 0 309 + for n := bytes / unit; n >= unit; n /= unit { 310 + div *= unit 311 + exp++ 312 + } 313 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 314 + } 315 + 316 + func formatDuration(d time.Duration) string { 317 + if d < time.Minute { 318 + return fmt.Sprintf("%.0f seconds", d.Seconds()) 319 + } 320 + if d < time.Hour { 321 + return fmt.Sprintf("%.1f minutes", d.Minutes()) 322 + } 323 + if d < 24*time.Hour { 324 + return fmt.Sprintf("%.1f hours", d.Hours()) 325 + } 326 + days := d.Hours() / 24 327 + if days < 30 { 328 + return fmt.Sprintf("%.1f days", days) 329 + } 330 + if days < 365 { 331 + return fmt.Sprintf("%.1f months", days/30) 332 + } 333 + return fmt.Sprintf("%.1f years", days/365) 334 + }
+127 -29
cmd/plcbundle/main.go
··· 474 func cmdInfo() { 475 fs := flag.NewFlagSet("info", flag.ExitOnError) 476 bundleNum := fs.Int("bundle", 0, "specific bundle info (0 = general info)") 477 fs.Parse(os.Args[2:]) 478 479 mgr, dir, err := getManager("") ··· 484 defer mgr.Close() 485 486 if *bundleNum > 0 { 487 - // Show specific bundle info 488 - ctx := context.Background() 489 - b, err := mgr.LoadBundle(ctx, *bundleNum) 490 - if err != nil { 491 - fmt.Fprintf(os.Stderr, "Error: %v\n", err) 492 - os.Exit(1) 493 - } 494 495 - fmt.Printf("Bundle %06d:\n", b.BundleNumber) 496 - fmt.Printf(" Directory: %s\n", dir) 497 - fmt.Printf(" Time range: %s - %s\n", b.StartTime.Format("2006-01-02 15:04:05"), b.EndTime.Format("2006-01-02 15:04:05")) 498 - fmt.Printf(" Operations: %d\n", len(b.Operations)) 499 - fmt.Printf(" Unique DIDs: %d\n", b.DIDCount) 500 - fmt.Printf(" Hash: %s\n", b.Hash) 501 - fmt.Printf(" Compressed: %.2f MB\n", float64(b.CompressedSize)/(1024*1024)) 502 - fmt.Printf(" Uncompressed: %.2f MB\n", float64(b.UncompressedSize)/(1024*1024)) 503 - fmt.Printf(" Compression ratio: %.2fx\n", b.CompressionRatio()) 504 - fmt.Printf(" Cursor: %s\n", b.Cursor) 505 if b.PrevBundleHash != "" { 506 - fmt.Printf(" Prev bundle hash: %s...\n", b.PrevBundleHash[:16]) 507 } 508 - } else { 509 - // Show general info 510 - info := mgr.GetInfo() 511 - fmt.Printf("Bundle Directory: %s\n", dir) 512 - fmt.Printf("Bundle count: %v\n", info["bundle_count"]) 513 - if bc, ok := info["bundle_count"].(int); ok && bc > 0 { 514 - fmt.Printf("Range: %06d - %06d\n", info["first_bundle"], info["last_bundle"]) 515 - fmt.Printf("Total size: %.2f MB\n", float64(info["total_size"].(int64))/(1024*1024)) 516 - if gaps, ok := info["gaps"].(int); ok && gaps > 0 { 517 - fmt.Printf("⚠ Missing bundles: %d\n", gaps) 518 } 519 } 520 - fmt.Printf("Index updated: %s\n", info["updated_at"]) 521 } 522 } 523
··· 474 func cmdInfo() { 475 fs := flag.NewFlagSet("info", flag.ExitOnError) 476 bundleNum := fs.Int("bundle", 0, "specific bundle info (0 = general info)") 477 + verbose := fs.Bool("v", false, "verbose output") 478 + showBundles := fs.Bool("bundles", false, "show bundle list") 479 + verify := fs.Bool("verify", false, "verify chain integrity") 480 + showTimeline := fs.Bool("timeline", false, "show timeline visualization") 481 fs.Parse(os.Args[2:]) 482 483 mgr, dir, err := getManager("") ··· 488 defer mgr.Close() 489 490 if *bundleNum > 0 { 491 + showBundleInfo(mgr, dir, *bundleNum, *verbose) 492 + } else { 493 + showGeneralInfo(mgr, dir, *verbose, *showBundles, *verify, *showTimeline) 494 + } 495 + } 496 + 497 + func showBundleInfo(mgr *bundle.Manager, dir string, bundleNum int, verbose bool) { 498 + ctx := context.Background() 499 + b, err := mgr.LoadBundle(ctx, bundleNum) 500 + if err != nil { 501 + fmt.Fprintf(os.Stderr, "Error: %v\n", err) 502 + os.Exit(1) 503 + } 504 505 + fmt.Printf("\n") 506 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 507 + fmt.Printf(" Bundle %06d\n", b.BundleNumber) 508 + fmt.Printf("═══════════════════════════════════════════════════════════════\n") 509 + fmt.Printf("\n") 510 + 511 + // Location 512 + fmt.Printf("📁 Location\n") 513 + fmt.Printf(" Directory: %s\n", dir) 514 + fmt.Printf(" File: %06d.jsonl.zst\n", b.BundleNumber) 515 + fmt.Printf("\n") 516 + 517 + // Time Range 518 + duration := b.EndTime.Sub(b.StartTime) 519 + fmt.Printf("📅 Time Range\n") 520 + fmt.Printf(" Start: %s\n", b.StartTime.Format("2006-01-02 15:04:05.000 MST")) 521 + fmt.Printf(" End: %s\n", b.EndTime.Format("2006-01-02 15:04:05.000 MST")) 522 + fmt.Printf(" Duration: %s\n", formatDuration(duration)) 523 + fmt.Printf(" Created: %s\n", b.CreatedAt.Format("2006-01-02 15:04:05 MST")) 524 + fmt.Printf("\n") 525 + 526 + // Content 527 + fmt.Printf("📊 Content\n") 528 + fmt.Printf(" Operations: %s\n", formatNumber(len(b.Operations))) 529 + fmt.Printf(" Unique DIDs: %s\n", formatNumber(b.DIDCount)) 530 + if len(b.Operations) > 0 { 531 + avgOpsPerDID := float64(len(b.Operations)) / float64(b.DIDCount) 532 + fmt.Printf(" Avg ops/DID: %.2f\n", avgOpsPerDID) 533 + } 534 + fmt.Printf("\n") 535 + 536 + // Size 537 + fmt.Printf("💾 Size\n") 538 + fmt.Printf(" Compressed: %s\n", formatBytes(b.CompressedSize)) 539 + fmt.Printf(" Uncompressed: %s\n", formatBytes(b.UncompressedSize)) 540 + fmt.Printf(" Ratio: %.2fx\n", b.CompressionRatio()) 541 + fmt.Printf(" Efficiency: %.1f%% savings\n", (1-float64(b.CompressedSize)/float64(b.UncompressedSize))*100) 542 + fmt.Printf("\n") 543 + 544 + // Hashes 545 + fmt.Printf("🔐 Cryptographic Hashes\n") 546 + fmt.Printf(" Content (SHA-256):\n") 547 + fmt.Printf(" %s\n", b.Hash) 548 + fmt.Printf(" Compressed:\n") 549 + fmt.Printf(" %s\n", b.CompressedHash) 550 + if b.PrevBundleHash != "" { 551 + fmt.Printf(" Previous Bundle:\n") 552 + fmt.Printf(" %s\n", b.PrevBundleHash) 553 + } 554 + fmt.Printf("\n") 555 + 556 + // Chain 557 + if b.PrevBundleHash != "" || b.Cursor != "" { 558 + fmt.Printf("🔗 Chain Information\n") 559 + if b.Cursor != "" { 560 + fmt.Printf(" Cursor: %s\n", b.Cursor) 561 + } 562 if b.PrevBundleHash != "" { 563 + fmt.Printf(" Links to: Bundle %06d\n", bundleNum-1) 564 + } 565 + if len(b.BoundaryCIDs) > 0 { 566 + fmt.Printf(" Boundary: %d CIDs at same timestamp\n", len(b.BoundaryCIDs)) 567 + } 568 + fmt.Printf("\n") 569 + } 570 + 571 + // Verbose: Show sample operations 572 + if verbose && len(b.Operations) > 0 { 573 + fmt.Printf("📝 Sample Operations (first 5)\n") 574 + showCount := 5 575 + if len(b.Operations) < showCount { 576 + showCount = len(b.Operations) 577 } 578 + for i := 0; i < showCount; i++ { 579 + op := b.Operations[i] 580 + fmt.Printf(" %d. %s\n", i+1, op.DID) 581 + fmt.Printf(" CID: %s\n", op.CID) 582 + fmt.Printf(" Time: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000")) 583 + if op.IsNullified() { 584 + fmt.Printf(" ⚠️ Nullified: %s\n", op.GetNullifyingCID()) 585 } 586 } 587 + fmt.Printf("\n") 588 + } 589 + 590 + // Verbose: Show DID statistics 591 + if verbose && len(b.Operations) > 0 { 592 + didOps := make(map[string]int) 593 + for _, op := range b.Operations { 594 + didOps[op.DID]++ 595 + } 596 + 597 + // Find most active DIDs 598 + type didCount struct { 599 + did string 600 + count int 601 + } 602 + var counts []didCount 603 + for did, count := range didOps { 604 + counts = append(counts, didCount{did, count}) 605 + } 606 + sort.Slice(counts, func(i, j int) bool { 607 + return counts[i].count > counts[j].count 608 + }) 609 + 610 + fmt.Printf("🏆 Most Active DIDs\n") 611 + showCount := 5 612 + if len(counts) < showCount { 613 + showCount = len(counts) 614 + } 615 + for i := 0; i < showCount; i++ { 616 + fmt.Printf(" %d. %s (%d ops)\n", i+1, counts[i].did, counts[i].count) 617 + } 618 + fmt.Printf("\n") 619 } 620 } 621
+38 -11
cmd/plcbundle/server.go
··· 144 index := mgr.GetIndex() 145 bundles := index.GetBundles() 146 147 - currentRecord := 0 148 149 - // Stream all operations from all bundles 150 - for _, meta := range bundles { 151 // Load bundle 152 b, err := mgr.LoadBundle(ctx, meta.BundleNumber) 153 if err != nil { ··· 155 continue 156 } 157 158 - // Send each operation 159 - for _, op := range b.Operations { 160 - // Skip records before cursor 161 - if currentRecord < cursor { 162 - currentRecord++ 163 - continue 164 - } 165 166 // Send raw JSON 167 if err := sendOperation(conn, op); err != nil { ··· 179 } 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)
··· 144 index := mgr.GetIndex() 145 bundles := index.GetBundles() 146 147 + if len(bundles) == 0 { 148 + return 149 + } 150 + 151 + // Calculate starting bundle and position from cursor 152 + // Each bundle has exactly BUNDLE_SIZE (10,000) operations 153 + // Cursor 88410345 = bundle 8841, position 345 154 + startBundleIdx := cursor / bundle.BUNDLE_SIZE 155 + startPosition := cursor % bundle.BUNDLE_SIZE 156 + 157 + // Validate starting bundle exists 158 + if startBundleIdx >= len(bundles) { 159 + // Cursor is beyond all bundles, check mempool 160 + currentRecord := len(bundles) * bundle.BUNDLE_SIZE 161 + streamMempool(conn, mgr, cursor, currentRecord) 162 + return 163 + } 164 165 + currentRecord := cursor 166 + 167 + // Stream bundles starting from the calculated bundle 168 + for i := startBundleIdx; i < len(bundles); i++ { 169 + meta := bundles[i] 170 + 171 // Load bundle 172 b, err := mgr.LoadBundle(ctx, meta.BundleNumber) 173 if err != nil { ··· 175 continue 176 } 177 178 + // Determine starting position in this bundle 179 + startPos := 0 180 + if i == startBundleIdx { 181 + startPos = startPosition 182 + } 183 + 184 + // Send operations from this bundle 185 + for j := startPos; j < len(b.Operations); j++ { 186 + op := b.Operations[j] 187 188 // Send raw JSON 189 if err := sendOperation(conn, op); err != nil { ··· 201 } 202 } 203 204 + // Stream mempool operations after all bundles 205 + streamMempool(conn, mgr, cursor, currentRecord) 206 + } 207 + 208 + // streamMempool streams mempool operations if cursor is in range 209 + func streamMempool(conn *websocket.Conn, mgr *bundle.Manager, cursor int, currentRecord int) { 210 mempoolOps, err := mgr.GetMempoolOperations() 211 if err != nil { 212 fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err)
+1 -1
plc/plc_test.go
··· 244 } 245 246 logger := &benchLogger{} 247 - operations, _ := bundle.NewOperations(bundle.CompressionBetter, logger) 248 defer operations.Close() 249 250 b.ResetTimer()
··· 244 } 245 246 logger := &benchLogger{} 247 + operations, _ := bundle.NewOperations(logger) 248 defer operations.Close() 249 250 b.ResetTimer()
-7
plc_bundles.json
··· 1 - { 2 - "version": "1.0", 3 - "last_bundle": 0, 4 - "updated_at": "2025-10-28T02:44:11.775384Z", 5 - "total_size_bytes": 0, 6 - "bundles": [] 7 - }
···
-6
plcbundle.go
··· 16 Index = bundle.Index 17 Manager = bundle.Manager 18 Config = bundle.Config 19 - CompressionLevel = bundle.CompressionLevel 20 VerificationResult = bundle.VerificationResult 21 ChainVerificationResult = bundle.ChainVerificationResult 22 DirectoryScanResult = bundle.DirectoryScanResult ··· 31 const ( 32 BUNDLE_SIZE = bundle.BUNDLE_SIZE 33 INDEX_FILE = bundle.INDEX_FILE 34 - 35 - CompressionFastest = bundle.CompressionFastest 36 - CompressionDefault = bundle.CompressionDefault 37 - CompressionBetter = bundle.CompressionBetter 38 - CompressionBest = bundle.CompressionBest 39 ) 40 41 // NewManager creates a new bundle manager (convenience wrapper)
··· 16 Index = bundle.Index 17 Manager = bundle.Manager 18 Config = bundle.Config 19 VerificationResult = bundle.VerificationResult 20 ChainVerificationResult = bundle.ChainVerificationResult 21 DirectoryScanResult = bundle.DirectoryScanResult ··· 30 const ( 31 BUNDLE_SIZE = bundle.BUNDLE_SIZE 32 INDEX_FILE = bundle.INDEX_FILE 33 ) 34 35 // NewManager creates a new bundle manager (convenience wrapper)