Documentation
¶
Overview ¶
Package stream provides a declarative reactive pipeline for go-codex, bridging push-based transport adapters (MQTT, ZeroMQ) with governed forge.Function computations over typed Go channels.
Reactive programming paradigm ¶
Every operator in this package follows the same model:
- Source: From, FromCodec (accepts any format.Format)
- Transform: Apply (forge.Function per-item), Filter, Tap, MapErr, Retry, FlatMapSlice
- Fan-in/out: Merge, Tee, CombineLatest2, CombineLatest3, CombineLatest4, Zip
- Time: Buffer (count/timeout), Window (fixed ticker), SlidingWindow, Debounce, Throttle
- Sink: Drain (safe — drains both channels), Collect
Pipelines are composed by passing Stream values between free functions:
sensors := stream.FromCodec(ctx, rawCh, format.JSON(sensorCodec), stream.SourceOptions{Observer: obs})
oeeData := stream.Apply(ctx, sensors, oeeCalcFn, stream.ApplyOptions{Observer: obs})
oeeData = stream.Tap(ctx, oeeData, func(oee OEE) { dashboard.Publish(oee) })
alerts := stream.Filter(ctx, oeeData, func(o OEE) bool { return float64(o) < 0.65 })
stream.Drain(ctx, alerts, publishAlert, logError, stream.DrainOptions{Observer: obs})
Explicit error channels ¶
Every Stream has two channels: Values for successful items and Errors for per-item errors. The stream continues after each error — a single bad sensor reading does not terminate a continuous monitoring pipeline.
Consumers MUST drain both channels concurrently to avoid goroutine leaks. Drain is the safe default sink: it handles both channels in a single select loop.
Use MapErr to recover from errors or reclassify them before the final sink.
Forge integration ¶
Apply bridges forge.Function (governed synchronous computation) into a streaming pipeline. Every item is validated by the function's input codec, processed, and validated by the output codec — the same governance that applies to batch computations also applies per-item in the stream.
Forge functions can be composed before being used in a stream:
composed := forge.Compose("c2k", "1.0.0", celsius2centi, centi2kelvin)
kelvinStream := stream.Apply(ctx, celsiusStream, composed, opts)
Two observer kinds ¶
Infrastructure metrics — how many items, latency, error counts:
opts := stream.ApplyOptions{Observer: obs} // obs implements stats.StreamObserver
Domain event observation — typed business values flowing through the pipeline:
oeeStream = stream.Tap(ctx, oeeStream, func(oee OEE) {
slog.Info("OEE computed", "value", float64(oee))
})
Both are orthogonal and composable. A pipeline can use both simultaneously.
Observer interfaces ¶
stats.StreamObserver.RecordStreamItem fires inside Apply for every item. stats.PipelineObserver.RecordApply fires separately inside forge for every item. Both fire independently — compose them via stats.NewFanout.
No external dependencies ¶
This package imports only codex, forge, format, stats, context, time, and sync — all already in the module. No RxGo or any other reactive library is needed.
Index ¶
- func Collect[T any](ctx context.Context, src Stream[T]) (values []T, errs []error)
- func Drain[T any](ctx context.Context, src Stream[T], onValue func(context.Context, T) error, ...)
- func Tee[T any](ctx context.Context, src Stream[T]) (Stream[T], Stream[T])
- type ApplyOptions
- type BroadcastHub
- type DrainOptions
- type SourceOptions
- type StepKind
- type Stream
- func Apply[In, Out any](ctx context.Context, src Stream[In], fn *forge.Function[In, Out], ...) Stream[Out]
- func Buffer[T any](ctx context.Context, src Stream[T], n int, maxWait time.Duration) Stream[[]T]
- func CombineLatest2[A, B, Out any](ctx context.Context, a Stream[A], b Stream[B], combine func(A, B) Out) Stream[Out]
- func CombineLatest3[A, B, C, Out any](ctx context.Context, a Stream[A], b Stream[B], c Stream[C], ...) Stream[Out]
- func CombineLatest4[A, B, C, D, Out any](ctx context.Context, a Stream[A], b Stream[B], c Stream[C], d Stream[D], ...) Stream[Out]
- func Debounce[T any](ctx context.Context, src Stream[T], d time.Duration) Stream[T]
- func Filter[T any](ctx context.Context, src Stream[T], pred func(T) bool) Stream[T]
- func FlatMapSlice[In, Out any](ctx context.Context, src Stream[In], fn func(In) []Out) Stream[Out]
- func From[T any](ctx context.Context, src <-chan T) Stream[T]
- func FromCodec[T any](ctx context.Context, src <-chan []byte, fmt format.Format[T], ...) Stream[T]
- func MapErr[T any](ctx context.Context, src Stream[T], fn func(error) (T, bool, error)) Stream[T]
- func Merge[T any](ctx context.Context, srcs ...Stream[T]) Stream[T]
- func Retry[T any](ctx context.Context, src Stream[T], retry func(error) (T, bool, error)) Stream[T]
- func Single[T any](ctx context.Context, v T) Stream[T]
- func SlidingWindow[T any](ctx context.Context, src Stream[T], size, step int) Stream[[]T]
- func Tap[T any](ctx context.Context, src Stream[T], onValue func(T)) Stream[T]
- func Throttle[T any](ctx context.Context, src Stream[T], interval time.Duration) Stream[T]
- func Window[T any](ctx context.Context, src Stream[T], duration time.Duration) Stream[[]T]
- func Zip[A, B, Out any](ctx context.Context, a Stream[A], b Stream[B], combine func(A, B) Out) Stream[Out]
- type StreamApplyError
- type StreamDecodeError
- type Topology
- func (t *Topology) Spec() TopologySpec
- func (t *Topology) WithBuffer(description string) *Topology
- func (t *Topology) WithCombineLatest(description string) *Topology
- func (t *Topology) WithDebounce(description string) *Topology
- func (t *Topology) WithDescription(desc string) *Topology
- func (t *Topology) WithFilter(description string) *Topology
- func (t *Topology) WithFlatMapSlice(description string) *Topology
- func (t *Topology) WithMerge(description string) *Topology
- func (t *Topology) WithSink(name, description string) *Topology
- func (t *Topology) WithSlidingWindow(description string) *Topology
- func (t *Topology) WithSource(name, description string) *Topology
- func (t *Topology) WithTap(description string) *Topology
- func (t *Topology) WithTee(description string) *Topology
- func (t *Topology) WithThrottle(description string) *Topology
- func (t *Topology) WithWindow(description string) *Topology
- func (t *Topology) WithZip(description string) *Topology
- type TopologyInfo
- type TopologySpec
- type TopologyStep
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Collect ¶
Collect accumulates all values and errors from src until it terminates or ctx is cancelled, then returns two slices.
Collect is primarily intended for testing and bounded streams. For long-running or infinite streams use Drain instead.
func Drain ¶
func Drain[T any]( ctx context.Context, src Stream[T], onValue func(context.Context, T) error, onError func(error), opts DrainOptions, )
Drain consumes src until it terminates or ctx is cancelled. onValue is called for each successful item in Stream.Values. onError is called for each error in Stream.Errors AND for errors returned by onValue.
Drain ALWAYS drains both channels concurrently using a single select loop — it is the safe default sink that prevents goroutine leaks. Use Drain whenever you want to consume a stream without building your own select loop.
onError may be nil; in that case errors are silently discarded.
Example ¶
package main
import (
"context"
"fmt"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx := context.Background()
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
var sum int
stream.Drain(ctx, stream.From(ctx, ch),
func(_ context.Context, v int) error {
sum += v
return nil
},
nil,
stream.DrainOptions{},
)
fmt.Println("sum:", sum)
}
Output: sum: 6
func Tee ¶
Tee splits src into two independent copies. Both copies receive all items and errors. Backpressure on either copy blocks the other — use buffered channels (via SourceOptions.Buffer on the original source) or Drain if the two consumers run at different speeds.
Use Tee when you need the same stream for two independent purposes (e.g. storing to a database while also computing KPIs):
store, compute := stream.Tee(ctx, sensorStream) go stream.Drain(ctx, store, saveToDB, logErr, opts) oeeStream := stream.Apply(ctx, compute, oeeCalcFn, opts)
Types ¶
type ApplyOptions ¶
type ApplyOptions struct {
// Observer receives [stats.StreamObserver.RecordStreamItem] for every item
// processed by Apply (success or failure).
// [stats.PipelineObserver.RecordApply] fires separately inside
// [forge.Function.Apply] — both observers fire independently.
// Defaults to [stats.NoopObserver] when nil.
Observer stats.Observer
// Buffer is the output Values and Errors channel buffer size. Default 0.
Buffer int
}
ApplyOptions configures Apply.
type BroadcastHub ¶
type BroadcastHub[T any] struct { // contains filtered or unexported fields }
BroadcastHub fans out a single Stream source to N independent subscribers. Each subscriber receives its own Stream with a private buffered channel. Slow subscribers apply backpressure only to themselves — items are dropped for a full subscriber buffer rather than blocking the hub goroutine.
Create a hub and subscribe before the source begins emitting:
hub := stream.NewBroadcastHub(ctx, oeeStream, 32) sub1 := hub.Subscribe() sub2 := hub.Subscribe()
Unsubscribe when a subscriber is no longer needed:
hub.Unsubscribe(sub1)
The hub runs until ctx is cancelled or the source stream terminates. All subscriber channels are closed when the hub exits.
Example ¶
package main
import (
"context"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
hub := stream.NewBroadcastHub(ctx, stream.From(ctx, ch), 8)
sub1 := hub.Subscribe()
sub2 := hub.Subscribe()
done1 := make(chan []int, 1)
done2 := make(chan []int, 1)
go func() { v, _ := stream.Collect(ctx, sub1); done1 <- v }()
go func() { v, _ := stream.Collect(ctx, sub2); done2 <- v }()
<-done1
<-done2
}
Output:
func NewBroadcastHub ¶
func NewBroadcastHub[T any](ctx context.Context, src Stream[T], bufPerSubscriber int) *BroadcastHub[T]
NewBroadcastHub creates a BroadcastHub that reads from src and fans out every value and error to all current subscribers.
bufPerSubscriber is the buffer size for each subscriber's Values and Errors channels. A buffer of 0 is unbuffered — the hub blocks until the subscriber reads each item. Use a positive buffer to prevent slow subscribers from stalling each other.
The hub goroutine terminates when ctx is cancelled or src closes. All subscriber channels are then closed so downstream consumers drain cleanly.
func (*BroadcastHub[T]) Subscribe ¶
func (h *BroadcastHub[T]) Subscribe() Stream[T]
Subscribe adds a new subscriber and returns its Stream. The returned stream's channels are buffered with the hub's configured bufPerSubscriber size. The channels are closed when the hub exits. Subscribe is safe to call concurrently.
func (*BroadcastHub[T]) Unsubscribe ¶
func (h *BroadcastHub[T]) Unsubscribe(s Stream[T])
Unsubscribe removes a subscriber. The subscriber's channels are not closed by Unsubscribe — they close when the hub exits. After Unsubscribe the hub stops sending to those channels.
type DrainOptions ¶
type DrainOptions struct {
// Observer receives [stats.Observer.RecordValidationError] for errors returned
// by onValue. Defaults to [stats.NoopObserver] when nil.
Observer stats.Observer
}
DrainOptions configures Drain.
type SourceOptions ¶
type SourceOptions struct {
// Name identifies this source in [StreamDecodeError] for structured logging.
// Defaults to "stream" when empty.
Name string
// Observer receives [stats.Observer.RecordValidationError] for per-field decode
// failures. Defaults to [stats.NoopObserver] when nil.
Observer stats.Observer
// Buffer is the Values and Errors channel buffer size. Default 0 (unbuffered).
Buffer int
}
SourceOptions configures FromCodec.
type StepKind ¶
type StepKind string
StepKind identifies the type of a pipeline step.
const ( // StepKindSource is a data source (MQTT channel, Go channel, file, etc.). StepKindSource StepKind = "source" // StepKindApply is a forge.Function applied per-item. StepKindApply StepKind = "apply" // StepKindFilter retains only items matching a predicate. StepKindFilter StepKind = "filter" // StepKindTap observes items without transforming them. StepKindTap StepKind = "tap" // StepKindBuffer batches items by count or time window. StepKindBuffer StepKind = "buffer" // StepKindDebounce emits only after a silence window. StepKindDebounce StepKind = "debounce" // StepKindThrottle rate-limits item emission. StepKindThrottle StepKind = "throttle" // StepKindMerge fan-in from multiple sources. StepKindMerge StepKind = "merge" // StepKindTee splits a stream into two copies. StepKindTee StepKind = "tee" // StepKindWindow collects items into fixed-interval time windows. StepKindWindow StepKind = "window" // StepKindSlidingWindow collects items into overlapping count-based windows. StepKindSlidingWindow StepKind = "slidingWindow" // StepKindCombineLatest merges the latest values from multiple sources. StepKindCombineLatest StepKind = "combineLatest" // StepKindZip pairs items from two streams by position. StepKindZip StepKind = "zip" // StepKindFlatMapSlice expands each item into multiple output items. StepKindFlatMapSlice StepKind = "flatMapSlice" // StepKindSink consumes items and errors. StepKindSink StepKind = "sink" )
type Stream ¶
type Stream[T any] struct { // Values carries successfully processed items. Values <-chan T // Errors carries per-item errors. The stream continues after each error — // a failing item does not terminate the pipeline. Use [MapErr] to recover // from errors or reclassify them. Errors <-chan error }
Stream[T] is a typed reactive stream with explicit error separation.
Values carries successful items; Errors carries per-item errors. Both channels are closed when the stream terminates (source closed or context cancelled).
Consumers MUST drain both channels concurrently — reading only from Values while ignoring Errors will cause the goroutine writing to Errors to block, leaking resources. Use Drain as the safe default sink: it handles both channels in a single select loop.
The nil-channel pattern is used throughout the package to disable a channel once it is closed, preventing accidental reads from a closed channel in select statements. All operators follow this convention internally.
func Apply ¶
func Apply[In, Out any]( ctx context.Context, src Stream[In], fn *forge.Function[In, Out], opts ApplyOptions, ) Stream[Out]
Apply applies fn to every value in src using forge.Function.ApplyContext.
All forge validation — input codec Refine, optional WithRefinement, compute function, output codec Refine — runs per item. Successful outputs go to Stream.Values. Validation or compute failures are wrapped in StreamApplyError and sent to Stream.Errors. The stream continues after each error.
stats.PipelineObserver.RecordApply fires inside forge for every item. If opts.Observer also implements stats.StreamObserver, RecordStreamItem fires for every item with the forge function name, success flag, and duration.
The forge function's own observer (set via forge.Function.Register) fires independently — both observers can be active simultaneously.
Example ¶
package main
import (
"context"
"fmt"
"github.com/DaniDeer/go-codex/codex"
"github.com/DaniDeer/go-codex/forge"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx := context.Background()
double := forge.NewFunction("double", "1.0.0",
codex.Float64().WithTitle("input"),
codex.Float64().WithTitle("doubled"),
func(v float64) (float64, error) { return v * 2, nil },
)
ch := make(chan float64, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
s := stream.Apply(ctx, stream.From(ctx, ch), double, stream.ApplyOptions{})
vals, _ := stream.Collect(ctx, s)
for _, v := range vals {
fmt.Printf("%.0f\n", v)
}
}
Output: 2 4 6
func Buffer ¶
Buffer collects up to n value items (or until maxWait elapses since the last emission) and emits them as a batch []T. Errors from src are forwarded to the output Stream.Errors immediately, without buffering.
Use Buffer to integrate item-at-a-time streams with forge.Map (which takes []T):
batchStream := stream.Buffer(ctx, sensorStream, 10, 500*time.Millisecond) batchOEE := stream.Apply(ctx, batchStream, batchOEECalc, opts)
func CombineLatest2 ¶
func CombineLatest2[A, B, Out any]( ctx context.Context, a Stream[A], b Stream[B], combine func(A, B) Out, ) Stream[Out]
CombineLatest2 merges the latest value from two independent streams using combine. A new combined value is emitted whenever either source emits a new value. The output stream blocks until both sources have emitted at least one value. Errors from either source are forwarded to the output Stream.Errors.
Use CombineLatest2 to feed a forge.Function that takes a 2-field input struct, where the two fields arrive on independent streams:
oeeInputs := stream.CombineLatest2(ctx, availStream, perfStream,
func(a Availability, p Performance) OEEIn { return OEEIn{a, p} })
oeeStream := stream.Apply(ctx, oeeInputs, oeeCalcFn, opts)
func CombineLatest3 ¶
func CombineLatest3[A, B, C, Out any]( ctx context.Context, a Stream[A], b Stream[B], c Stream[C], combine func(A, B, C) Out, ) Stream[Out]
CombineLatest3 merges the latest value from three independent streams using combine. A new combined value is emitted whenever any source emits a new value, after all three sources have emitted at least one value. Errors from any source are forwarded to the output Stream.Errors.
Use CombineLatest3 to feed a forge function with a 3-field input struct where each field arrives on a separate stream (e.g. OEE = Availability × Performance × Quality):
oeeInputs := stream.CombineLatest3(ctx, availStream, perfStream, qualStream,
func(a Availability, p Performance, q Quality) OEEIn {
return OEEIn{Availability: a, Performance: p, Quality: q}
})
oeeStream := stream.Apply(ctx, oeeInputs, oeeCalcFn, opts)
func CombineLatest4 ¶
func CombineLatest4[A, B, C, D, Out any]( ctx context.Context, a Stream[A], b Stream[B], c Stream[C], d Stream[D], combine func(A, B, C, D) Out, ) Stream[Out]
CombineLatest4 merges the latest value from four independent streams. Same semantics as CombineLatest3 extended to four sources.
func Debounce ¶
Debounce emits a value only when src.Values is silent for at least d. Intermediate values during the silence window are dropped; only the last value before the silence elapses is emitted. Error items are forwarded to Stream.Errors immediately.
Use Debounce when only the final value of a burst matters — for example, sensor readings that settle after a transient spike.
func Filter ¶
Filter keeps value items where pred returns true. Value items for which pred returns false are dropped silently. Error items are forwarded to the output Stream.Errors unchanged.
func FlatMapSlice ¶
FlatMapSlice maps each value item to a []Out slice and emits each element of the slice individually to the output Stream.Values. An empty slice from fn produces no output items (filter-like behaviour for that item). Errors from src pass through to Stream.Errors unchanged.
Use FlatMapSlice when one incoming item should expand into multiple outgoing items — for example, one batch record expanding into N individual readings, or one sensor event triggering multiple derived measurements.
Unlike a hypothetical FlatMap[In, Stream[Out]], FlatMapSlice requires no goroutine pool: fn is called synchronously per item.
func From ¶
From wraps a typed channel as a Stream. Each value received from src becomes a value item. When src is closed or ctx is cancelled, both Stream channels are closed.
The returned Stream.Errors channel is never written — it closes when the stream terminates. This is intentional: From is a type-safe source with no error path. Use FromCodec when decode failures must be captured.
intCh := make(chan int, 3)
intCh <- 1; intCh <- 2; intCh <- 3; close(intCh)
s := stream.From(ctx, intCh)
stream.Drain(ctx, s, func(_ context.Context, v int) error {
fmt.Println(v)
return nil
}, nil, stream.DrainOptions{})
Example ¶
package main
import (
"context"
"fmt"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx := context.Background()
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
s := stream.From(ctx, ch)
vals, _ := stream.Collect(ctx, s)
for _, v := range vals {
fmt.Println(v)
}
}
Output: 10 20 30
func FromCodec ¶
func FromCodec[T any](ctx context.Context, src <-chan []byte, fmt format.Format[T], opts SourceOptions) Stream[T]
FromCodec decodes raw []byte payloads from src using the given format. Successful decodes go to Stream.Values. Decode or validation failures go to Stream.Errors as StreamDecodeError.
Pass any format.Format value — JSON, YAML, TOML, or a custom format:
sensors := stream.FromCodec(ctx, rawCh, format.JSON(sensorCodec),
stream.SourceOptions{Name: "mqtt/sensors/+", Observer: obs})
sensors := stream.FromCodec(ctx, rawCh, format.YAML(sensorCodec),
stream.SourceOptions{Name: "mqtt/sensors/+", Observer: obs})
Use with MQTT or ZeroMQ SubscribeHandlers that write raw payloads to a channel:
rawCh := make(chan []byte, 64)
mqttClient.Subscribe("sensors/+/data", 1,
adaptermqtt.SubscribeHandler(ctx, handle, func(_ context.Context, raw []byte) error {
select { case rawCh <- raw: default: }
return nil
}, adaptermqtt.SubscribeOptions{}))
sensors := stream.FromCodec(ctx, rawCh, format.JSON(sensorCodec),
stream.SourceOptions{Name: "mqtt/sensors/+", Observer: obs})
Example ¶
package main
import (
"context"
"fmt"
"github.com/DaniDeer/go-codex/codex"
"github.com/DaniDeer/go-codex/format"
stream "github.com/DaniDeer/go-codex/stream"
"github.com/DaniDeer/go-codex/validate"
)
type reading struct {
Sensor string
Value float64
}
var readingCodec = codex.Struct(
codex.RequiredField("sensor",
codex.String().Refine(validate.NonEmptyString),
func(r reading) string { return r.Sensor },
func(r *reading, v string) { r.Sensor = v }),
codex.RequiredField("value",
codex.Float64(),
func(r reading) float64 { return r.Value },
func(r *reading, v float64) { r.Value = v }),
)
func main() {
ctx := context.Background()
rawCh := make(chan []byte, 2)
rawCh <- []byte(`{"sensor":"s1","value":23.5}`)
rawCh <- []byte(`{"sensor":"s2","value":87.3}`)
close(rawCh)
s := stream.FromCodec(ctx, rawCh, format.JSON(readingCodec),
stream.SourceOptions{Name: "example"})
vals, errs := stream.Collect(ctx, s)
fmt.Println(len(vals), "values,", len(errs), "errors")
}
Output: 2 values, 0 errors
func MapErr ¶
MapErr transforms errors in Stream.Errors, enabling error recovery or reclassification.
fn receives each error and returns one of:
- (value, true, nil) — recover: emit value to Stream.Values
- (zero, false, err) — reclassify: emit new error to Stream.Errors
- (zero, false, nil) — silence: drop the error entirely
Value items pass through to the output Stream.Values unchanged.
Use MapErr for dead-lettering, retry-after-transformation, or silencing expected transient errors:
recovered := stream.MapErr(ctx, src, func(err error) (T, bool, error) {
var sde stream.StreamDecodeError
if errors.As(err, &sde) && isTransient(sde.Err) {
return zero, false, nil // silence transient decode errors
}
return zero, false, err // re-emit all other errors
})
func Merge ¶
Merge combines multiple streams into one. Items and errors from all source streams are forwarded as they arrive. The output stream terminates when all source streams have terminated.
Use Merge to combine readings from multiple sensors or topics into a single processing pipeline.
func Retry ¶
Retry transforms error items, enabling recovery or reclassification with caller-controlled retry logic. Successful value items pass through unchanged.
retry receives each error and returns one of:
- (value, true, nil) — recover: emit value to Stream.Values
- (zero, false, err) — reclassify: emit new error to Stream.Errors
- (zero, false, nil) — silence: drop the error entirely
The caller's retry function controls timing and backoff. For exponential backoff:
retried := stream.Retry(ctx, src, func(err error) (T, bool, error) {
var sde stream.StreamDecodeError
if errors.As(err, &sde) {
time.Sleep(100 * time.Millisecond) // simple backoff
return zero, false, err // reclassify for a retry queue
}
return zero, false, nil // silence unrecoverable errors
})
Retry is a specialisation of MapErr for the common case where the retry function needs a concise name. Callers who need the full (value,isValue,err) tuple directly should use MapErr.
func Single ¶
Single wraps a single value as a Stream that emits v once, then terminates. The Stream.Errors channel is never written.
Use Single to start a per-request pipeline inside a [PipelineHandlerFunc] or [AsPipelineFunc], or any time you need a bounded one-shot stream source:
s := stream.Single(ctx, req)
out := stream.Apply(ctx, s, computeFn, stream.ApplyOptions{Observer: obs})
out = stream.Tap(ctx, out, func(v Out) { slog.Info("computed", "v", v) })
Example ¶
package main
import (
"context"
"fmt"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx := context.Background()
s := stream.Single(ctx, 7)
vals, _ := stream.Collect(ctx, s)
fmt.Println(vals[0])
}
Output: 7
func SlidingWindow ¶
SlidingWindow emits a []T slice every step items, containing the last size items. The window slides forward by step items on each emission. When step == size the windows are non-overlapping (tumbling). Requires step > 0 and size >= step.
Errors from src are forwarded to Stream.Errors immediately without affecting the sliding window position.
func Tap ¶
Tap inserts a domain event observer on the value channel without transforming items. onValue is called for each successful value; the value is then forwarded unchanged. Error items are forwarded to the output Stream.Errors unchanged.
Use Tap for domain-level observation — auditing, triggering side effects, logging application-level events — independently from infrastructure metrics:
oeeStream = stream.Tap(ctx, oeeStream, func(oee OEE) {
slog.Info("OEE computed", "value", float64(oee))
businessDashboard.Publish(oee)
})
Example ¶
package main
import (
"context"
"fmt"
stream "github.com/DaniDeer/go-codex/stream"
)
func main() {
ctx := context.Background()
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
var observed []int
s := stream.Tap(ctx, stream.From(ctx, ch), func(v int) {
observed = append(observed, v)
})
stream.Collect(ctx, s)
fmt.Println(len(observed), "items observed")
}
Output: 3 items observed
func Throttle ¶
Throttle emits at most one value per interval, dropping intermediates. The first value in an interval is emitted; subsequent values arriving before the interval elapses are dropped. Error items are forwarded to Stream.Errors immediately.
Use Throttle to rate-limit high-frequency sources while ensuring at least one value is emitted per interval.
func Window ¶
Window emits all values collected during each fixed-duration time window as a []T slice. An empty slice is emitted when no items arrived during a window. Errors from src are forwarded immediately.
Unlike Buffer, Window always emits at fixed calendar-aligned intervals using time.NewTicker — the emission clock never resets when items arrive. This gives consistent time boundaries (e.g. "all readings in the past 1 minute, every minute") suitable for time-series analytics.
func Zip ¶
func Zip[A, B, Out any]( ctx context.Context, a Stream[A], b Stream[B], combine func(A, B) Out, ) Stream[Out]
Zip pairs items from two streams by position: (a[0],b[0]), (a[1],b[1]), ... A combined item is emitted when both sources have emitted their n-th value. If one source emits faster, the faster source's items are buffered internally. Errors from either source are forwarded to the output Stream.Errors immediately.
Unlike CombineLatest2, which emits on every update using the latest values, Zip waits for matched pairs in order.
type StreamApplyError ¶
type StreamApplyError struct {
// Function is the forge function name (from [forge.FunctionSpec.Name]).
Function string
// Err is the inner forge error. Use errors.As to reach
// forge.InputError, forge.OutputError, or forge.ApplyError.
Err error
}
StreamApplyError is sent to Stream.Errors by Apply when forge.Function.Apply fails. The inner Err is always a typed forge error (forge.InputError, forge.OutputError, forge.ApplyError, etc.) and is reachable via errors.As.
StreamApplyError implements slog.LogValuer for structured logging:
slog.Warn("apply failed", "error", sae)
// → {function:"oeeCalc", err:{...}}
func (StreamApplyError) Error ¶
func (e StreamApplyError) Error() string
func (StreamApplyError) LogValue ¶
func (e StreamApplyError) LogValue() slog.Value
LogValue implements slog.LogValuer for structured logging.
type StreamDecodeError ¶
type StreamDecodeError struct {
// Source identifies the stream source (from [SourceOptions.Name]).
Source string
// Err is the underlying codec error (e.g. [codex.ValidationErrors]).
Err error
}
StreamDecodeError is sent to Stream.Errors by FromCodec when a raw payload fails codec decode or Refine constraints.
StreamDecodeError implements slog.LogValuer for structured logging:
slog.Warn("decode failed", "error", sde)
// → {source:"mqtt/sensors/+", err:{...}}
func (StreamDecodeError) Error ¶
func (e StreamDecodeError) Error() string
func (StreamDecodeError) LogValue ¶
func (e StreamDecodeError) LogValue() slog.Value
LogValue implements slog.LogValuer for structured logging.
type Topology ¶
type Topology struct {
// contains filtered or unexported fields
}
Topology is a declarative builder for a TopologySpec. Chain builder methods to describe the pipeline, then call Topology.Spec to produce the machine-readable spec.
Usage mirrors forge.Registry:
topo := stream.NewTopology("Sensor OEE Pipeline", "1.0.0").
WithDescription("Real-time OEE from MQTT sensor readings.").
WithSource("mqtt/sensors/+/data", "Decoded sensor readings").
WithFilter("oee < 0.65").
WithSink("mqtt/alerts/oee", "Low-OEE alerts")
stream.WithApply(topo, oeeCalcFn) // free function — Go generics cannot add type params to methods
yaml, _ := streamrender.Render(topo.Spec())
func NewTopology ¶
NewTopology returns a new Topology with the given title and version.
Example ¶
package main
import (
"github.com/DaniDeer/go-codex/codex"
"github.com/DaniDeer/go-codex/forge"
stream "github.com/DaniDeer/go-codex/stream"
)
var topoFn = forge.NewFunction("oeeCalc", "1.0.0",
codex.Float64().WithTitle("oee"),
codex.Float64().WithTitle("grade"),
func(v float64) (float64, error) { return v * 100, nil },
)
func main() {
topo := stream.NewTopology("Sensor Pipeline", "1.0.0").
WithDescription("Real-time sensor processing pipeline.").
WithSource("mqtt/sensors/+/data", "Raw sensor readings from MQTT").
WithFilter("value > 0").
WithTap("dashboard observer").
WithSink("mqtt/alerts", "OEE alert publisher")
stream.WithApply(topo, topoFn)
spec := topo.Spec()
_ = spec // pass spec to render/stream.Render to get YAML
}
Output:
func WithApply ¶
WithApply records an apply step from a forge.Function. The function's name, version, hash, and description are captured from its forge.FunctionSpec.
func (*Topology) Spec ¶
func (t *Topology) Spec() TopologySpec
Spec returns the accumulated TopologySpec.
func (*Topology) WithBuffer ¶
WithBuffer records a buffer (windowing) step.
func (*Topology) WithCombineLatest ¶
WithCombineLatest records a CombineLatest step (merges latest values from multiple sources).
func (*Topology) WithDebounce ¶
WithDebounce records a debounce step.
func (*Topology) WithDescription ¶
WithDescription sets the pipeline-level description and returns t for chaining.
func (*Topology) WithFilter ¶
WithFilter records a filter step with a human-readable description of the predicate.
func (*Topology) WithFlatMapSlice ¶
WithFlatMapSlice records a FlatMapSlice step (expands each item into multiple items).
func (*Topology) WithMerge ¶
WithMerge records a merge (fan-in) step combining multiple source streams.
func (*Topology) WithSlidingWindow ¶
WithSlidingWindow records a sliding window step (overlapping count-based windows).
func (*Topology) WithSource ¶
WithSource records a source step (e.g. an MQTT topic, a file path, a typed channel).
func (*Topology) WithTee ¶
WithTee records a tee (fan-out) step splitting one stream into two copies.
func (*Topology) WithThrottle ¶
WithThrottle records a throttle step.
func (*Topology) WithWindow ¶
WithWindow records a window step (fixed-interval tumbling time windows).
type TopologyInfo ¶
TopologyInfo is pipeline-level metadata for a stream topology.
type TopologySpec ¶
type TopologySpec struct {
Info TopologyInfo
Steps []TopologyStep
}
TopologySpec is the full machine-readable description of a stream pipeline. Use render/stream.Render to serialise it as YAML.
type TopologyStep ¶
type TopologyStep struct {
// Kind identifies the operator type.
Kind StepKind
// Name is a human-readable label for this step.
// For source/sink steps this is typically the channel address or topic.
// For apply steps it is the forge function name.
Name string
// Description is an optional longer human-readable description.
Description string
// Function carries governance metadata when Kind == StepKindApply.
// Nil for all other step kinds.
Function *forge.FunctionSpec
}
TopologyStep describes one step in a stream pipeline.