Documentation
¶
Overview ¶
Package chunkflow provides lazily evaluated, type-safe data pipelines on top of Go's native iterators (iter.Seq). It targets stream processing and chunked I/O work (ETL-style batching) and comes in two flavours that share one fluent API:
- Stream[T] is synchronous and infallible. Every intermediate operation is a closure over the upstream iterator, nothing is buffered except by Chunk, and nothing runs until a terminal operation pulls.
- IoStream[T] adds a context.Context, error propagation and optional worker pools. Every Stream method exists on IoStream with the same name and shape; terminal operations additionally return an error, and callbacks that need the context or may fail have an *Async counterpart (MapAsync, FilterAsync, ReduceAsync, AllAsync, AnyAsync, ForEachAsync).
Constructing streams ¶
Both types are built from native iterators only. Generators live in the sub-package seq (Items, Range, RangeInclusive, Numbers, Const) so that they are equally usable with slices.Collect, range loops and this package:
evens := chunkflow.NewStream(seq.Range(0, 100)).
Filter(func(i int) bool { return i%2 == 0 }).
Collect()
rows, err := chunkflow.NewIoStream(ctx, seq.Items(ids...)).
MapAsync(fetchRow, chunkflow.WithParallel(8)).
Chunk[[]Row](500).
ForEachAsync(insertBatch)
NewIoStream2 wraps an iter.Seq2[T, error] and is the inverse of IoStream.Seq, which exposes any IoStream as a native (value, error) iterator.
Type-changing operations ¶
Go methods cannot introduce type parameters that constrain the receiver, so operations such as Flatten (Stream[[]E] -> Stream[E]) are top-level functions. Through keeps the left-to-right reading order when using them:
stream.Chunk[[]int](3).Through(chunkflow.Flatten) ioStream.Chunk[[]int](3).Through(chunkflow.IoFlatten)
Errors in IoStream ¶
An error, whether produced by the source or by a callback, travels down the pipeline as an element. Intermediate operations never interpret it: Map, Filter, Take, Skip, Chunk and IoFlatten forward it untouched and keep working on the values around it, so Take(n) and Skip(n) count values only. Terminal operations stop at the first error they see, which makes a plain pipeline fail fast and stop consuming the source right after the failing element.
CircuitBreaker(n) is the one operator that tolerates errors. Below its threshold it re-emits each error wrapped so that errors.Is(err, ErrSuppressed) is true while the original error stays in the chain; terminal operations skip such elements, and consumers of Seq can still observe them. On the n-th consecutive error the breaker trips with a fatal error. Context errors are never suppressed.
Short-circuiting and cancellation ¶
Every operation honours the yield protocol: when a consumer stops (Take, First, Any, All, or a plain break in a range loop), the whole chain stops pulling from the source. In IoStream a consumer stopping, or the context being cancelled, also shuts down any worker pool spawned by MapAsync or FilterAsync; the cancellation itself is reported as an error by the terminal operation.
Options ¶
WithParallel(n) sets the worker count for *Async operations (default 1, ordered; with n > 1 the output order is not guaranteed). WithLogger and WithDiscardLogger control diagnostics. Options passed to Opts become defaults for every downstream operation; options passed to a single operation apply to that call only.
Index ¶
- Variables
- type IoStream
- func (s IoStream[T]) All(predicate func(T) bool) (bool, error)
- func (s IoStream[T]) AllAsync(predicate func(ctx context.Context, item T) (bool, error)) (bool, error)
- func (s IoStream[T]) Any(predicate func(T) bool) (bool, error)
- func (s IoStream[T]) AnyAsync(predicate func(ctx context.Context, item T) (bool, error)) (bool, error)
- func (s IoStream[T]) Chunk[R []T](size int) IoStream[R]
- func (s IoStream[T]) CircuitBreaker(maxConsecutiveFailures int) IoStream[T]
- func (s IoStream[T]) Collect() ([]T, error)
- func (s IoStream[T]) Count() (int, error)
- func (s IoStream[T]) Exec() error
- func (s IoStream[T]) Filter(predicate func(T) bool) IoStream[T]
- func (s IoStream[T]) FilterAsync(predicate func(ctx context.Context, item T) (bool, error), opts ...Option) IoStream[T]
- func (s IoStream[T]) First() (T, bool, error)
- func (s IoStream[T]) ForEach(fn func(T)) error
- func (s IoStream[T]) ForEachAsync(fn func(ctx context.Context, item T) error) error
- func (s IoStream[T]) Last() (T, bool, error)
- func (s IoStream[T]) Map[R any](mapFn func(T) R) IoStream[R]
- func (s IoStream[T]) MapAsync[R any](mapFn func(ctx context.Context, item T) (R, error), opts ...Option) IoStream[R]
- func (s IoStream[T]) Opts(opts ...Option) IoStream[T]
- func (s IoStream[T]) Reduce(init T, fn func(item, acc T) T) (T, error)
- func (s IoStream[T]) ReduceAsync(init T, fn func(ctx context.Context, item, acc T) (T, error), opts ...Option) (T, error)
- func (s IoStream[T]) Seq() iter.Seq2[T, error]
- func (s IoStream[T]) Skip(nr int) IoStream[T]
- func (s IoStream[T]) Take(nr int) IoStream[T]
- func (s IoStream[T]) Through[R any](transform func(IoStream[T]) IoStream[R]) IoStream[R]
- type Option
- type Stream
- func (a Stream[T]) All(predicate func(T) bool) bool
- func (a Stream[T]) Any(predicate func(T) bool) bool
- func (a Stream[T]) Chunk[R []T](size int) Stream[R]
- func (a Stream[T]) Collect() []T
- func (a Stream[T]) Count() int
- func (a Stream[T]) Exec()
- func (a Stream[T]) Filter(predicate func(T) bool) Stream[T]
- func (a Stream[T]) First() (T, bool)
- func (a Stream[T]) ForEach(fn func(T))
- func (a Stream[T]) Last() (T, bool)
- func (a Stream[T]) Map[R any](mapFn func(T) R) Stream[R]
- func (a Stream[T]) Reduce(init T, fn func(item, acc T) T) T
- func (a Stream[T]) Seq() iter.Seq[T]
- func (a Stream[T]) Skip(nr int) Stream[T]
- func (a Stream[T]) Take(nr int) Stream[T]
- func (a Stream[T]) Through[R any](transform func(Stream[T]) Stream[R]) Stream[R]
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrSuppressed = errors.New("suppressed by circuit breaker")
ErrSuppressed marks an error that a CircuitBreaker decided to tolerate. Terminal operations skip elements carrying such an error instead of failing. Use errors.Is(err, ErrSuppressed) to detect one, for example when iterating IoStream.Seq(); the original error stays in the chain, so errors.Is(err, original) keeps working too.
Functions ¶
This section is empty.
Types ¶
type IoStream ¶
type IoStream[T any] struct { // contains filtered or unexported fields }
IoStream represents a lazily evaluated, context-aware pipeline whose elements may carry errors. It mirrors the Stream API; every method that accepts a user callback has an *Async counterpart that receives a context and may return an error.
Error semantics: an error produced by the source or by a callback travels down the pipeline as an element. Intermediate operations pass it through untouched and keep processing the remaining input; they operate on values only and never interpret errors. CircuitBreaker may mark an error as tolerated (see ErrSuppressed). Terminal operations skip elements whose error matches ErrSuppressed and stop at the first other error, so a pipeline without a CircuitBreaker behaves as "fail fast".
func IoFlatten ¶
IoFlatten unwraps a stream of slices into a flat stream of individual elements. It is the IoStream counterpart of Flatten; use it with Through.
func NewIoStream ¶
NewIoStream wraps a native Go iterator into a context-aware IoStream. The context is checked before every element is emitted.
Example ¶
package main
import (
"context"
"fmt"
"strings"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
upper, err := chunkflow.NewIoStream(ctx, seq.Items("go", "iter", "seq")).
Map(strings.ToUpper).
Collect()
fmt.Println(upper, err)
}
Output: [GO ITER SEQ] <nil>
func NewIoStream2 ¶
NewIoStream2 wraps a native (value, error) iterator into an IoStream. It is the inverse of IoStream.Seq.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/shults/chunkflow"
)
func main() {
ctx := context.Background()
// A (value, error) iterator, e.g. wrapping a database cursor.
rows := func(yield func(string, error) bool) {
for _, r := range []string{"alice", "bob"} {
if !yield(r, nil) {
return
}
}
yield("", errors.New("connection lost"))
}
got, err := chunkflow.NewIoStream2(ctx, rows).Collect()
fmt.Println(got, err)
}
Output: [alice bob] connection lost
func (IoStream[T]) All ¶
All verifies whether all elements satisfy the predicate. Short-circuits on the first mismatch.
On an empty stream, or one consisting only of suppressed errors, All returns true (vacuous truth): no element could violate the predicate. All(p) == !Any(!p) always holds.
func (IoStream[T]) AllAsync ¶
func (s IoStream[T]) AllAsync(predicate func(ctx context.Context, item T) (bool, error)) (bool, error)
AllAsync verifies whether all elements satisfy the context-aware predicate.
func (IoStream[T]) Any ¶
Any verifies whether at least one element satisfies the predicate. Short-circuits on the first match.
On an empty stream, or one consisting only of suppressed errors, Any returns false: no element could be a witness.
func (IoStream[T]) AnyAsync ¶
func (s IoStream[T]) AnyAsync(predicate func(ctx context.Context, item T) (bool, error)) (bool, error)
AnyAsync verifies whether at least one element satisfies the context-aware predicate.
func (IoStream[T]) Chunk ¶
Chunk groups elements into slices of the given size. The final chunk may contain fewer elements than size. Emits an error if size < 1. Errors are passed through immediately; the partially filled chunk is kept and continues to accumulate subsequent elements.
Example ¶
package main
import (
"context"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
// Chunk turns N+1 round trips into N/size batches.
err := chunkflow.NewIoStream(ctx, seq.Range(1, 8)).
Chunk(3).
ForEach(func(batch []int) {
fmt.Println("insert", batch)
})
fmt.Println(err)
}
Output: insert [1 2 3] insert [4 5 6] insert [7] <nil>
func (IoStream[T]) CircuitBreaker ¶
CircuitBreaker tolerates up to maxConsecutiveFailures-1 errors in a row and trips on the next one, interrupting the stream with a wrapped error. A successful element resets the counter.
Tolerated errors are not dropped: they are re-emitted wrapped in an error matching ErrSuppressed so that downstream terminal operations skip them while consumers of Seq() can still observe them. Errors already marked as suppressed by an upstream breaker pass through without affecting the counter. Context errors (context.Canceled, context.DeadlineExceeded) are never suppressed.
After a parallel stage the notion of "consecutive" follows arrival order, not source order.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
errFlaky := errors.New("flaky")
fetch := func(_ context.Context, i int) (int, error) {
if i == 3 || i == 4 {
return 0, errFlaky
}
return i * 10, nil
}
// Up to 2 consecutive failures are tolerated; the 3rd in a row would trip the breaker.
got, err := chunkflow.NewIoStream(ctx, seq.Range(1, 7)).
MapAsync(fetch).
CircuitBreaker(3).
Collect()
fmt.Println(got, err)
}
Output: [10 20 50 60] <nil>
Example (Tripped) ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
errDown := errors.New("service down")
fetch := func(_ context.Context, i int) (int, error) {
if i >= 3 {
return 0, errDown
}
return i, nil
}
got, err := chunkflow.NewIoStream(ctx, seq.Numbers(1)). // infinite source
MapAsync(fetch).
CircuitBreaker(3).
Collect()
fmt.Println(got)
fmt.Println(err)
fmt.Println(errors.Is(err, errDown))
}
Output: [1 2] circuit breaker tripped after 3 consecutive errors: service down true
func (IoStream[T]) Collect ¶
Collect materializes the stream into a slice. Elements collected before an error are returned together with that error.
func (IoStream[T]) Count ¶
Count consumes the entire stream and returns the number of elements seen before the first error.
func (IoStream[T]) FilterAsync ¶
func (s IoStream[T]) FilterAsync(predicate func(ctx context.Context, item T) (bool, error), opts ...Option) IoStream[T]
FilterAsync emits only the elements for which the context-aware predicate returns true. With WithParallel(n > 1) predicates are evaluated by n workers and the output order is not guaranteed. The first error terminates the stream.
func (IoStream[T]) First ¶
First consumes at most one element. Returns (zero, false, nil) for an empty stream.
func (IoStream[T]) ForEach ¶
ForEach executes a side effect for each element and returns the first error.
func (IoStream[T]) ForEachAsync ¶
ForEachAsync executes a context-aware side effect for each element and returns the first error.
func (IoStream[T]) MapAsync ¶
func (s IoStream[T]) MapAsync[R any](mapFn func(ctx context.Context, item T) (R, error), opts ...Option) IoStream[R]
MapAsync transforms each element using a context-aware function that may fail. With WithParallel(n > 1) elements are processed by n workers and the output order is not guaranteed. The first error terminates the stream.
Example ¶
package main
import (
"context"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
parse := func(_ context.Context, s string) (int, error) {
var n int
_, err := fmt.Sscanf(s, "%d", &n)
return n, err
}
// The first error stops the pipeline; values collected before it are returned.
nums, err := chunkflow.NewIoStream(ctx, seq.Items("1", "2", "x", "4")).
MapAsync(parse).
Collect()
fmt.Println(nums, err != nil)
}
Output: [1 2] true
Example (Parallel) ¶
package main
import (
"context"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
square := func(_ context.Context, i int) (int, error) { return i * i, nil }
// With WithParallel(n) the output order is not guaranteed, so reduce instead of collecting.
sum, err := chunkflow.NewIoStream(ctx, seq.RangeInclusive(1, 100)).
MapAsync(square, chunkflow.WithParallel(4)).
Reduce(0, func(item, acc int) int { return acc + item })
fmt.Println(sum, err)
}
Output: 338350 <nil>
func (IoStream[T]) Opts ¶
Opts returns a copy of the stream with the given options applied as defaults for all subsequent operations.
func (IoStream[T]) ReduceAsync ¶
func (s IoStream[T]) ReduceAsync(init T, fn func(ctx context.Context, item, acc T) (T, error), opts ...Option) (T, error)
ReduceAsync aggregates the stream into a single value using a context-aware function. Reduction is always sequential; WithParallel is accepted for API symmetry and logged.
func (IoStream[T]) Seq ¶
Seq returns the stream as a native (value, error) iterator. Elements tolerated by a CircuitBreaker are yielded with an error matching ErrSuppressed; iteration stops after the first error that is not suppressed.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
errOdd := errors.New("odd")
rejectOdd := func(_ context.Context, i int) (int, error) {
if i%2 == 1 {
return 0, errOdd
}
return i, nil
}
// Seq exposes suppressed errors instead of hiding them, so the consumer can
// count or log what the breaker tolerated.
tolerated := 0
for v, err := range chunkflow.NewIoStream(ctx, seq.Range(0, 6)).MapAsync(rejectOdd).CircuitBreaker(2).Seq() {
switch {
case err == nil:
fmt.Println("value", v)
case errors.Is(err, chunkflow.ErrSuppressed):
tolerated++
default:
fmt.Println("fatal", err)
}
}
fmt.Println("tolerated", tolerated)
}
Output: value 0 value 2 value 4 tolerated 3
func (IoStream[T]) Skip ¶
Skip bypasses the first nr elements and emits the remainder of the stream. Errors are passed through and do not count towards nr.
func (IoStream[T]) Take ¶
Take consumes at most nr elements from the stream and then short-circuits. Errors are passed through and do not count towards nr.
func (IoStream[T]) Through ¶
Through pipes the current stream into an external transformation function. It acts as a structural bridge to maintain fluent API chaining for operations that must be implemented as top-level functions (like IoFlatten).
Example ¶
package main
import (
"context"
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
ctx := context.Background()
// Reusable pipeline fragments are plain functions; Through plugs them in.
onlyEven := func(s chunkflow.IoStream[int]) chunkflow.IoStream[int] {
return s.Filter(func(i int) bool { return i%2 == 0 })
}
got, err := chunkflow.NewIoStream(ctx, seq.Range(0, 10)).
Through(onlyEven).
Chunk[[]int](2).
Through(chunkflow.IoFlatten).
Collect()
fmt.Println(got, err)
}
Output: [0 2 4 6 8] <nil>
type Option ¶
type Option func(*options)
Option configures an IoStream or a single asynchronous operation.
func WithDiscardLogger ¶
func WithDiscardLogger() Option
WithDiscardLogger drops all diagnostics. Handy to silence a logger previously set via Opts.
func WithLogger ¶
WithLogger sets the logger used for diagnostics. Diagnostics are discarded by default.
func WithParallel ¶
WithParallel sets the number of concurrent workers used by *Async operations.
type Stream ¶
type Stream[T any] struct { // contains filtered or unexported fields }
Stream represents a lazily evaluated pipeline operating on elements of type T. Transforming operations do not allocate memory until a terminal operation is called.
func Flatten ¶
Flatten unwraps a stream of slices into a continuous, flat stream of individual elements. It is implemented as a top-level function rather than a method because Go's compiler does not support narrowing receiver type constraints (i.e., enforcing that T must be a slice).
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
// Flatten changes the element type, so it is a top-level function.
// Through keeps the pipeline readable left to right.
flat := chunkflow.NewStream(seq.Range(1, 8)).
Chunk[[]int](3).
Through(chunkflow.Flatten).
Collect()
fmt.Println(flat)
}
Output: [1 2 3 4 5 6 7]
func NewStream ¶
NewStream initializes a stream based on a native Go iterator.
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
evens := chunkflow.NewStream(seq.Range(0, 10)).
Filter(func(i int) bool { return i%2 == 0 }).
Collect()
fmt.Println(evens)
}
Output: [0 2 4 6 8]
func (Stream[T]) All ¶
All is a terminal operation that verifies if all elements satisfy the predicate. Returns false and short-circuits on the first mismatch.
On an empty stream All returns true (vacuous truth): there is no element that could violate the predicate. This mirrors slices.ContainsFunc's complement and keeps All(p) == !Any(!p) for every stream.
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
s := chunkflow.NewStream(seq.Items[int]())
// Vacuous truth: nothing in the stream can violate the predicate.
fmt.Println(s.All(func(int) bool { return false }))
fmt.Println(s.Any(func(int) bool { return true }))
}
Output: true false
func (Stream[T]) Any ¶
Any is a terminal operation that verifies if at least one element satisfies the predicate. Returns true and short-circuits on the first match.
On an empty stream Any returns false: there is no element that could be a witness.
func (Stream[T]) Chunk ¶
Chunk groups elements into slices of the given size. The final chunk may contain fewer elements than size. Panics if size < 1.
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
chunks := chunkflow.NewStream(seq.Range(1, 8)).
Chunk(3).
Collect()
fmt.Println(chunks)
}
Output: [[1 2 3] [4 5 6] [7]]
func (Stream[T]) Collect ¶
func (a Stream[T]) Collect() []T
Collect is a terminal operation that materializes the stream into a slice allocated on the heap. Unsafe for infinite streams.
func (Stream[T]) Count ¶
Count is a terminal operation that consumes the entire stream and returns the number of elements. Unsafe for infinite streams.
func (Stream[T]) Exec ¶
func (a Stream[T]) Exec()
Exec is a terminal operation that exhausts the stream to force the execution of side effects.
func (Stream[T]) First ¶
First consumes at most one element. Returns the element and true, or a zero value and false if the stream was empty.
func (Stream[T]) ForEach ¶
func (a Stream[T]) ForEach(fn func(T))
ForEach is a terminal operation that executes a side effect for each element.
func (Stream[T]) Map ¶
Map transforms each element of the stream using the provided function.
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
words := chunkflow.NewStream(seq.Items("a", "bb", "ccc")).
Map(func(s string) int { return len(s) }).
Collect()
fmt.Println(words)
}
Output: [1 2 3]
func (Stream[T]) Reduce ¶
func (a Stream[T]) Reduce(init T, fn func(item, acc T) T) T
Reduce is a terminal operation that aggregates the stream into a single value.
Example ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
sum := chunkflow.NewStream(seq.RangeInclusive(1, 10)).
Reduce(0, func(item, acc int) int { return acc + item })
fmt.Println(sum)
}
Output: 55
func (Stream[T]) Seq ¶
Seq returns the underlying native Go iterator for integration with standard packages.
func (Stream[T]) Take ¶
Take consumes at most nr elements from the stream and then short-circuits.
Example (Infinite) ¶
package main
import (
"fmt"
"github.com/shults/chunkflow"
"github.com/shults/chunkflow/seq"
)
func main() {
// Numbers is infinite; Take short-circuits so only 5 elements are ever generated.
first := chunkflow.NewStream(seq.Numbers(1)).
Map(func(i int) int { return i * i }).
Take(5).
Collect()
fmt.Println(first)
}
Output: [1 4 9 16 25]
Directories
¶
| Path | Synopsis |
|---|---|
|
Package seq provides generators of native Go iterators (iter.Seq) that can be fed into chunkflow.NewStream or chunkflow.NewIoStream, or used directly in range-over-func loops.
|
Package seq provides generators of native Go iterators (iter.Seq) that can be fed into chunkflow.NewStream or chunkflow.NewIoStream, or used directly in range-over-func loops. |