Extending Streams

Build your own operators, bridge to Kotlin Flow, and understand the platform matrix.

Custom operators

Build operators by extending StreamMapper (transform) or StreamWrapper (pass-through):

import io.peekandpoke.ultra.streams.Stream
import io.peekandpoke.ultra.streams.StreamMapper

fun Stream<Int>.clamp(min: Int, max: Int): Stream<Int> =
    StreamMapper(
        wrapped = this,
        mapper = { it.coerceIn(min, max) }
    )

val source = StreamSource(50)
val clamped = source.clamp(0, 100)

source(150)
println(clamped())  // 100

For operators with internal state, extend StreamWrapperBase directly and override handleIncoming() and invoke().

Flow interop

Convert any stream to a Kotlin Flow:

import io.peekandpoke.ultra.streams.ops.asFlow

val source = StreamSource(1)

source.asFlow()
    .filter { it > 5 }
    .take(3)
    .collect { println(it) }

The Flow emits the current value immediately on collection, then subsequent values. Cancelling the collector unsubscribes automatically.

We didn't try to replace Flow. Sometimes you need the full coroutines ecosystem. This bridge lets you get there without rewriting everything.

Platform summary

Operator Platform
map, filter, combine, fold, distinct, fallback, history, indexed, onEach, cutoff All platforms
ticker, debounce, mapAsync, asFlow All platforms (coroutine-based)
animTicker() JS only (requestAnimationFrame)
persistInLocalStorage() JS only (localStorage)
debouncedFunc(), debouncedFuncExceptFirst() JS only (setTimeout)