Documentation
¶
Overview ¶
Package stream provides composable, pull-based data stream operators plus a bounded push fan-out source.
Canonical shape ¶
gokit converges on the rskit-stream operator vocabulary (map/filter/fan_out/window/batch/parallel/merge/partition/…) but keeps an idiomatic Go pull-iterator model for transformation pipelines: pipelines are lazy — no work happens until values are pulled via Collect, Drain, or ForEach, and each stage pulls from the previous stage on demand. Pull gives natural, allocation-free backpressure without an explicit flow-control protocol.
The genuinely push-shaped concern — fanning one source out to many independent observers — is served by Broadcaster, mirroring rskit's Broadcaster<T>. Every subscriber owns a private bounded channel; a subscriber that lags beyond its buffer drops overflow (backpressure by drop) but never blocks the broadcaster or its peers.
Bounded buffers ¶
No operator buffers without bound. Buffer clamps size <= 0 to 1; Broadcaster clamps its per-subscriber buffer to at least 1; the time/size-aware operators (Batch, TumblingWindow, SlidingWindow) emit and release each group as it completes. Concurrent operators (Parallel, Merge, Buffer) run owned goroutines bounded by ctx cancellation and closed via the iterator's Close.
The Iterator interface is structurally compatible with provider.Iterator[T], so provider streams plug directly into pipelines.
Operators ¶
Synchronous (single-goroutine):
- Map: transform each value
- FlatMap: transform each value into multiple values
- Filter: keep values matching a predicate
- Tap: side-effect without altering the value (logging, metrics, mid-pipeline publish)
- TapEach: per-element side-effect on []T (e.g., after FanOut)
- FanOut: apply multiple functions in parallel, collect results as []O
- Reduce: accumulate all values into one result
- Concat: join pipelines sequentially
Concurrent (multi-goroutine):
- Buffer: decouple producer/consumer with a buffered channel
- Parallel: concurrent Map with a worker pool (order NOT preserved)
- Merge: combine multiple pipelines concurrently (order NOT preserved)
Stream/time-aware:
- Throttle: rate-limit values (drop values arriving faster than interval)
- Batch: collect N items or wait timeout, emit as slice
- Debounce: wait for silence before emitting the latest value
- TumblingWindow: non-overlapping fixed-duration windows
- SlidingWindow: overlapping windows with configurable slide
Push fan-out:
- Broadcaster: bounded, cancellable one-to-many event fan-out (drop overflow)
Usage ¶
src := stream.FromSlice([]int{1, 2, 3, 4, 5})
doubled := stream.Map(src, func(_ context.Context, n int) (int, error) {
return n * 2, nil
})
evens := stream.Filter(doubled, func(n int) bool { return n%2 == 0 })
results, _ := stream.Collect(ctx, evens)
With providers:
src := stream.From(audioSource) transcribed := stream.FlatMap(src, transcriber.Execute) tapped := stream.Tap(transcribed, kafkaPublish.Send) identified := stream.Map(tapped, speakerID.Execute) stream.Drain(identified, finalSink.Send).Run(ctx)
Index ¶
- Constants
- func Collect[T any](ctx context.Context, p *Pipeline[T]) ([]T, error)
- func ForEach[T any](ctx context.Context, p *Pipeline[T], fn func(context.Context, T) error) error
- func Partition[T any](p *Pipeline[T], predicate func(T) bool) (matching *Pipeline[T], rejected *Pipeline[T])
- type Broadcaster
- type BroadcasterOption
- type Executor
- type ExecutorOption
- type Iterator
- type Pipeline
- func Batch[T any](p *Pipeline[T], size int, timeout time.Duration) *Pipeline[[]T]
- func Buffer[T any](p *Pipeline[T], size int) *Pipeline[T]
- func Concat[T any](pipelines ...*Pipeline[T]) *Pipeline[T]
- func Debounce[T any](p *Pipeline[T], duration time.Duration) *Pipeline[T]
- func Distinct[T comparable](p *Pipeline[T]) *Pipeline[T]
- func FanOut[I, O any](p *Pipeline[I], fns ...func(context.Context, I) (O, error)) *Pipeline[[]O]
- func Filter[T any](p *Pipeline[T], fn func(T) bool) *Pipeline[T]
- func FlatMap[I, O any](p *Pipeline[I], fn func(context.Context, I) (Iterator[O], error)) *Pipeline[O]
- func From[T any](iter Iterator[T]) *Pipeline[T]
- func FromFunc[T any](fn func(ctx context.Context) Iterator[T]) *Pipeline[T]
- func FromSlice[T any](items []T) *Pipeline[T]
- func Map[I, O any](p *Pipeline[I], fn func(context.Context, I) (O, error)) *Pipeline[O]
- func Merge[T any](pipelines ...*Pipeline[T]) *Pipeline[T]
- func Parallel[I, O any](p *Pipeline[I], n int, fn func(context.Context, I) (O, error)) *Pipeline[O]
- func Reduce[T, R any](p *Pipeline[T], init R, fn func(R, T) R) *Pipeline[R]
- func Skip[T any](p *Pipeline[T], n int) *Pipeline[T]
- func SlidingWindow[T any](p *Pipeline[T], timeFn func(T) time.Time, windowSize, slideBy time.Duration) *Pipeline[[]T]
- func Take[T any](p *Pipeline[T], n int) *Pipeline[T]
- func Tap[T any](p *Pipeline[T], fn func(context.Context, T) error) *Pipeline[T]
- func TapEach[T any](p *Pipeline[[]T], fns ...func(context.Context, T) error) *Pipeline[[]T]
- func Throttle[T any](p *Pipeline[T], interval time.Duration) *Pipeline[T]
- func TumblingWindow[T any](p *Pipeline[T], duration time.Duration) *Pipeline[[]T]
- type Runnable
- type Step
- type StepProgress
- type StepResult
- type StepStatus
Examples ¶
Constants ¶
const DefaultBroadcastBuffer = 64
DefaultBroadcastBuffer is the per-subscriber buffer used when no buffer size is configured. A subscriber lagging more than this many unconsumed events drops the overflow rather than stalling the broadcaster.
Variables ¶
This section is empty.
Functions ¶
func Partition ¶
func Partition[T any](p *Pipeline[T], predicate func(T) bool) (matching *Pipeline[T], rejected *Pipeline[T])
Partition splits a pipeline into two streaming branches using predicate. The upstream is consumed once by a bounded tee. Both branches should be consumed concurrently; closing one branch drops values routed to it while the other branch continues.
Types ¶
type Broadcaster ¶
type Broadcaster[T any] struct { // contains filtered or unexported fields }
Broadcaster is a bounded, cancellable fan-out source: it turns "observe a backend" into a bounded stream of typed events delivered to many independent subscribers. Each subscriber owns a private bounded channel — a subscriber that falls further behind than the buffer loses interim events (backpressure by drop) but never blocks the broadcaster or its peers. This is the canonical owner for the "watch a source → typed change stream" shape that recurs across config reloads, service discovery, cache invalidation, and secret rotation.
Share a Broadcaster by pointer; every holder observes the same subscriber set. It is safe for concurrent use. NewBroadcaster is the canonical constructor, but the zero value is also usable: it lazily initializes on first use with the default buffer.
func NewBroadcaster ¶
func NewBroadcaster[T any](opts ...BroadcasterOption) *Broadcaster[T]
NewBroadcaster creates a Broadcaster with the given options. Without WithBroadcastBuffer it uses DefaultBroadcastBuffer.
func (*Broadcaster[T]) Broadcast ¶
func (b *Broadcaster[T]) Broadcast(item T)
Broadcast delivers item to every live subscriber. Delivery to a full subscriber is dropped rather than blocked, so a slow subscriber never stalls the broadcaster or its peers. Broadcasting after Close is a no-op.
func (*Broadcaster[T]) Buffer ¶
func (b *Broadcaster[T]) Buffer() int
Buffer returns the effective per-subscriber buffer size.
func (*Broadcaster[T]) Close ¶
func (b *Broadcaster[T]) Close()
Close terminates the Broadcaster: every subscriber channel is closed and all subscriber goroutines are released. It is idempotent, and further Broadcast calls become no-ops.
func (*Broadcaster[T]) Subscribe ¶
func (b *Broadcaster[T]) Subscribe(ctx context.Context) <-chan T
Subscribe registers a new subscriber and returns its receive-only event channel. The channel is closed — terminating any range over it — when ctx is canceled or the Broadcaster is closed. Subscribing with an already-canceled context, or to a closed Broadcaster, returns an already-closed channel without registering a subscriber or spawning a watcher goroutine.
func (*Broadcaster[T]) SubscriberCount ¶
func (b *Broadcaster[T]) SubscriberCount() int
SubscriberCount returns the number of currently live subscribers.
type BroadcasterOption ¶
type BroadcasterOption func(*broadcasterConfig)
BroadcasterOption configures a Broadcaster at construction time.
func WithBroadcastBuffer ¶
func WithBroadcastBuffer(size int) BroadcasterOption
WithBroadcastBuffer sets the per-subscriber buffer size. Values below 1 are clamped to 1 so every subscriber can hold at least one in-flight event.
type Executor ¶
type Executor[T any] struct { // contains filtered or unexported fields }
Executor runs a sequence of steps with progress reporting.
func NewExecutor ¶
func NewExecutor[T any](steps []Step[T], opts ...ExecutorOption) *Executor[T]
NewExecutor creates an Executor for the given steps.
func (*Executor[T]) Execute ¶
func (e *Executor[T]) Execute(ctx context.Context, input T, onProgress func(StepProgress[T])) (T, error)
Execute runs all steps sequentially, calling onProgress for each step's lifecycle events. Returns the final output or the first error encountered. Supports context cancellation between steps.
type ExecutorOption ¶
type ExecutorOption func(*executorConfig)
ExecutorOption configures an Executor.
type Iterator ¶
type Iterator[T any] interface { // Next returns the next value. Returns (zero, false, nil) when exhausted. Next(ctx context.Context) (T, bool, error) // Close releases any resources held by the iterator. Close() error }
Iterator provides pull-based sequential access to a stream of values. Structurally compatible with provider.Iterator[T].
type Pipeline ¶
type Pipeline[T any] struct { // contains filtered or unexported fields }
Pipeline represents a lazy, pull-based data pipeline. No work happens until values are pulled via Collect, Drain, or ForEach.
func Batch ¶
Batch collects up to size values or waits timeout (whichever comes first), then emits them as a slice.
size=0 means collect until timeout. timeout=0 means collect until size. Both zero is invalid and defaults to size=1.
Named Batch (not Buffer) because pipeline.Buffer already exists for channel-based decoupling between stages.
func Buffer ¶
Buffer adds a buffered channel between pipeline stages. This decouples the production rate from the consumption rate.
func Concat ¶
Concat joins multiple pipelines sequentially. All values from the first pipeline are yielded before the second, etc.
func Debounce ¶
Debounce waits for silence of the given duration after the last value before emitting. If a new value arrives during the quiet period, the timer resets and only the latest value is emitted.
Useful for "wait until input stops" patterns (e.g., search-as-you-type, batching rapid events).
func Distinct ¶
func Distinct[T comparable](p *Pipeline[T]) *Pipeline[T]
Distinct removes duplicate comparable values while preserving first-seen order.
func FanOut ¶
FanOut applies multiple functions to each input value in parallel and collects all results as a slice.
func Filter ¶
Filter keeps only values that satisfy the predicate.
Example ¶
package main
import (
"context"
"fmt"
"strings"
"github.com/kbukum/gokit/stream"
)
func main() {
p := stream.FromSlice([]string{"alpha", "", "beta", ""})
nonEmpty := stream.Filter(p, func(s string) bool { return s != "" })
got, _ := stream.Collect(context.Background(), nonEmpty)
fmt.Println(strings.Join(got, ","))
}
Output: alpha,beta
func FlatMap ¶
func FlatMap[I, O any](p *Pipeline[I], fn func(context.Context, I) (Iterator[O], error)) *Pipeline[O]
FlatMap transforms each value into an iterator and flattens the results.
func FromSlice ¶
FromSlice creates a pipeline from a slice of values.
Example ¶
package main
import (
"context"
"fmt"
"github.com/kbukum/gokit/stream"
)
func main() {
p := stream.FromSlice([]int{1, 2, 3, 4})
doubled := stream.Map(p, func(_ context.Context, x int) (int, error) {
return x * 2, nil
})
got, err := stream.Collect(context.Background(), doubled)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(got)
}
Output: [2 4 6 8]
func Merge ¶
Merge combines multiple pipelines concurrently. Values are yielded as they become available from any source. Order is NOT preserved.
func Parallel ¶
Parallel applies fn to each value concurrently with up to n workers. Order is NOT preserved. Use Map for ordered processing.
func Reduce ¶
Reduce accumulates all values into a single result. The pipeline yields exactly one value: the final accumulator.
func SlidingWindow ¶
func SlidingWindow[T any](p *Pipeline[T], timeFn func(T) time.Time, windowSize, slideBy time.Duration) *Pipeline[[]T]
SlidingWindow emits overlapping windows based on a time extraction function. windowSize is the duration of each window. slideBy is how far each window advances.
Values must arrive in time order. Each emitted slice contains all values whose timestamp falls within [windowStart, windowStart+windowSize).
func Tap ¶
Tap calls fn as a side-effect for each value, then passes the value through unchanged. Use for logging, metrics, or mid-pipeline publishing.
func TapEach ¶
TapEach applies fn[i] to each element of a []T slice as a side-effect, then passes the slice through unchanged. Useful after FanOut.
func Throttle ¶
Throttle drops values that arrive faster than the given interval. Only the first value in each interval window is emitted; subsequent values within the same window are dropped. Useful for rate-limiting downstream processing.
func TumblingWindow ¶
TumblingWindow groups values into non-overlapping fixed-duration windows. Each window is emitted as a slice when its duration expires. The final partial window is emitted when the source is exhausted.
type Runnable ¶
type Runnable struct {
// contains filtered or unexported fields
}
Runnable is a fully-configured pipeline ready to execute.
type Step ¶
type Step[T any] struct { ID string Name string Execute func(ctx context.Context, input T) (T, error) Skip func(ctx context.Context, input T) bool // optional: skip if true }
Step represents a named unit of work in an executable pipeline.
type StepProgress ¶
type StepProgress[T any] struct { StepID string `json:"step_id"` Name string `json:"name"` Status StepStatus `json:"status"` Result *StepResult[T] `json:"result,omitempty"` }
StepProgress reports progress for a single step in pipeline execution.
type StepResult ¶
type StepResult[T any] struct { StepID string `json:"step_id"` Output T `json:"output,omitempty"` Err error `json:"error,omitempty"` Elapsed time.Duration `json:"elapsed"` }
StepResult captures the outcome of a step execution.
type StepStatus ¶
type StepStatus string
StepStatus describes the status of a step during execution.
const ( StepStarted StepStatus = "started" StepCompleted StepStatus = "completed" StepFailed StepStatus = "failed" StepSkipped StepStatus = "skipped" )