package tui import ( "context" "errors" "fmt" "go.etcd.io/bbolt" ) type Item[T any] struct { val T err error } func resolvePath(tx *bbolt.Tx, path [][]byte) (*bbolt.Bucket, error) { if len(path) == 0 { return nil, errors.New("can't find bucket without path") } name := path[0] bucket := tx.Bucket(name) if bucket == nil { return nil, fmt.Errorf("no bucket with name %s", name) } for _, name := range path[1:] { bucket = bucket.Bucket(name) if bucket == nil { return nil, fmt.Errorf("no bucket with name %s", name) } } return bucket, nil } func load[T any]( ctx context.Context, db *bbolt.DB, path [][]byte, bufferSize int, filter func(key, value []byte) bool, transform func(key, value []byte) T, ) (<-chan Item[T], error) { buffer := make(chan Item[T], bufferSize) go func() { defer close(buffer) err := db.View(func(tx *bbolt.Tx) error { var cursor *bbolt.Cursor if len(path) == 0 { cursor = tx.Cursor() } else { bucket, err := resolvePath(tx, path) if err != nil { buffer <- Item[T]{err: fmt.Errorf("can't find bucket: %w", err)} return nil } cursor = bucket.Cursor() } key, value := cursor.First() for { if key == nil { return nil } if filter != nil && !filter(key, value) { key, value = cursor.Next() continue } select { case <-ctx.Done(): return nil case buffer <- Item[T]{val: transform(key, value)}: key, value = cursor.Next() } } }) if err != nil { buffer <- Item[T]{err: err} } }() return buffer, nil } func LoadBuckets( ctx context.Context, db *bbolt.DB, path [][]byte, bufferSize int, ) (<-chan Item[*Bucket], error) { buffer, err := load( ctx, db, path, bufferSize, func(_, value []byte) bool { return value == nil }, func(key, _ []byte) *Bucket { base := make([][]byte, 0, len(path)) base = append(base, path...) return &Bucket{ Name: key, Path: append(base, key), } }, ) if err != nil { return nil, fmt.Errorf("can't start iterating bucket: %w", err) } return buffer, nil } func LoadRecords( ctx context.Context, db *bbolt.DB, path [][]byte, bufferSize int, ) (<-chan Item[*Record], error) { buffer, err := load( ctx, db, path, bufferSize, func(_, value []byte) bool { return value != nil }, func(key, value []byte) *Record { base := make([][]byte, 0, len(path)) base = append(base, path...) return &Record{ Key: key, Value: value, Path: append(base, key), } }, ) if err != nil { return nil, fmt.Errorf("can't start iterating bucket: %w", err) } return buffer, nil } // HasBuckets checks if a bucket has nested buckets. It relies on assumption // that a bucket can have either nested buckets or records but not both. func HasBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (bool, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() buffer, err := load( ctx, db, path, 1, nil, func(_, value []byte) []byte { return value }, ) if err != nil { return false, err } x, ok := <-buffer if !ok { return false, nil } if x.err != nil { return false, err } if x.val != nil { return false, err } return true, nil }