├── LICENSE ├── README.md ├── all.go ├── all_test.go ├── append.go ├── asobservable.go ├── assign.go ├── autoconnect.go ├── autounsubscribe.go ├── buffercount.go ├── catch.go ├── catcherror.go ├── combineall.go ├── combinelatest.go ├── concat.go ├── concatall.go ├── concatmap.go ├── concatwith.go ├── connectable.go ├── connector.go ├── constraints.go ├── count.go ├── create.go ├── creator.go ├── defer.go ├── delay.go ├── distinctuntilchanged.go ├── do.go ├── doc.go ├── elementat.go ├── empty.go ├── endwith.go ├── equal.go ├── err.go ├── example_test.go ├── exhaustall.go ├── exhaustmap.go ├── filter.go ├── first.go ├── fprint.go ├── fprintf.go ├── fprintln.go ├── from.go ├── go.go ├── go.mod ├── go.sum ├── interval.go ├── iter_test.go ├── last.go ├── map.go ├── mape.go ├── marshal.go ├── maxbuffersize.go ├── merge.go ├── mergeall.go ├── mergemap.go ├── mergewith.go ├── multicast.go ├── must.go ├── never.go ├── observable.go ├── observer.go ├── of.go ├── oncomplete.go ├── ondone.go ├── onerror.go ├── onnext.go ├── passthrough.go ├── pipe.go ├── print.go ├── printf.go ├── println.go ├── publish.go ├── pull.go ├── pull_test.go ├── race.go ├── racewith.go ├── recv.go ├── reduce.go ├── reducee.go ├── refcount.go ├── repeat.go ├── retry.go ├── retrytime.go ├── sampletime.go ├── scan.go ├── scane.go ├── scheduler.go ├── send.go ├── share.go ├── skip.go ├── slice.go ├── startwith.go ├── subject.go ├── subscribe.go ├── subscribeon.go ├── subscriber.go ├── subscription.go ├── subscription_test.go ├── switchall.go ├── switchmap.go ├── take.go ├── take_test.go ├── takewhile.go ├── tap.go ├── throw.go ├── ticker.go ├── timer.go ├── tuple.go ├── values.go ├── values_test.go ├── wait.go ├── withlatestfrom.go ├── withlatestfromall.go ├── zip.go ├── zipall.go └── zipall_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2025 René Post, ReactiveGo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rx 2 | 3 | import "github.com/reactivego/rx" 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/reactivego/rx.svg)](https://pkg.go.dev/github.com/reactivego/rx) 6 | 7 | Package `rx` provides _**R**eactive E**x**tensions_, a powerful API for asynchronous programming in Go, built around [observables](#observables) and [operators](#operators) to process streams of data seamlessly. 8 | 9 | ## Prerequisites 10 | 11 | You’ll need [*Go 1.23*](https://golang.org/dl/) or later, as the implementation depends on language support for generics and iterators. 12 | 13 | ## Observables 14 | 15 | In `rx`, an [**Observables**](http://reactivex.io/documentation/observable.html) represents a stream of data that can emit items over time, while an **Observer** subscribes to it to receive and react to those emissions. This reactive approach enables asynchronous and concurrent operations without blocking execution. Instead of waiting for values to become available, an observer passively listens and responds whenever the observable emits data, errors, or a completion signal. 16 | 17 | This page introduces the **reactive pattern**, explaining what **Observables** and **Observers** are and how subscriptions work. Other sections explore the powerful set of [**Operators**](https://reactivex.io/documentation/operators.html) that allow you to transform, combine, and control data streams efficiently. 18 | 19 | An Observable: 20 | 21 | - is a stream of events. 22 | - assumes zero to many values over time. 23 | - pushes values 24 | - can take any amount of time to complete (or may never) 25 | - is cancellable 26 | - is lazy (it doesn't do anything until you subscribe). 27 | 28 | Example 29 | ```go 30 | package main 31 | 32 | import "github.com/reactivego/x" 33 | 34 | func main() { 35 | x.From[any](1,"hi",2.3).Println().Wait() 36 | } 37 | ``` 38 | > Note the program creates a mixed type `any` observable from an int, string and a float64. 39 | 40 | Output 41 | ``` 42 | 1 43 | hi 44 | 2.3 45 | ``` 46 | Example 47 | ```go 48 | package main 49 | 50 | import "github.com/reactivego/rx" 51 | 52 | func main() { 53 | rx.From(1,2,3).Println().Wait() 54 | } 55 | ``` 56 | > Note the program uses inferred type `int` for the observable. 57 | 58 | Output 59 | ``` 60 | 1 61 | 2 62 | 3 63 | ``` 64 | 65 | Observables in `rx` offer several advantages over standard Go channels: 66 | 67 | ### Hot vs Cold Observables 68 | 69 | - **Hot Observables** emit values regardless of subscription status. Like a live broadcast, any values emitted when no subscribers are listening are permanently missed. Examples include system events, mouse movements, or real-time data feeds. 70 | 71 | - **Cold Observables** begin emission only when subscribed to, ensuring subscribers receive the complete data sequence from the beginning. Examples include file contents, database queries, or HTTP requests that are executed on-demand. 72 | 73 | ### Rich Lifecycle Management 74 | 75 | Observables offer comprehensive lifecycle handling. They can complete normally, terminate with errors, or continue indefinitely. Subscriptions provide fine-grained control, allowing subscribers to cancel at any point, preventing resource leaks and unwanted processing. 76 | 77 | ### Time-Varying Data Model 78 | 79 | Unlike traditional variables that represent static values, Observables elegantly model how values evolve over time. They represent the entire progression of a value's state changes, not just its current state, making them ideal for reactive programming paradigms. 80 | 81 | ### Native Concurrency Support 82 | 83 | Concurrency is built into the Observable paradigm. Each Observable conceptually operates as an independent process that asynchronously pushes values to subscribers. This approach naturally aligns with concurrent programming models while abstracting away much of the complexity typically associated with managing concurrent operations. 84 | 85 | ## Operators 86 | 87 | Operators form a language for expressing programs with Observables. They transform, filter, and combine one or more Observables into new Observables, allowing for powerful data stream processing. Each operator performs a specific function in the reactive pipeline, enabling you to compose complex asynchronous workflows through method chaining. 88 | 89 | ### Index 90 | 91 | [__All__](https://pkg.go.dev/github.com/reactivego/rx#Observable.All) converts an Observable stream into a Go 1.22+ iterator sequence that provides each emitted value paired with its sequential zero-based index 92 | 93 | [__All2__](https://pkg.go.dev/github.com/reactivego/rx#All2) 94 | converts an Observable of Tuple2 pairs into a Go 1.22+ iterator sequence that yields each tuple's components (First, Second) as separate values. 95 | 96 | [__Append__](https://pkg.go.dev/github.com/reactivego/rx#Append) 97 | creates a pipe that appends emitted values to a provided slice while forwarding them to the next observer, with a method variant available for chaining. 98 | 99 | [__AsObservable__](https://pkg.go.dev/github.com/reactivego/rx#AsObservable) 100 | provides type conversion between observables, allowing you to safely cast an Observable of one type to another, and to convert a typed Observable to an Observable of 'any' type (and vice versa). 101 | 102 | [__AsObserver__](https://pkg.go.dev/github.com/reactivego/rx#AsObserver) converts an Observer of type `any` to an Observer of a specific type T. 103 | 104 | [__Assign__](https://pkg.go.dev/github.com/reactivego/rx#Observable.Assign) stores each emitted value from an Observable into a provided pointer variable while passing all emissions through to the next observer, enabling value capture during stream processing. 105 | 106 | [__AutoConnect__](https://pkg.go.dev/github.com/reactivego/rx#Connectable.AutoConnect) makes a (Connectable) Multicaster behave like an ordinary Observable that automatically connects the mullticaster to its source when the specified number of observers have subscribed to it. 107 | 108 | [__AutoUnsubscribe__](https://pkg.go.dev/github.com/reactivego/rx#Observable.AutoUnsubscribe) 109 | 110 | [__BufferCount__](https://pkg.go.dev/github.com/reactivego/rx#BufferCount) 111 | 112 | [__Catch__](https://pkg.go.dev/github.com/reactivego/rx#Observable.Catch) recovers from an error notification by continuing the sequence without emitting the error but switching to the catch ObservableInt to provide items. 113 | 114 | [__CatchError__](https://pkg.go.dev/github.com/reactivego/rx#Observable.CatchError) catches errors on the Observable to be handled by returning a new Observable or throwing error. 115 | 116 | __CombineAll__ 117 | 118 | __CombineLatest__ combines multiple Observables into one by emitting an array containing the latest values from each source whenever any input Observable emits a value, with variants (__CombineLatest2__, __CombineLatest3__, __CombineLatest4__, __CombineLatest5__) that return strongly-typed tuples for 2-5 input Observables respectively. 119 | 120 | __Concat__ combines multiple Observables sequentially by emitting all values from the first Observable before proceeding to the next one, ensuring emissions never overlap. 121 | 122 | __ConcatAll__ transforms a higher-order Observable (an Observable that emits other Observables) into a first-order Observable by subscribing to each inner Observable only after the previous one completes. 123 | 124 | __ConcatMap__ projects each source value to an Observable, subscribes to it, and emits its values, waiting for each one to complete before processing the next source value. 125 | 126 | __ConcatWith__ extends an Observable by appending additional Observables, ensuring that emissions from each Observable only begin after the previous one completes. 127 | 128 | __Connectable__ is an Observable with delayed connection to its source, combining both Observable and Connector interfaces. It separates the subscription process into two parts: observers can register via Subscribe, but the Observable won't subscribe to its source until Connect is explicitly called. This enables multiple observers to subscribe before any emissions begin (multicast behavior), allowing a single source Observable to be efficiently shared among multiple consumers. Besides inheriting all methods from Observable and Connector, Connectable provides the convenience methods __AutoConnect__ and __RefCount__ to manage connection behavior. 129 | 130 | __Connect__ establishes a connection to the source Observable and returns a Subscription that can be used to cancel the connection when no longer needed. 131 | 132 | __Connector__ provides a mechanism for controlling when a Connectable Observable subscribes to its source, allowing you to connect the Observable independently from when observers subscribe to it. This separation enables multiple subscribers to prepare their subscriptions before the source begins emitting items. It has a single method __Connect__. 133 | 134 | __Constraints__ type constraints __Signed__, __Unsigned__, __Integer__ and __Float__ copied verbatim from `golang.org/x/exp` so we could drop the dependency on that package. 135 | 136 | __Count__ returns an Observable that emits a single value representing the total number of items emitted by the source Observable before it completes. 137 | 138 | __Create__ constructs a new Observable from a Creator function, providing a bridge between imperative code and the reactive Observable pattern. The Observable will continue producing values until the Creator signals completion, the Observer unsubscribes, or the Creator returns an error. 139 | 140 | __Creator__ is a function type that generates values for an Observable stream. It receives a zero-based index for the current iteration and returns a tuple containing the next value to emit, any error that occurred, and a boolean flag indicating whether the sequence is complete. 141 | 142 | __Defer__ 143 | 144 | __Delay__ 145 | 146 | __DistinctUntilChanged__ only emits when the current value is different from the last. 147 | 148 | __Do__ calls a function for each next value passing through the observable. 149 | 150 | __ElementAt__ emit only item n emitted by an Observable. 151 | 152 | __Empty__ creates an Observable that emits no items but terminates normally. 153 | 154 | __EndWith__ 155 | 156 | __Equal__ 157 | 158 | __Err__ 159 | 160 | __ExhaustAll__ 161 | 162 | __ExhaustMap__ 163 | 164 | __Filter__ emits only those items from an observable that pass a predicate test. 165 | 166 | __First__ emits only the first item from an Observable. 167 | 168 | __Fprint__ 169 | 170 | __Fprintf__ 171 | 172 | __Fprintln__ 173 | 174 | __From__ creates an observable from multiple values passed in. 175 | 176 | __Go__ subscribes to the observable and starts execution on a separate goroutine, ignoring all emissions from the observable sequence. This makes it useful when you only care about side effects and not the actual values. Returns a Subscription that can be used to cancel the subscription when no longer needed. 177 | 178 | __Ignore[T]__ creates an Observer[T] that simply discards any emissions from an Observable. It is useful when you need to create an Observer but don't care about its values. 179 | 180 | __Interval__ creates an ObservableInt that emits a sequence of integers spaced by a particular time 181 | terval. 182 | 183 | __Last__ emits only the last item emitted by an Observable. 184 | 185 | __Map__ transforms the items emitted by an Observable by applying a function to each item. 186 | 187 | __MapE__ 188 | 189 | __Marshal__ 190 | 191 | __MaxBufferSizeOption__, __WithMaxBufferSize__ 192 | 193 | __Merge__ combines multiple Observables into one by merging their emissions. 194 | 195 | __MergeAll__ flattens a higher order observable by merging the observables it emits. 196 | 197 | __MergeMap__ transforms the items emitted by an Observable by applying a function to each item an 198 | turning an Observable. 199 | 200 | __MergeWith__ combines multiple Observables into one by merging their emissions. 201 | 202 | __Multicast__ 203 | 204 | __Must__ 205 | 206 | __Never__ creates an Observable that emits no items and does't terminate. 207 | 208 | __Observable__ 209 | 210 | __Observer__ 211 | 212 | __Of__ emits a variable amount of values in a sequence and then emits a complete notification. 213 | 214 | __OnComplete__ 215 | 216 | __OnDone__ 217 | 218 | __OnError__ 219 | 220 | __OnNext__ 221 | 222 | __Passthrough__ just passes through all output from the Observable. 223 | 224 | __Pipe__ 225 | 226 | __Print__ 227 | 228 | __Printf__ 229 | 230 | __Println__ subscribes to the Observable and prints every item to os.Stdout. 231 | 232 | __Publish__ returns a multicasting Observable[T] for an underlying Observable[T] as a Connectable[T] type. 233 | 234 | __Pull__ 235 | 236 | __Pull2__ 237 | 238 | __Race__ 239 | 240 | __RaceWith__ 241 | 242 | __Recv__ 243 | 244 | __Reduce__ applies a reducer function to each item emitted by an Observable and the previous reducer 245 | sult. 246 | 247 | __ReduceE__ 248 | 249 | __RefCount__ makes a Connectable behave like an ordinary Observable. 250 | 251 | __Repeat__ creates an observable that emits a sequence of items repeatedly. 252 | 253 | __Retry__ if a source Observable sends an error notification, resubscribe to it in the hopes that it 254 | ll complete without error. 255 | 256 | __RetryTime__ 257 | 258 | __SampleTime__ emits the most recent item emitted by an Observable within periodic time intervals. 259 | 260 | __Scan__ applies a accumulator function to each item emitted by an Observable and the previous 261 | cumulator result. 262 | 263 | __ScanE__ 264 | 265 | __Scheduler__ 266 | 267 | __Send__ 268 | 269 | __Share__ 270 | 271 | __Skip__ suppresses the first n items emitted by an Observable. 272 | 273 | __Slice__ 274 | 275 | __StartWith__ returns an observable that, at the moment of subscription, will synchronously emit all values provided to this operator, then subscribe to the source and mirror all of its emissions to subscribers. 276 | 277 | __Subject__ is a combination of an observer and observable. 278 | 279 | __Subscribe__ operates upon the emissions and notifications from an Observable. 280 | 281 | __SubscribeOn__ specifies the scheduler an Observable should use when it is subscribed to. 282 | 283 | __Subscriber__ 284 | 285 | __Subscription__ 286 | 287 | __SwitchAll__ 288 | 289 | __SwitchMap__ 290 | 291 | __Take__ emits only the first n items emitted by an Observable. 292 | 293 | __TakeWhile__ mirrors items emitted by an Observable until a specified condition becomes false. 294 | 295 | __Tap__ 296 | 297 | __Throw__ creates an observable that emits no items and terminates with an error. 298 | 299 | __Ticker__ creates an ObservableTime that emits a sequence of timestamps after an initialDelay has passed. 300 | 301 | __Timer__ creates an Observable that emits a sequence of integers (starting at zero) after an initialDelay has passed. 302 | 303 | __Tuple__ 304 | 305 | __Values__ 306 | 307 | __Wait__ subscribes to the Observable and waits for completion or error. 308 | 309 | __WithLatestFrom__ will subscribe to all Observables and wait for all of them to emit before emitting the first slice. 310 | 311 | __WithLatestFromAll__ flattens a higher order observable. 312 | 313 | __Zip__ 314 | 315 | __ZipAll__ 316 | -------------------------------------------------------------------------------- /all.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "iter" 4 | 5 | // All2 converts an Observable of Tuple2 pairs into an iterator sequence. It 6 | // transforms an Observable[Tuple2[T, U]] into an iter.Seq2[T, U] that yields 7 | // each tuple's components. The scheduler parameter is optional and determines 8 | // the execution context. Note: This method ignores any errors from the 9 | // observable stream. 10 | func All2[T, U any](observable Observable[Tuple2[T, U]], scheduler ...Scheduler) iter.Seq2[T, U] { 11 | return func(yield func(T, U) bool) { 12 | yielding := func(next Tuple2[T, U]) bool { 13 | return yield(next.First, next.Second) 14 | } 15 | err := observable.TakeWhile(yielding).Wait(scheduler...) 16 | _ = err // ignores error! so will fail silently 17 | } 18 | } 19 | 20 | // All converts an Observable stream into an iterator sequence that pairs each 21 | // element with its index. It returns an iter.Seq2[int, T] which yields each 22 | // element along with its position in the sequence. The scheduler parameter is 23 | // optional and determines the execution context. Note: This method ignores any 24 | // errors from the observable stream. 25 | func (observable Observable[T]) All(scheduler ...Scheduler) iter.Seq2[int, T] { 26 | return func(yield func(int, T) bool) { 27 | index := -1 28 | yielding := func(value T) bool { 29 | index++ 30 | return yield(index, value) 31 | } 32 | err := observable.TakeWhile(yielding).Wait(scheduler...) 33 | _ = err // ignores error! so will fail silently 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reactivego/rx" 7 | ) 8 | 9 | func TestAll2(t *testing.T) { 10 | t.Run("Basic pair iteration", func(t *testing.T) { 11 | tuples := rx.From([]rx.Tuple2[string, int]{ 12 | {"a", 1}, 13 | {"b", 2}, 14 | {"c", 3}, 15 | }...) 16 | 17 | var firsts []string 18 | var seconds []int 19 | 20 | for f, s := range rx.All2(tuples) { 21 | firsts = append(firsts, f) 22 | seconds = append(seconds, s) 23 | } 24 | 25 | expectedFirsts := []string{"a", "b", "c"} 26 | expectedSeconds := []int{1, 2, 3} 27 | 28 | if len(firsts) != len(expectedFirsts) { 29 | t.Errorf("Expected %v firsts, got %v", len(expectedFirsts), len(firsts)) 30 | } 31 | 32 | if len(seconds) != len(expectedSeconds) { 33 | t.Errorf("Expected %v seconds, got %v", len(expectedSeconds), len(seconds)) 34 | } 35 | 36 | for i, v := range expectedFirsts { 37 | if firsts[i] != v { 38 | t.Errorf("Expected first %v at position %v, got %v", v, i, firsts[i]) 39 | } 40 | } 41 | 42 | for i, v := range expectedSeconds { 43 | if seconds[i] != v { 44 | t.Errorf("Expected second %v at position %v, got %v", v, i, seconds[i]) 45 | } 46 | } 47 | }) 48 | 49 | t.Run("Empty tuple observable", func(t *testing.T) { 50 | tuples := rx.Empty[rx.Tuple2[string, int]]() 51 | count := 0 52 | 53 | for range rx.All2(tuples) { 54 | count++ 55 | } 56 | 57 | if count != 0 { 58 | t.Errorf("Expected 0 tuples, got %v", count) 59 | } 60 | }) 61 | 62 | t.Run("Direct sequence access", func(t *testing.T) { 63 | tuples := rx.From([]rx.Tuple2[string, int]{ 64 | {"x", 10}, 65 | {"y", 20}, 66 | }...) 67 | 68 | var firsts []string 69 | var seconds []int 70 | 71 | for f, s := range rx.All2(tuples) { 72 | firsts = append(firsts, f) 73 | seconds = append(seconds, s) 74 | } 75 | 76 | expectedFirsts := []string{"x", "y"} 77 | expectedSeconds := []int{10, 20} 78 | 79 | if len(firsts) != len(expectedFirsts) || len(seconds) != len(expectedSeconds) { 80 | t.Errorf("Expected %v pairs, got %v", len(expectedFirsts), len(firsts)) 81 | } 82 | 83 | for i := range expectedFirsts { 84 | if firsts[i] != expectedFirsts[i] || seconds[i] != expectedSeconds[i] { 85 | t.Errorf("At position %v, expected (%v, %v), got (%v, %v)", 86 | i, expectedFirsts[i], expectedSeconds[i], firsts[i], seconds[i]) 87 | } 88 | } 89 | }) 90 | } 91 | 92 | func TestAll(t *testing.T) { 93 | t.Run("Basic indexed values", func(t *testing.T) { 94 | nums := rx.From("a", "b", "c") 95 | var indexes []int 96 | var values []string 97 | for i, v := range nums.All() { 98 | indexes = append(indexes, i) 99 | values = append(values, v) 100 | } 101 | expectedIndexes := []int{0, 1, 2} 102 | expectedValues := []string{"a", "b", "c"} 103 | 104 | if len(indexes) != len(expectedIndexes) { 105 | t.Errorf("Expected %v indexes, got %v", len(expectedIndexes), len(indexes)) 106 | } 107 | 108 | if len(values) != len(expectedValues) { 109 | t.Errorf("Expected %v values, got %v", len(expectedValues), len(values)) 110 | } 111 | 112 | for i, v := range expectedIndexes { 113 | if indexes[i] != v { 114 | t.Errorf("Expected index %v at position %v, got %v", v, i, indexes[i]) 115 | } 116 | } 117 | 118 | for i, v := range expectedValues { 119 | if values[i] != v { 120 | t.Errorf("Expected value %v at position %v, got %v", v, i, values[i]) 121 | } 122 | } 123 | }) 124 | 125 | t.Run("Empty observable", func(t *testing.T) { 126 | nums := rx.Empty[string]() 127 | count := 0 128 | for i, v := range nums.All() { 129 | count++ 130 | _, _ = i, v // Avoid unused variable warnings 131 | } 132 | if count != 0 { 133 | t.Errorf("Expected 0 items, got %v", count) 134 | } 135 | }) 136 | 137 | t.Run("All with filtering", func(t *testing.T) { 138 | nums := rx.From(10, 20, 30, 40, 50) 139 | filtered := nums.Filter(func(i int) bool { 140 | return i > 25 141 | }) 142 | // Collect indexes and values using direct iteration 143 | var indexes []int 144 | var values []int 145 | seq := filtered.All() 146 | seq(func(i int, v int) bool { 147 | indexes = append(indexes, i) 148 | values = append(values, v) 149 | return true 150 | }) 151 | // Note: indexes should be 0, 1, 2 (not 2, 3, 4) because All reindexes 152 | expectedIndexes := []int{0, 1, 2} 153 | expectedValues := []int{30, 40, 50} 154 | 155 | if len(indexes) != len(expectedIndexes) { 156 | t.Errorf("Expected %v indexes, got %v", len(expectedIndexes), len(indexes)) 157 | } 158 | 159 | for i, v := range expectedIndexes { 160 | if indexes[i] != v { 161 | t.Errorf("Expected index %v at position %v, got %v", v, i, indexes[i]) 162 | } 163 | } 164 | 165 | for i, v := range expectedValues { 166 | if values[i] != v { 167 | t.Errorf("Expected value %v at position %v, got %v", v, i, values[i]) 168 | } 169 | } 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /append.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Append creates a pipe that appends each emitted value to the provided slice. 4 | // It passes each value through to the next observer after appending it. 5 | // This allows collecting all emitted values in a slice while still forwarding them. 6 | // Only values emitted before completion (done=false) are appended. 7 | func Append[T any](slice *[]T) Pipe[T] { 8 | return func(observable Observable[T]) Observable[T] { 9 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 10 | observable(func(next T, err error, done bool) { 11 | if !done { 12 | *slice = append(*slice, next) 13 | } 14 | observe(next, err, done) 15 | }, scheduler, subscriber) 16 | } 17 | } 18 | } 19 | 20 | // Append is a method variant of the Append function that appends each emitted value 21 | // to the provided slice while forwarding all emissions to downstream operators. 22 | // This is a convenience method that calls the standalone Append function. 23 | func (observable Observable[T]) Append(slice *[]T) Observable[T] { 24 | return Append(slice)(observable) 25 | } 26 | -------------------------------------------------------------------------------- /asobservable.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func AsObservable[T any](observable Observable[any]) Observable[T] { 4 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 5 | observable(observe.AsObserver(), scheduler, subscriber) 6 | } 7 | } 8 | 9 | func (observable Observable[T]) AsObservable() Observable[any] { 10 | return func(observe Observer[any], scheduler Scheduler, subscriber Subscriber) { 11 | observable(AsObserver[T](observe), scheduler, subscriber) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assign.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Assign creates a pipe that assigns every next value that is not done to the 4 | // provided variable. The pipe will forward all events (next, err, done) to the 5 | // next observer. 6 | func Assign[T any](value *T) Pipe[T] { 7 | return func(observable Observable[T]) Observable[T] { 8 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 9 | observable(func(next T, err error, done bool) { 10 | if !done { 11 | *value = next 12 | } 13 | observe(next, err, done) 14 | }, scheduler, subscriber) 15 | } 16 | } 17 | } 18 | 19 | // Assign is a method version of the Assign function. 20 | // It assigns every next value that is not done to the provided variable. 21 | func (observable Observable[T]) Assign(value *T) Observable[T] { 22 | return Assign(value)(observable) 23 | } 24 | -------------------------------------------------------------------------------- /autoconnect.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | var ErrInvalidCount = errors.Join(Err, errors.New("invalid count")) 10 | 11 | // AutoConnect returns an Observable that automatically connects to the Connectable source when a specified 12 | // number of subscribers subscribe to it. 13 | // 14 | // When the specified number of subscribers (count) is reached, the Connectable source is connected, 15 | // allowing it to start emitting items. The connection is shared among all subscribers. 16 | // When all subscribers unsubscribe, the connection is terminated. 17 | // 18 | // If count is less than 1, it returns an Observable that emits an ErrInvalidCount error. 19 | func (connectable Connectable[T]) AutoConnect(count int) Observable[T] { 20 | if count < 1 { 21 | return Throw[T](ErrInvalidCount) 22 | } 23 | var source struct { 24 | sync.Mutex 25 | refcount int32 26 | subscription *subscription 27 | } 28 | observable := func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 29 | subscriber.OnUnsubscribe(func() { 30 | source.Lock() 31 | if atomic.AddInt32(&source.refcount, -1) == 0 { 32 | if source.subscription != nil { 33 | source.subscription.Unsubscribe() 34 | } 35 | } 36 | source.Unlock() 37 | }) 38 | connectable.Observable(observe, scheduler, subscriber) 39 | source.Lock() 40 | if atomic.AddInt32(&source.refcount, 1) == int32(count) { 41 | if source.subscription == nil || source.subscription.err != nil { 42 | source.subscription = newSubscription(scheduler) 43 | source.Unlock() 44 | connectable.Connector(scheduler, source.subscription) 45 | source.Lock() 46 | } 47 | } 48 | source.Unlock() 49 | } 50 | return observable 51 | } 52 | -------------------------------------------------------------------------------- /autounsubscribe.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func AutoUnsubscribe[T any]() Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | subscriber = subscriber.Add() 7 | observer := func(next T, err error, done bool) { 8 | observe(next, err, done) 9 | if done { 10 | subscriber.Unsubscribe() 11 | } 12 | } 13 | observable(observer, scheduler, subscriber) 14 | } 15 | } 16 | } 17 | 18 | func (observable Observable[T]) AutoUnsubscribe() Observable[T] { 19 | return AutoUnsubscribe[T]()(observable) 20 | } 21 | -------------------------------------------------------------------------------- /buffercount.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func BufferCount[T any](observable Observable[T], bufferSize, startBufferEvery int) Observable[[]T] { 4 | return func(observe Observer[[]T], scheduler Scheduler, subscriber Subscriber) { 5 | var buffer []T 6 | observer := func(next T, err error, done bool) { 7 | switch { 8 | case !done: 9 | buffer = append(buffer, next) 10 | n := len(buffer) 11 | if n >= bufferSize { 12 | if n == bufferSize { 13 | clone := append(make([]T, 0, n), buffer...) 14 | observe(clone, nil, false) 15 | } 16 | if n >= startBufferEvery { 17 | n = copy(buffer, buffer[startBufferEvery:]) 18 | buffer = buffer[:n] 19 | } 20 | } 21 | case err != nil: 22 | observe(nil, err, true) 23 | default: 24 | n := len(buffer) 25 | if 0 < n && n <= bufferSize { 26 | Of(buffer)(observe, scheduler, subscriber) 27 | } else { 28 | observe(nil, nil, true) 29 | } 30 | } 31 | } 32 | observable(observer, scheduler, subscriber) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /catch.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Catch[T any](other Observable[T]) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | if !done || err == nil { 8 | observe(next, err, done) 9 | } else { 10 | other(observe, scheduler, subscriber) 11 | } 12 | }, scheduler, subscriber) 13 | } 14 | } 15 | } 16 | 17 | func (observable Observable[T]) Catch(other Observable[T]) Observable[T] { 18 | return Catch[T](other)(observable) 19 | } 20 | -------------------------------------------------------------------------------- /catcherror.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func CatchError[T any](selector func(err error, caught Observable[T]) Observable[T]) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | if !done || err == nil { 8 | observe(next, err, done) 9 | } else { 10 | selector(err, observable)(observe, scheduler, subscriber) 11 | } 12 | }, scheduler, subscriber) 13 | } 14 | } 15 | } 16 | 17 | func (observable Observable[T]) CatchError(selector func(err error, caught Observable[T]) Observable[T]) Observable[T] { 18 | return CatchError[T](selector)(observable) 19 | } 20 | -------------------------------------------------------------------------------- /combineall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func CombineAll[T any](observable Observable[Observable[T]]) Observable[[]T] { 8 | return func(observe Observer[[]T], scheduler Scheduler, subscriber Subscriber) { 9 | sources := []Observable[T](nil) 10 | var buffers struct { 11 | sync.Mutex 12 | assigned []bool 13 | values []T 14 | initialized int 15 | active int 16 | } 17 | makeObserver := func(sourceIndex int) Observer[T] { 18 | observer := func(next T, err error, done bool) { 19 | buffers.Lock() 20 | defer buffers.Unlock() 21 | if buffers.active > 0 { 22 | switch { 23 | case !done: 24 | if !buffers.assigned[sourceIndex] { 25 | buffers.assigned[sourceIndex] = true 26 | buffers.initialized++ 27 | } 28 | buffers.values[sourceIndex] = next 29 | if buffers.initialized == len(buffers.values) { 30 | observe(buffers.values, nil, false) 31 | } 32 | case err != nil: 33 | buffers.active = 0 34 | var zero []T 35 | observe(zero, err, true) 36 | default: 37 | if buffers.active--; buffers.active == 0 { 38 | var zero []T 39 | observe(zero, nil, true) 40 | } 41 | } 42 | } 43 | } 44 | return observer 45 | } 46 | observer := func(next Observable[T], err error, done bool) { 47 | switch { 48 | case !done: 49 | sources = append(sources, next) 50 | case err != nil: 51 | var zero []T 52 | observe(zero, err, true) 53 | default: 54 | if len(sources) == 0 { 55 | var zero []T 56 | observe(zero, nil, true) 57 | return 58 | } 59 | runner := scheduler.Schedule(func() { 60 | if subscriber.Subscribed() { 61 | numSources := len(sources) 62 | buffers.assigned = make([]bool, numSources) 63 | buffers.values = make([]T, numSources) 64 | buffers.active = numSources 65 | for sourceIndex, source := range sources { 66 | if !subscriber.Subscribed() { 67 | return 68 | } 69 | source.AutoUnsubscribe()(makeObserver(sourceIndex), scheduler, subscriber) 70 | } 71 | } 72 | }) 73 | subscriber.OnUnsubscribe(runner.Cancel) 74 | } 75 | } 76 | observable(observer, scheduler, subscriber) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /combinelatest.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func CombineLatest[T any](observables ...Observable[T]) Observable[[]T] { 4 | return CombineAll(From(observables...)) 5 | } 6 | 7 | func CombineLatest2[T, U any](first Observable[T], second Observable[U]) Observable[Tuple2[T, U]] { 8 | return Map(CombineAll(From(first.AsObservable(), second.AsObservable())), func(next []any) Tuple2[T, U] { 9 | return Tuple2[T, U]{next[0].(T), next[1].(U)} 10 | }) 11 | } 12 | 13 | func CombineLatest3[T, U, V any](first Observable[T], second Observable[U], third Observable[V]) Observable[Tuple3[T, U, V]] { 14 | return Map(CombineAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable())), func(next []any) Tuple3[T, U, V] { 15 | return Tuple3[T, U, V]{next[0].(T), next[1].(U), next[2].(V)} 16 | }) 17 | } 18 | 19 | func CombineLatest4[T, U, V, W any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W]) Observable[Tuple4[T, U, V, W]] { 20 | return Map(CombineAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable())), func(next []any) Tuple4[T, U, V, W] { 21 | return Tuple4[T, U, V, W]{next[0].(T), next[1].(U), next[2].(V), next[3].(W)} 22 | }) 23 | } 24 | 25 | func CombineLatest5[T, U, V, W, X any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W], fifth Observable[X]) Observable[Tuple5[T, U, V, W, X]] { 26 | return Map(CombineAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable(), fifth.AsObservable())), func(next []any) Tuple5[T, U, V, W, X] { 27 | return Tuple5[T, U, V, W, X]{next[0].(T), next[1].(U), next[2].(V), next[3].(W), next[4].(X)} 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /concat.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Concat[T any](observables ...Observable[T]) Observable[T] { 4 | if len(observables) == 0 { 5 | return Empty[T]() 6 | } 7 | return observables[0].ConcatWith(observables[1:]...) 8 | } 9 | -------------------------------------------------------------------------------- /concatall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func ConcatAll[T any](observable Observable[Observable[T]]) Observable[T] { 8 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 9 | var concat struct { 10 | sync.Mutex 11 | chain []func() 12 | } 13 | concatenator := func(next T, err error, done bool) { 14 | concat.Lock() 15 | defer concat.Unlock() 16 | if !done || err != nil { 17 | observe(next, err, done) 18 | } else { 19 | if len(concat.chain) > 0 { 20 | link := concat.chain[0] 21 | concat.chain = concat.chain[1:] 22 | link() 23 | } else { 24 | concat.chain = nil 25 | } 26 | } 27 | } 28 | link := func(observable Observable[T]) func() { 29 | return func() { 30 | if observable != nil { 31 | observable.AutoUnsubscribe()(concatenator, scheduler, subscriber) 32 | } else { 33 | Empty[T]()(observe, scheduler, subscriber) 34 | } 35 | } 36 | } 37 | appender := func(next Observable[T], err error, done bool) { 38 | if !done || err == nil { 39 | concat.Lock() 40 | active := (concat.chain != nil) 41 | concat.chain = append(concat.chain, link(next)) 42 | concat.Unlock() 43 | if !active { 44 | var zero T 45 | concatenator(zero, nil, true) 46 | } 47 | } else { 48 | var zero T 49 | concatenator(zero, err, true) 50 | } 51 | } 52 | observable.AutoUnsubscribe()(appender, scheduler, subscriber) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /concatmap.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func ConcatMap[T, U any](observable Observable[T], project func(T) Observable[U]) Observable[U] { 4 | return ConcatAll(Map(observable, project)) 5 | } 6 | -------------------------------------------------------------------------------- /concatwith.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "slices" 4 | 5 | func ConcatWith[T any](others ...Observable[T]) Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | if len(others) == 0 { 8 | return observable 9 | } 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | var ( 12 | observables = slices.Clone(others) 13 | observer Observer[T] 14 | ) 15 | observer = func(next T, err error, done bool) { 16 | if !done || err != nil { 17 | observe(next, err, done) 18 | } else { 19 | if len(observables) == 0 { 20 | var zero T 21 | observe(zero, nil, true) 22 | } else { 23 | o := observables[0] 24 | observables = observables[1:] 25 | o(observer, scheduler, subscriber) 26 | } 27 | } 28 | } 29 | observable(observer, scheduler, subscriber) 30 | } 31 | } 32 | } 33 | 34 | func (observable Observable[T]) ConcatWith(others ...Observable[T]) Observable[T] { 35 | return ConcatWith(others...)(observable) 36 | } 37 | -------------------------------------------------------------------------------- /connectable.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Connectable[T] is an Observable[T] that provides delayed connection to its source. 4 | // It combines both Observable and Connector interfaces: 5 | // - Subscribe: Allows consumers to register for notifications from this Observable 6 | // - Connect: Triggers the actual subscription to the underlying source Observable 7 | // 8 | // The key feature of Connectable[T] is that it doesn't subscribe to its source 9 | // until the Connect method is explicitly called, allowing multiple observers to 10 | // subscribe before the source begins emitting items (multicast behavior). 11 | type Connectable[T any] struct { 12 | Observable[T] 13 | Connector 14 | } 15 | -------------------------------------------------------------------------------- /connector.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Connector provides the Connect method for a Connectable[T]. 4 | type Connector func(Scheduler, Subscriber) 5 | 6 | // Connect instructs a Connectable[T] to subscribe to its source and begin 7 | // emitting items to its subscribers. Connect accepts an optional scheduler 8 | // argument. 9 | func (connect Connector) Connect(schedulers ...Scheduler) Subscription { 10 | if len(schedulers) == 0 { 11 | schedulers = []Scheduler{NewScheduler()} 12 | } 13 | scheduler := schedulers[0] 14 | subscription := newSubscription(scheduler) 15 | connect(schedulers[0], subscription) 16 | return subscription 17 | } 18 | -------------------------------------------------------------------------------- /constraints.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. Use of this source code 2 | // is governed by a BSD-style license that can be found in the LICENSE file for 3 | // package golang.org/x/exp 4 | 5 | package rx 6 | 7 | // Signed is a constraint that permits any signed integer type. 8 | // If future releases of Go add new predeclared signed integer types, 9 | // this constraint will be modified to include them. 10 | type Signed interface { 11 | ~int | ~int8 | ~int16 | ~int32 | ~int64 12 | } 13 | 14 | // Unsigned is a constraint that permits any unsigned integer type. 15 | // If future releases of Go add new predeclared unsigned integer types, 16 | // this constraint will be modified to include them. 17 | type Unsigned interface { 18 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 19 | } 20 | 21 | // Integer is a constraint that permits any integer type. 22 | // If future releases of Go add new predeclared integer types, 23 | // this constraint will be modified to include them. 24 | type Integer interface { 25 | Signed | Unsigned 26 | } 27 | 28 | // Float is a constraint that permits any floating-point type. 29 | // If future releases of Go add new predeclared floating-point types, 30 | // this constraint will be modified to include them. 31 | type Float interface { 32 | ~float32 | ~float64 33 | } 34 | -------------------------------------------------------------------------------- /count.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Count() Observable[int] { 4 | return func(observe Observer[int], scheduler Scheduler, subscriber Subscriber) { 5 | var count int 6 | observer := func(next T, err error, done bool) { 7 | if !done { 8 | count++ 9 | } else { 10 | Of(count)(observe, scheduler, subscriber) 11 | } 12 | } 13 | observable(observer, scheduler, subscriber) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /create.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Create constructs a new Observable from a Creator function. 4 | // 5 | // The Creator function is called repeatedly with an incrementing index value, 6 | // and returns a tuple of (next value, error, done flag). The Observable will 7 | // continue producing values until either: 8 | // 1. The Creator signals completion by returning done=true 9 | // 2. The Observer unsubscribes 10 | // 3. The Creator returns an error (which will be emitted with done=true) 11 | // 12 | // This function provides a bridge between imperative code and the reactive 13 | // Observable pattern. 14 | func Create[T any](create Creator[T]) Observable[T] { 15 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 16 | task := func(index int, again func(int)) { 17 | if subscriber.Subscribed() { 18 | next, err, done := create(index) 19 | if subscriber.Subscribed() { 20 | if !done { 21 | observe(next, nil, false) 22 | if subscriber.Subscribed() { 23 | again(index + 1) 24 | } 25 | } else { 26 | var zero T 27 | observe(zero, err, true) 28 | } 29 | } 30 | } 31 | } 32 | runner := scheduler.ScheduleLoop(0, task) 33 | subscriber.OnUnsubscribe(runner.Cancel) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /creator.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Creator[T] is a function type that generates values for an Observable stream. 4 | // 5 | // The Creator function receives a zero-based index for the current iteration and 6 | // returns a tuple containing: 7 | // - Next: The next value to emit (of type T) 8 | // - Err: Any error that occurred during value generation 9 | // - Done: Boolean flag indicating whether the sequence is complete 10 | // 11 | // When Done is true, the Observable will complete after emitting any provided error. 12 | // When Err is non-nil, the Observable will emit the error and then complete. 13 | type Creator[T any] func(index int) (Next T, Err error, Done bool) 14 | -------------------------------------------------------------------------------- /defer.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Defer creates an Observable that will use the provided factory function to 4 | // create a new Observable every time it's subscribed to. This is useful for 5 | // creating cold Observables or for delaying expensive Observable creation until 6 | // subscription time. 7 | func Defer[T any](factory func() Observable[T]) Observable[T] { 8 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 9 | factory()(observe, scheduler, subscriber) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /delay.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | func Delay[T any](duration time.Duration) Pipe[T] { 9 | type emission[T any] struct { 10 | at time.Time 11 | next T 12 | err error 13 | done bool 14 | } 15 | 16 | return func(observable Observable[T]) Observable[T] { 17 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 18 | var delay struct { 19 | sync.Mutex 20 | emissions []emission[T] 21 | } 22 | delayer := scheduler.ScheduleFutureRecursive(duration, func(again func(time.Duration)) { 23 | if subscriber.Subscribed() { 24 | delay.Lock() 25 | for _, entry := range delay.emissions { 26 | delay.Unlock() 27 | due := entry.at.Sub(scheduler.Now()) 28 | if due > 0 { 29 | again(due) 30 | return 31 | } 32 | observe(entry.next, entry.err, entry.done) 33 | if entry.done || !subscriber.Subscribed() { 34 | return 35 | } 36 | delay.Lock() 37 | delay.emissions = delay.emissions[1:] 38 | } 39 | delay.Unlock() 40 | again(duration) // keep on rescheduling the emitter 41 | } 42 | }) 43 | subscriber.OnUnsubscribe(delayer.Cancel) 44 | observer := func(next T, err error, done bool) { 45 | delay.Lock() 46 | delay.emissions = append(delay.emissions, emission[T]{scheduler.Now().Add(duration), next, err, done}) 47 | delay.Unlock() 48 | } 49 | observable(observer, scheduler, subscriber) 50 | } 51 | } 52 | } 53 | 54 | func (observable Observable[T]) Delay(duration time.Duration) Observable[T] { 55 | return Delay[T](duration)(observable) 56 | } 57 | -------------------------------------------------------------------------------- /distinctuntilchanged.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func DistinctUntilChanged[T any](equal func(T, T) bool) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], subscribeOn Scheduler, subscriber Subscriber) { 6 | var seen struct { 7 | initialized bool 8 | value T 9 | } 10 | observer := func(next T, err error, done bool) { 11 | if !done { 12 | if seen.initialized && equal(seen.value, next) { 13 | return // skip equal 14 | } else { 15 | seen.initialized = true 16 | seen.value = next 17 | } 18 | } 19 | observe(next, err, done) 20 | } 21 | observable(observer, subscribeOn, subscriber) 22 | } 23 | } 24 | } 25 | 26 | func (observable Observable[T]) DistinctUntilChanged(equal func(T, T) bool) Observable[T] { 27 | return DistinctUntilChanged(equal)(observable) 28 | } 29 | -------------------------------------------------------------------------------- /do.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Do[T any](do func(T)) Pipe[T] { 4 | return Tap[T](func(next T, err error, done bool) { 5 | if !done { 6 | do(next) 7 | } 8 | }) 9 | } 10 | 11 | func (observable Observable[T]) Do(f func(T)) Observable[T] { 12 | return Do(f)(observable) 13 | } 14 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package rx provides Reactive Extensions, a powerful API for asynchronous programming in Go, 2 | // built around observables and operators to process streams of data seamlessly. 3 | package rx 4 | -------------------------------------------------------------------------------- /elementat.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func ElementAt[T any](n int) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | i := 0 7 | observer := func(next T, err error, done bool) { 8 | if !done { 9 | if i >= n { 10 | if i == n { 11 | observe(next, nil, false) 12 | } else { 13 | var zero T 14 | observe(zero, nil, true) 15 | } 16 | } 17 | i++ 18 | } else { 19 | observe(next, err, done) 20 | } 21 | } 22 | observable(observer, scheduler, subscriber) 23 | } 24 | } 25 | } 26 | 27 | func (observable Observable[T]) ElementAt(n int) Observable[T] { 28 | return ElementAt[T](n)(observable) 29 | } 30 | -------------------------------------------------------------------------------- /empty.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Empty[T any]() Observable[T] { 4 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 5 | task := func() { 6 | if subscriber.Subscribed() { 7 | var zero T 8 | observe(zero, nil, true) 9 | } 10 | } 11 | runner := scheduler.Schedule(task) 12 | subscriber.OnUnsubscribe(runner.Cancel) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /endwith.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func EndWith[T any](values ...T) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return observable.ConcatWith(From(values...)) 6 | } 7 | } 8 | 9 | func (observable Observable[T]) EndWith(values ...T) Observable[T] { 10 | return EndWith(values...)(observable) 11 | } 12 | -------------------------------------------------------------------------------- /equal.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Equal[T comparable]() func(T, T) bool { 4 | return func(a T, b T) bool { return a == b } 5 | } 6 | -------------------------------------------------------------------------------- /err.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "errors" 4 | 5 | // Err declares the base error that is joined with every error returned by this package. 6 | // It serves as the foundation for error types in the reactive extensions library, 7 | // allowing for error type checking and custom error creation with consistent taxonomy. 8 | var Err = errors.New("rx") 9 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/reactivego/rx" 11 | "github.com/reactivego/scheduler" 12 | ) 13 | 14 | func Example_share() { 15 | serial := rx.NewScheduler() 16 | 17 | shared := rx.From(1, 2, 3).Share() 18 | 19 | shared.Println().Go(serial) 20 | shared.Println().Go(serial) 21 | shared.Println().Go(serial) 22 | 23 | serial.Wait() 24 | // Output: 25 | // 1 26 | // 1 27 | // 1 28 | // 2 29 | // 2 30 | // 2 31 | // 3 32 | // 3 33 | // 3 34 | } 35 | 36 | func Example_subject() { 37 | serial := rx.NewScheduler() 38 | 39 | // subject collects emits when there are no subscriptions active. 40 | in, out := rx.Subject[int](0, 1) 41 | 42 | // ignore everything before any subscriptions, except the last because buffer size is 1 43 | in.Next(-2) 44 | in.Next(-1) 45 | in.Next(0) 46 | in.Next(1) 47 | 48 | // add a couple of subscriptions 49 | sub1 := out.Println().Go(serial) 50 | sub2 := out.Println().Go(serial) 51 | 52 | // schedule the subsequent emits on the serial scheduler otherwise these calls 53 | // will block because the buffer is full. 54 | // subject will detect usage of scheduler on observable side and use it on the 55 | // observer side to keep the data flow through the subject going. 56 | serial.Schedule(func() { 57 | in.Next(2) 58 | in.Next(3) 59 | in.Done(rx.Err) 60 | }) 61 | 62 | serial.Wait() 63 | fmt.Println(sub1.Wait()) 64 | fmt.Println(sub2.Wait()) 65 | // Output: 66 | // 1 67 | // 1 68 | // 2 69 | // 2 70 | // 3 71 | // 3 72 | // rx 73 | // rx 74 | } 75 | 76 | func Example_multicast() { 77 | serial := rx.NewScheduler() 78 | 79 | in, out := rx.Multicast[int](1) 80 | 81 | // Ignore everything before any subscriptions, including the last! 82 | in.Next(-2) 83 | in.Next(-1) 84 | in.Next(0) 85 | in.Next(1) 86 | 87 | // Schedule the subsequent emits in a loop. This will be the first task to 88 | // run on the serial scheduler after the subscriptions have been added. 89 | serial.ScheduleLoop(2, func(index int, again func(next int)) { 90 | if index < 4 { 91 | in.Next(index) 92 | again(index + 1) 93 | } else { 94 | in.Done(rx.Err) 95 | } 96 | }) 97 | 98 | // Add a couple of subscriptions 99 | sub1 := out.Println().Go(serial) 100 | sub2 := out.Println().Go(serial) 101 | 102 | // Let the scheduler run and wait for all of its scheduled tasks to finish. 103 | serial.Wait() 104 | fmt.Println(sub1.Wait()) 105 | fmt.Println(sub2.Wait()) 106 | // Output: 107 | // 2 108 | // 2 109 | // 3 110 | // 3 111 | // rx 112 | // rx 113 | } 114 | 115 | func Example_multicastDrop() { 116 | serial := rx.NewScheduler() 117 | 118 | const onBackpressureDrop = -1 119 | 120 | // multicast with backpressure handling set to dropping incoming 121 | // items that don't fit in the buffer once it has filled up. 122 | in, out := rx.Multicast[int](1 * onBackpressureDrop) 123 | 124 | // ignore everything before any subscriptions, including the last! 125 | in.Next(-2) 126 | in.Next(-1) 127 | in.Next(0) 128 | in.Next(1) 129 | 130 | // add a couple of subscriptions 131 | sub1 := out.Println().Go(serial) 132 | sub2 := out.Println().Go(serial) 133 | 134 | in.Next(2) // accepted: buffer not full 135 | in.Next(3) // dropped: buffer full 136 | in.Done(rx.Err) // dropped: buffer full 137 | 138 | serial.Wait() 139 | fmt.Println(sub1.Wait()) 140 | fmt.Println(sub2.Wait()) 141 | // Output: 142 | // 2 143 | // 2 144 | // 145 | // 146 | } 147 | 148 | func Example_concatAll() { 149 | source := rx.Empty[rx.Observable[string]]() 150 | rx.ConcatAll(source).Wait() 151 | 152 | source = rx.Of(rx.Empty[string]()) 153 | rx.ConcatAll(source).Wait() 154 | 155 | req := func(request string, duration time.Duration) rx.Observable[string] { 156 | req := rx.From(request + " response") 157 | if duration == 0 { 158 | return req 159 | } 160 | return req.Delay(duration) 161 | } 162 | 163 | const ms = time.Millisecond 164 | 165 | req1 := req("first", 10*ms) 166 | req2 := req("second", 20*ms) 167 | req3 := req("third", 0*ms) 168 | req4 := req("fourth", 60*ms) 169 | 170 | source = rx.From(req1).ConcatWith(rx.From(req2, req3, req4).Delay(100 * ms)) 171 | rx.ConcatAll(source).Println().Wait() 172 | 173 | fmt.Println("OK") 174 | // Output: 175 | // first response 176 | // second response 177 | // third response 178 | // fourth response 179 | // OK 180 | } 181 | 182 | func Example_race() { 183 | const ms = time.Millisecond 184 | 185 | req := func(request string, duration time.Duration) rx.Observable[string] { 186 | return rx.From(request + " response").Delay(duration) 187 | } 188 | 189 | req1 := req("first", 50*ms) 190 | req2 := req("second", 10*ms) 191 | req3 := req("third", 60*ms) 192 | 193 | rx.Race(req1, req2, req3).Println().Wait() 194 | 195 | err := func(text string, duration time.Duration) rx.Observable[int] { 196 | return rx.Throw[int](errors.New(text + " error")).Delay(duration) 197 | } 198 | 199 | err1 := err("first", 10*ms) 200 | err2 := err("second", 20*ms) 201 | err3 := err("third", 30*ms) 202 | 203 | fmt.Println(rx.Race(err1, err2, err3).Wait(rx.Goroutine)) 204 | // Output: 205 | // second response 206 | // first error 207 | } 208 | 209 | func Example_marshal() { 210 | type R struct { 211 | A string `json:"a"` 212 | B string `json:"b"` 213 | } 214 | 215 | b2s := func(data []byte) string { return string(data) } 216 | 217 | rx.Map(rx.Of(R{"Hello", "World"}).Marshal(json.Marshal), b2s).Println().Wait() 218 | // Output: 219 | // {"a":"Hello","b":"World"} 220 | } 221 | 222 | func Example_elementAt() { 223 | rx.From(0, 1, 2, 3, 4).ElementAt(2).Println().Wait() 224 | // Output: 225 | // 2 226 | } 227 | 228 | func Example_exhaustAll() { 229 | const ms = time.Millisecond 230 | 231 | stream := func(name string, duration time.Duration, count int) rx.Observable[string] { 232 | return rx.Map(rx.Timer[int](0*ms, duration), func(next int) string { 233 | return name + "-" + strconv.Itoa(next) 234 | }).Take(count) 235 | } 236 | 237 | streams := []rx.Observable[string]{ 238 | stream("a", 20*ms, 3), 239 | stream("b", 20*ms, 3), 240 | stream("c", 20*ms, 3), 241 | rx.Empty[string](), 242 | } 243 | 244 | streamofstreams := rx.Map(rx.Timer[int](20*ms, 30*ms, 250*ms, 100*ms).Take(4), func(next int) rx.Observable[string] { 245 | return streams[next] 246 | }) 247 | 248 | err := rx.ExhaustAll(streamofstreams).Println().Wait() 249 | 250 | if err == nil { 251 | fmt.Println("success") 252 | } 253 | // Output: 254 | // a-0 255 | // a-1 256 | // a-2 257 | // c-0 258 | // c-1 259 | // c-2 260 | // success 261 | } 262 | 263 | func Example_bufferCount() { 264 | source := rx.From(0, 1, 2, 3) 265 | 266 | fmt.Println("BufferCount(From(0, 1, 2, 3), 2, 1)") 267 | rx.BufferCount(source, 2, 1).Println().Wait() 268 | 269 | fmt.Println("BufferCount(From(0, 1, 2, 3), 2, 2)") 270 | rx.BufferCount(source, 2, 2).Println().Wait() 271 | 272 | fmt.Println("BufferCount(From(0, 1, 2, 3), 2, 3)") 273 | rx.BufferCount(source, 2, 3).Println().Wait() 274 | 275 | fmt.Println("BufferCount(From(0, 1, 2, 3), 3, 2)") 276 | rx.BufferCount(source, 3, 2).Println().Wait() 277 | 278 | fmt.Println("BufferCount(From(0, 1, 2, 3), 6, 6)") 279 | rx.BufferCount(source, 6, 6).Println().Wait() 280 | 281 | fmt.Println("BufferCount(From(0, 1, 2, 3), 2, 0)") 282 | rx.BufferCount(source, 2, 0).Println().Wait() 283 | // Output: 284 | // BufferCount(From(0, 1, 2, 3), 2, 1) 285 | // [0 1] 286 | // [1 2] 287 | // [2 3] 288 | // [3] 289 | // BufferCount(From(0, 1, 2, 3), 2, 2) 290 | // [0 1] 291 | // [2 3] 292 | // BufferCount(From(0, 1, 2, 3), 2, 3) 293 | // [0 1] 294 | // [3] 295 | // BufferCount(From(0, 1, 2, 3), 3, 2) 296 | // [0 1 2] 297 | // [2 3] 298 | // BufferCount(From(0, 1, 2, 3), 6, 6) 299 | // [0 1 2 3] 300 | // BufferCount(From(0, 1, 2, 3), 2, 0) 301 | // [0 1] 302 | } 303 | 304 | func Example_switchAll() { 305 | const ms = time.Millisecond 306 | 307 | interval42x4 := rx.Interval[int](42 * ms).Take(4) 308 | interval16x4 := rx.Interval[int](16 * ms).Take(4) 309 | 310 | err := rx.SwitchAll(rx.Map(interval42x4, func(next int) rx.Observable[int] { return interval16x4 })).Println().Wait(rx.Goroutine) 311 | 312 | if err == nil { 313 | fmt.Println("success") 314 | } 315 | // Output: 316 | // 0 317 | // 1 318 | // 0 319 | // 1 320 | // 0 321 | // 1 322 | // 0 323 | // 1 324 | // 2 325 | // 3 326 | // success 327 | } 328 | 329 | func Example_switchMap() { 330 | const ms = time.Millisecond 331 | 332 | webreq := func(request string, duration time.Duration) rx.Observable[string] { 333 | return rx.From(request + " result").Delay(duration) 334 | } 335 | 336 | first := webreq("first", 50*ms) 337 | second := webreq("second", 10*ms) 338 | latest := webreq("latest", 50*ms) 339 | 340 | switchmap := rx.SwitchMap(rx.Interval[int](20*ms).Take(3), func(i int) rx.Observable[string] { 341 | switch i { 342 | case 0: 343 | return first 344 | case 1: 345 | return second 346 | case 2: 347 | return latest 348 | default: 349 | return rx.Empty[string]() 350 | } 351 | }) 352 | 353 | err := switchmap.Println().Wait() 354 | if err == nil { 355 | fmt.Println("success") 356 | } 357 | // Output: 358 | // second result 359 | // latest result 360 | // success 361 | } 362 | 363 | func Example_retry() { 364 | var first error = rx.Err 365 | a := rx.Create(func(index int) (next int, err error, done bool) { 366 | if index < 3 { 367 | return index, nil, false 368 | } 369 | err, first = first, nil 370 | return 0, err, true 371 | }) 372 | err := a.Retry().Println().Wait() 373 | fmt.Println(first == nil) 374 | fmt.Println(err) 375 | // Output: 376 | // 0 377 | // 1 378 | // 2 379 | // 0 380 | // 1 381 | // 2 382 | // true 383 | // 384 | } 385 | 386 | func Example_count() { 387 | source := rx.From(1, 2, 3, 4, 5) 388 | 389 | count := source.Count() 390 | count.Println().Wait() 391 | 392 | emptySource := rx.Empty[int]() 393 | emptyCount := emptySource.Count() 394 | emptyCount.Println().Wait() 395 | 396 | fmt.Println("OK") 397 | // Output: 398 | // 5 399 | // 0 400 | // OK 401 | } 402 | 403 | func Example_values() { 404 | source := rx.From(1, 3, 5) 405 | 406 | // Why choose the Goroutine concurrent scheduler? 407 | // An observable can actually be at the root of a tree 408 | // of separately running observables that have their 409 | // responses merged. The Goroutine scheduler allows 410 | // these observables to run concurrently. 411 | 412 | // run the observable on 1 or more goroutines 413 | for i := range source.Values(scheduler.Goroutine) { 414 | // This is called from a newly created goroutine 415 | fmt.Println(i) 416 | } 417 | 418 | // run the observable on the current goroutine 419 | for i := range source.Values(scheduler.New()) { 420 | fmt.Println(i) 421 | } 422 | 423 | fmt.Println("OK") 424 | // Output: 425 | // 1 426 | // 3 427 | // 5 428 | // 1 429 | // 3 430 | // 5 431 | // OK 432 | } 433 | 434 | func Example_all() { 435 | source := rx.From("ZERO", "ONE", "TWO") 436 | 437 | for k, v := range source.All() { 438 | fmt.Println(k, v) 439 | } 440 | 441 | fmt.Println("OK") 442 | // Output: 443 | // 0 ZERO 444 | // 1 ONE 445 | // 2 TWO 446 | // OK 447 | } 448 | 449 | func Example_skip() { 450 | rx.From(1, 2, 3, 4, 5).Skip(2).Println().Wait() 451 | // Output: 452 | // 3 453 | // 4 454 | // 5 455 | } 456 | 457 | func Example_autoConnect() { 458 | // Create a multicaster hot observable that will emit every 100 milliseconds 459 | hot := rx.Interval[int](100 * time.Millisecond).Take(10).Publish() 460 | hotsub := hot.Connect(rx.Goroutine) 461 | defer hotsub.Unsubscribe() 462 | fmt.Println("Hot observable created and emitting 0,1,2,3,4,5,6 ...") 463 | 464 | // Publish the hot observable again but only Connect to it when 2 465 | // subscribers have connected. 466 | source := hot.Take(5).Publish().AutoConnect(2) 467 | 468 | // First subscriber 469 | sub1 := source.Printf("Subscriber 1: %d\n").Go() 470 | fmt.Println("First subscriber connected, waiting a bit...") 471 | 472 | // Wait a bit, nothing will emit yet 473 | time.Sleep(525 * time.Millisecond) 474 | 475 | fmt.Println("Second subscriber connecting, emissions begin!") 476 | // Second subscriber triggers the connection 477 | sub2 := source.Printf("Subscriber 2: %d\n").Go() 478 | 479 | // Wait for emissions to complete 480 | hotsub.Wait() 481 | sub1.Wait() 482 | sub2.Wait() 483 | 484 | // Unordered output: 485 | // Hot observable created and emitting 0,1,2,3,4,5,6 ... 486 | // First subscriber connected, waiting a bit... 487 | // Second subscriber connecting, emissions begin! 488 | // Subscriber 1: 5 489 | // Subscriber 2: 5 490 | // Subscriber 1: 6 491 | // Subscriber 2: 6 492 | // Subscriber 1: 7 493 | // Subscriber 2: 7 494 | // Subscriber 1: 8 495 | // Subscriber 2: 8 496 | // Subscriber 1: 9 497 | // Subscriber 2: 9 498 | } 499 | 500 | func Example_mergeMap() { 501 | source := rx.From("https://reactivego.io", "https://github.com/reactivego") 502 | 503 | merged := rx.MergeMap(source, func(next string) rx.Observable[string] { 504 | fakeFetchData := rx.Of(fmt.Sprintf("content of %q", next)) 505 | return fakeFetchData 506 | }) 507 | 508 | merged.Println().Go().Wait() 509 | 510 | // Output: 511 | // content of "https://reactivego.io" 512 | // content of "https://github.com/reactivego" 513 | } 514 | 515 | func Example_mergeMapSubject() { 516 | source := rx.From("https://google.com", "https://reactivego.io", "https://github.com/reactivego") 517 | 518 | merged := rx.MergeMap(source, func(next string) rx.Observable[string] { 519 | fakeFetchData := rx.Of(fmt.Sprintf("content of %q", next)) 520 | return fakeFetchData 521 | }) 522 | 523 | // subject remembers last 2 emits by the observer for an hour. 524 | observer, subject := rx.Subject[string](time.Hour, 2) 525 | merged.Tap(observer).Go() 526 | 527 | // Sees all emits by the subject 528 | subject.Println().Go().Wait() 529 | 530 | // Sees only last 2 emits 531 | subject.Println().Go().Wait() 532 | 533 | // Sees only last 2 emits 534 | subject.Println().Go().Wait() 535 | 536 | // Output: 537 | // content of "https://google.com" 538 | // content of "https://reactivego.io" 539 | // content of "https://github.com/reactivego" 540 | // content of "https://reactivego.io" 541 | // content of "https://github.com/reactivego" 542 | // content of "https://reactivego.io" 543 | // content of "https://github.com/reactivego" 544 | } 545 | 546 | func Example_commandPattern() { 547 | type Data struct { 548 | Id string 549 | Val int 550 | } 551 | 552 | // collect is an example of a long running observable producing data. 553 | collect := func(id string) rx.Observable[Data] { 554 | take5 := rx.Interval[int](100 * time.Millisecond).Take(5) 555 | // use rx.Defer to have a scope per subscription available. 556 | return rx.Defer(func() rx.Observable[Data] { 557 | // this scope is for storing data per subscription 558 | // for example say you want to support retries, 559 | // then this scope will be active per retry. 560 | return rx.Map(take5, func(val int) Data { 561 | return Data{Id: id, Val: val} 562 | }) 563 | }) 564 | } 565 | 566 | // setup a channel to push commands into, where a command is an rx.Observable[Data] 567 | commands := make(chan rx.Observable[Data], 2) 568 | 569 | // now user MergeAll to run all commands in parallel and merge their output 570 | sub := rx.MergeAll(rx.Recv(commands)).Println().Go() 571 | 572 | // launch a bunch of commands. 573 | commands <- collect("hello") 574 | commands <- collect("world") 575 | 576 | // We need to make sure the command channel is closed. 577 | close(commands) 578 | 579 | // then wait for everything to play out 580 | sub.Wait() 581 | 582 | // Unordered output: 583 | // {hello 0} 584 | // {world 0} 585 | // {hello 1} 586 | // {world 1} 587 | // {hello 2} 588 | // {world 2} 589 | // {hello 3} 590 | // {world 3} 591 | // {hello 4} 592 | // {world 4} 593 | } 594 | -------------------------------------------------------------------------------- /exhaustall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | func ExhaustAll[T any](observable Observable[Observable[T]]) Observable[T] { 9 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 10 | var observers struct { 11 | sync.Mutex 12 | done bool 13 | count int32 14 | } 15 | observer := func(next T, err error, done bool) { 16 | observers.Lock() 17 | defer observers.Unlock() 18 | if !observers.done { 19 | switch { 20 | case !done: 21 | observe(next, nil, false) 22 | case err != nil: 23 | observers.done = true 24 | var zero T 25 | observe(zero, err, true) 26 | default: 27 | if atomic.AddInt32(&observers.count, -1) == 0 { 28 | var zero T 29 | observe(zero, nil, true) 30 | } 31 | } 32 | } 33 | } 34 | exhauster := func(next Observable[T], err error, done bool) { 35 | if !done { 36 | if atomic.CompareAndSwapInt32(&observers.count, 1, 2) { 37 | next.AutoUnsubscribe()(observer, scheduler, subscriber) 38 | } 39 | } else { 40 | var zero T 41 | observer(zero, err, true) 42 | } 43 | } 44 | observers.count += 1 45 | observable.AutoUnsubscribe()(exhauster, scheduler, subscriber) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /exhaustmap.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func ExhaustMap[T, U any](observable Observable[T], project func(T) Observable[U]) Observable[U] { 4 | return ExhaustAll(Map(observable, project)) 5 | } 6 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Filter[T any](predicate func(T) bool) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | if done || predicate(next) { 8 | observe(next, err, done) 9 | } 10 | }, scheduler, subscriber) 11 | } 12 | } 13 | } 14 | 15 | func (observable Observable[T]) Filter(predicate func(T) bool) Observable[T] { 16 | return Filter[T](predicate)(observable) 17 | } 18 | -------------------------------------------------------------------------------- /first.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) First(schedulers ...Scheduler) (value T, err error) { 4 | err = observable.Take(1).Assign(&value).Wait(schedulers...) 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /fprint.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | func Fprint[T any](out io.Writer) Pipe[T] { 9 | return func(observable Observable[T]) Observable[T] { 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | observable(func(next T, err error, done bool) { 12 | if !done { 13 | fmt.Fprint(out, next) 14 | } 15 | observe(next, err, done) 16 | }, scheduler, subscriber) 17 | } 18 | } 19 | } 20 | 21 | func (observable Observable[T]) Fprint(out io.Writer) Observable[T] { 22 | return Fprint[T](out)(observable) 23 | } 24 | -------------------------------------------------------------------------------- /fprintf.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | func Fprintf[T any](out io.Writer, format string) Pipe[T] { 9 | return func(observable Observable[T]) Observable[T] { 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | observable(func(next T, err error, done bool) { 12 | if !done { 13 | fmt.Fprintf(out, format, next) 14 | } 15 | observe(next, err, done) 16 | }, scheduler, subscriber) 17 | } 18 | } 19 | } 20 | 21 | func (observable Observable[T]) Fprintf(out io.Writer, format string) Observable[T] { 22 | return Fprintf[T](out, format)(observable) 23 | } 24 | -------------------------------------------------------------------------------- /fprintln.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | func Fprintln[T any](out io.Writer) Pipe[T] { 9 | return func(observable Observable[T]) Observable[T] { 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | observable(func(next T, err error, done bool) { 12 | if !done { 13 | fmt.Fprintln(out, next) 14 | } 15 | observe(next, err, done) 16 | }, scheduler, subscriber) 17 | } 18 | } 19 | } 20 | 21 | func (observable Observable[T]) Fprintln(out io.Writer) Observable[T] { 22 | return Fprintln[T](out)(observable) 23 | } 24 | -------------------------------------------------------------------------------- /from.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func From[T any](slice ...T) Observable[T] { 4 | if len(slice) == 0 { 5 | return Empty[T]() 6 | } 7 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 8 | task := func(index int, again func(next int)) { 9 | if subscriber.Subscribed() { 10 | if index < len(slice) { 11 | observe(slice[index], nil, false) 12 | if subscriber.Subscribed() { 13 | again(index + 1) 14 | } 15 | } else { 16 | var zero T 17 | observe(zero, nil, true) 18 | } 19 | } 20 | } 21 | runner := scheduler.ScheduleLoop(0, task) 22 | subscriber.OnUnsubscribe(runner.Cancel) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Go subscribes to the observable and starts execution on a separate goroutine. 4 | // It ignores all emissions from the observable sequence, making it useful when you 5 | // only care about side effects and not the actual values. By default, it uses the 6 | // Goroutine scheduler, but an optional scheduler can be provided. Returns a 7 | // Subscription that can be used to cancel the subscription when no longer needed. 8 | func (observable Observable[T]) Go(schedulers ...Scheduler) Subscription { 9 | if len(schedulers) == 0 { 10 | return observable.Subscribe(Ignore[T](), Goroutine) 11 | } else { 12 | return observable.Subscribe(Ignore[T](), schedulers[0]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reactivego/rx 2 | 3 | go 1.23.0 4 | 5 | require github.com/reactivego/scheduler v0.1.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/reactivego/scheduler v0.1.2 h1:SFnTs9OE9b/QQXbetqu9TGTbk1mhGjble5+kVvpSILc= 2 | github.com/reactivego/scheduler v0.1.2/go.mod h1:Kw99rMXR1WJr/FgeqQfGAWClsWWlVKXZCUwJ+iCXUtQ= 3 | -------------------------------------------------------------------------------- /interval.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "time" 4 | 5 | func Interval[T Integer | Float](interval time.Duration) Observable[T] { 6 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 7 | var i T 8 | task := func(again func(due time.Duration)) { 9 | if subscriber.Subscribed() { 10 | observe(i, nil, false) 11 | i++ 12 | if subscriber.Subscribed() { 13 | again(interval) 14 | } 15 | } 16 | } 17 | runner := scheduler.ScheduleFutureRecursive(interval, task) 18 | subscriber.OnUnsubscribe(runner.Cancel) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iter_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/reactivego/rx" 8 | ) 9 | 10 | // Additional integration test that combines multiple iterator functions 11 | func TestIter(t *testing.T) { 12 | t.Run("Pull to Values to Pull round trip", func(t *testing.T) { 13 | // Create a sequence 14 | seq := func(yield func(int) bool) { 15 | for i := 1; i <= 5; i++ { 16 | if !yield(i * 10) { 17 | break 18 | } 19 | } 20 | } 21 | 22 | // Convert to Observable, then back to an iterator, and then back to Observable 23 | obs1 := rx.Pull(seq) 24 | iter := obs1.Values() 25 | obs2 := rx.Pull(iter) 26 | 27 | // Check results 28 | var results []int 29 | err := obs2.Append(&results).Wait() 30 | 31 | if err != nil { 32 | t.Errorf("Expected no error, got %v", err) 33 | } 34 | 35 | expected := []int{10, 20, 30, 40, 50} 36 | if len(results) != len(expected) { 37 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 38 | } 39 | 40 | for i, v := range expected { 41 | if results[i] != v { 42 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 43 | } 44 | } 45 | }) 46 | 47 | t.Run("All iterator with transformation", func(t *testing.T) { 48 | nums := rx.From("apple", "banana", "cherry") 49 | 50 | // Use the iterator to create a formatted string 51 | var formattedItems []string 52 | for i, v := range nums.All() { 53 | formattedItems = append(formattedItems, fmt.Sprintf("%d: %s", i, v)) 54 | } 55 | 56 | expected := []string{ 57 | "0: apple", 58 | "1: banana", 59 | "2: cherry", 60 | } 61 | 62 | if len(formattedItems) != len(expected) { 63 | t.Errorf("Expected %v items, got %v", len(expected), len(formattedItems)) 64 | } 65 | 66 | for i, v := range expected { 67 | if formattedItems[i] != v { 68 | t.Errorf("Expected %v at index %v, got %v", v, i, formattedItems[i]) 69 | } 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /last.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Last(schedulers ...Scheduler) (value T, err error) { 4 | err = observable.Assign(&value).Wait(schedulers...) 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Map[T, U any](observable Observable[T], project func(T) U) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | observable(func(next T, err error, done bool) { 6 | var mapped U 7 | if !done { 8 | mapped = project(next) 9 | } 10 | observe(mapped, err, done) 11 | }, scheduler, subscriber) 12 | } 13 | } 14 | 15 | func (observable Observable[T]) Map(project func(T) any) Observable[any] { 16 | return Map(observable, project) 17 | } 18 | -------------------------------------------------------------------------------- /mape.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func MapE[T, U any](observable Observable[T], project func(T) (U, error)) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | observable(func(next T, err error, done bool) { 6 | var mapped U 7 | if !done { 8 | mapped, err = project(next) 9 | } 10 | observe(mapped, err, done || err != nil) 11 | }, scheduler, subscriber) 12 | } 13 | } 14 | 15 | func (observable Observable[T]) MapE(project func(T) (any, error)) Observable[any] { 16 | return MapE(observable, project) 17 | } 18 | -------------------------------------------------------------------------------- /marshal.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Marshal(marshal func(any) ([]byte, error)) Observable[[]byte] { 4 | return func(observe Observer[[]byte], scheduler Scheduler, subscriber Subscriber) { 5 | observer := func(next T, err error, done bool) { 6 | if !done { 7 | data, err := marshal(next) 8 | observe(data, err, err != nil) 9 | } else { 10 | observe(nil, err, true) 11 | } 12 | } 13 | observable(observer, scheduler, subscriber) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /maxbuffersize.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // MaxBufferSizeOption is a function type used for configuring the maximum buffer size 4 | // of an observable stream. 5 | type MaxBufferSizeOption = func(*int) 6 | 7 | // WithMaxBufferSize creates a MaxBufferSizeOption that sets the maximum buffer size to n. 8 | // This option is typically used when creating new observables to control memory usage. 9 | func WithMaxBufferSize(n int) MaxBufferSizeOption { 10 | return func(MaxBufferSize *int) { 11 | *MaxBufferSize = n 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /merge.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Merge[T any](observables ...Observable[T]) Observable[T] { 4 | if len(observables) == 0 { 5 | return Empty[T]() 6 | } 7 | return observables[0].MergeWith(observables[1:]...) 8 | } 9 | -------------------------------------------------------------------------------- /mergeall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | func MergeAll[T any](observable Observable[Observable[T]]) Observable[T] { 9 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 10 | var merge struct { 11 | sync.Mutex 12 | done bool 13 | count atomic.Int32 14 | } 15 | merger := func(next T, err error, done bool) { 16 | merge.Lock() 17 | defer merge.Unlock() 18 | if !merge.done { 19 | switch { 20 | case !done: 21 | observe(next, nil, false) 22 | case err != nil: 23 | merge.done = true 24 | var zero T 25 | observe(zero, err, true) 26 | default: 27 | if merge.count.Add(-1) == 0 { 28 | var zero T 29 | observe(zero, nil, true) 30 | } 31 | } 32 | } 33 | } 34 | appender := func(next Observable[T], err error, done bool) { 35 | if !done { 36 | merge.count.Add(1) 37 | next.AutoUnsubscribe()(merger, scheduler, subscriber) 38 | } else { 39 | var zero T 40 | merger(zero, err, true) 41 | } 42 | } 43 | merge.count.Add(1) 44 | observable.AutoUnsubscribe()(appender, scheduler, subscriber) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mergemap.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func MergeMap[T, U any](observable Observable[T], project func(T) Observable[U]) Observable[U] { 4 | return MergeAll(Map(observable, project)) 5 | } 6 | -------------------------------------------------------------------------------- /mergewith.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "sync" 4 | 5 | func MergeWith[T any](others ...Observable[T]) Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | if len(others) == 0 { 8 | return observable 9 | } 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | var merge struct { 12 | sync.Mutex 13 | done bool 14 | count int 15 | } 16 | merger := func(next T, err error, done bool) { 17 | merge.Lock() 18 | defer merge.Unlock() 19 | if !merge.done { 20 | switch { 21 | case !done: 22 | observe(next, nil, false) 23 | case err != nil: 24 | merge.done = true 25 | var zero T 26 | observe(zero, err, true) 27 | default: 28 | if merge.count--; merge.count == 0 { 29 | var zero T 30 | observe(zero, nil, true) 31 | } 32 | } 33 | } 34 | } 35 | merge.count = 1 + len(others) 36 | observable.AutoUnsubscribe()(merger, scheduler, subscriber) 37 | for _, other := range others { 38 | if subscriber.Subscribed() { 39 | other.AutoUnsubscribe()(merger, scheduler, subscriber) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | func (observable Observable[T]) MergeWith(others ...Observable[T]) Observable[T] { 47 | return MergeWith(others...)(observable) 48 | } 49 | -------------------------------------------------------------------------------- /multicast.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "sync" 4 | 5 | // Multicast returns both an Observer and and Observable. The returned Observer is 6 | // used to send items into the Multicast. The returned Observable is used to subscribe 7 | // to the Multicast. The Multicast multicasts items send through the Observer to every 8 | // Subscriber of the Observable. 9 | // 10 | // size size of the item buffer, number of items kept to replay to a new Subscriber. 11 | // 12 | // Backpressure handling depends on the sign of the size argument. For positive size the 13 | // multicast will block when one of the subscribers lets the buffer fill up. For negative 14 | // size the multicast will drop items on the blocking subscriber, allowing the others to 15 | // keep on receiving values. For hot observables dropping is preferred. 16 | func Multicast[T any](size int) (Observer[T], Observable[T]) { 17 | var multicast struct { 18 | sync.Mutex 19 | channels []chan any 20 | err error 21 | done bool 22 | } 23 | drop := (size < 0) 24 | if size < 0 { 25 | size = -size 26 | } 27 | observer := func(next T, err error, done bool) { 28 | multicast.Lock() 29 | defer multicast.Unlock() 30 | if !multicast.done { 31 | for _, ch := range multicast.channels { 32 | switch { 33 | case !done: 34 | if drop { 35 | select { 36 | case ch <- next: 37 | default: 38 | // dropping 39 | } 40 | } else { 41 | ch <- next 42 | } 43 | case err != nil: 44 | if drop { 45 | select { 46 | case ch <- err: 47 | default: 48 | // dropping 49 | } 50 | } else { 51 | ch <- err 52 | } 53 | close(ch) 54 | default: 55 | close(ch) 56 | } 57 | } 58 | multicast.err = err 59 | multicast.done = done 60 | } 61 | } 62 | observable := func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 63 | addChannel := func() <-chan any { 64 | multicast.Lock() 65 | defer multicast.Unlock() 66 | var ch chan any 67 | if !multicast.done { 68 | ch = make(chan any, size) 69 | multicast.channels = append(multicast.channels, ch) 70 | } else if multicast.err != nil { 71 | ch = make(chan any, 1) 72 | ch <- multicast.err 73 | close(ch) 74 | } else { 75 | ch = make(chan any) 76 | close(ch) 77 | } 78 | return ch 79 | } 80 | removeChannel := func(ch <-chan any) func() { 81 | return func() { 82 | multicast.Lock() 83 | defer multicast.Unlock() 84 | if !multicast.done && ch != nil { 85 | channels := multicast.channels[:0] 86 | for _, c := range multicast.channels { 87 | if ch != c { 88 | channels = append(channels, c) 89 | } 90 | } 91 | multicast.channels = channels 92 | } 93 | } 94 | } 95 | ch := addChannel() 96 | Recv(ch)(observe.AsObserver(), scheduler, subscriber) 97 | subscriber.OnUnsubscribe(removeChannel(ch)) 98 | } 99 | return observer, observable 100 | } 101 | -------------------------------------------------------------------------------- /must.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Must[T any](t T, err error) T { 4 | if err != nil { 5 | panic(err) 6 | } 7 | return t 8 | } 9 | -------------------------------------------------------------------------------- /never.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Never[T any]() Observable[T] { 4 | return func(Observer[T], Scheduler, Subscriber) {} 5 | } 6 | -------------------------------------------------------------------------------- /observable.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | type Observable[T any] func(Observer[T], Scheduler, Subscriber) 4 | -------------------------------------------------------------------------------- /observer.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "errors" 4 | 5 | // ErrTypecastFailed is returned when a type conversion fails during observer operations, 6 | // typically when using AsObserver() to convert between generic and typed observers. 7 | var ErrTypecastFailed = errors.Join(Err, errors.New("typecast failed")) 8 | 9 | // Observer[T] represents a consumer of values delivered by an Observable. 10 | // It is implemented as a function that takes three parameters: 11 | // - next: the next value emitted by the Observable 12 | // - err: any error that occurred during emission (nil if no error) 13 | // - done: a boolean indicating whether the Observable has completed 14 | // 15 | // Observers follow the reactive pattern by receiving a stream of events 16 | // (values, errors, or completion signals) and reacting to them accordingly. 17 | type Observer[T any] func(next T, err error, done bool) 18 | 19 | // Ignore creates an Observer that simply discards any emissions 20 | // from an Observable. It is useful when you need to create an 21 | // Observer but don't care about its values. 22 | func Ignore[T any]() Observer[T] { 23 | return func(next T, err error, done bool) {} 24 | } 25 | 26 | // AsObserver converts an Observer of type `any` to an Observer of a specific type T. 27 | // This allows adapting a generic Observer to a more specific type context. 28 | func AsObserver[T any](observe Observer[any]) Observer[T] { 29 | return func(next T, err error, done bool) { 30 | observe(next, err, done) 31 | } 32 | } 33 | 34 | // AsObserver converts a typed Observer[T] to a generic Observer[any]. It 35 | // handles type conversion from 'any' back to T, and will emit an 36 | // ErrTypecastFailed error when conversion fails. 37 | func (observe Observer[T]) AsObserver() Observer[any] { 38 | return func(next any, err error, done bool) { 39 | if !done { 40 | if nextT, ok := next.(T); ok { 41 | observe(nextT, err, done) 42 | } else { 43 | var zero T 44 | observe(zero, ErrTypecastFailed, true) 45 | } 46 | } else { 47 | var zero T 48 | observe(zero, err, true) 49 | } 50 | } 51 | } 52 | 53 | // Next sends a new value to the Observer. This is a convenience method that 54 | // handles the common case of emitting a new value without errors or completion 55 | // signals. 56 | func (observe Observer[T]) Next(next T) { 57 | observe(next, nil, false) 58 | } 59 | 60 | // Done signals that the Observable has completed emitting values, 61 | // optionally with an error. If err is nil, it indicates normal completion. 62 | // If err is non-nil, it indicates that the Observable terminated with an error. 63 | // 64 | // After Done is called, the Observable will not emit any more values, 65 | // regardless of whether the completion was successful or due to an error. 66 | func (observe Observer[T]) Done(err error) { 67 | var zero T 68 | observe(zero, err, true) 69 | } 70 | -------------------------------------------------------------------------------- /of.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Of[T any](value T) Observable[T] { 4 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 5 | task := func(index int, again func(next int)) { 6 | if subscriber.Subscribed() { 7 | if index == 0 { 8 | observe(value, nil, false) 9 | if subscriber.Subscribed() { 10 | again(1) 11 | } 12 | } else { 13 | var zero T 14 | observe(zero, nil, true) 15 | } 16 | } 17 | } 18 | runner := scheduler.ScheduleLoop(0, task) 19 | subscriber.OnUnsubscribe(runner.Cancel) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /oncomplete.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func OnComplete[T any](onComplete func()) Pipe[T] { 4 | return Tap[T](func(next T, err error, done bool) { 5 | if done && err == nil { 6 | onComplete() 7 | } 8 | }) 9 | } 10 | 11 | func (observable Observable[T]) OnComplete(f func()) Observable[T] { 12 | return OnComplete[T](f)(observable) 13 | } 14 | -------------------------------------------------------------------------------- /ondone.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func OnDone[T any](onDone func(error)) Pipe[T] { 4 | return Tap[T](func(next T, err error, done bool) { 5 | if done { 6 | onDone(err) 7 | } 8 | }) 9 | } 10 | 11 | func (observable Observable[T]) OnDone(f func(error)) Observable[T] { 12 | return OnDone[T](f)(observable) 13 | } 14 | -------------------------------------------------------------------------------- /onerror.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func OnError[T any](onError func(error)) Pipe[T] { 4 | return Tap[T](func(next T, err error, done bool) { 5 | if done && err != nil { 6 | onError(err) 7 | } 8 | }) 9 | } 10 | 11 | func (observable Observable[T]) OnError(f func(error)) Observable[T] { 12 | return OnError[T](f)(observable) 13 | } 14 | -------------------------------------------------------------------------------- /onnext.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func OnNext[T any](onNext func(T)) Pipe[T] { 4 | return Tap[T](func(next T, err error, done bool) { 5 | if !done { 6 | onNext(next) 7 | } 8 | }) 9 | } 10 | 11 | func (observable Observable[T]) OnNext(f func(T)) Observable[T] { 12 | return OnNext(f)(observable) 13 | } 14 | -------------------------------------------------------------------------------- /passthrough.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Passthrough[T any]() Pipe[T] { 4 | // Pipe scope 5 | return func(observable Observable[T]) Observable[T] { 6 | // Operator scope 7 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 8 | // Subscribe scope 9 | observable(func(next T, err error, done bool) { 10 | // Observe scope 11 | observe(next, err, done) 12 | }, scheduler, subscriber) 13 | } 14 | } 15 | } 16 | 17 | func (observable Observable[T]) Passthrough() Observable[T] { 18 | return Passthrough[T]()(observable) 19 | } 20 | -------------------------------------------------------------------------------- /pipe.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | type Pipe[T any] func(Observable[T]) Observable[T] 4 | 5 | func (observable Observable[T]) Pipe(segments ...Pipe[T]) Observable[T] { 6 | for _, s := range segments { 7 | observable = s(observable) 8 | } 9 | return observable 10 | } 11 | -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "fmt" 4 | 5 | func Print[T any]() Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 8 | observable(func(next T, err error, done bool) { 9 | if !done { 10 | fmt.Print(next) 11 | } 12 | observe(next, err, done) 13 | }, scheduler, subscriber) 14 | } 15 | } 16 | } 17 | 18 | func (observable Observable[T]) Print() Observable[T] { 19 | return Print[T]()(observable) 20 | } 21 | -------------------------------------------------------------------------------- /printf.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "fmt" 4 | 5 | func Printf[T any](format string) Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 8 | observable(func(next T, err error, done bool) { 9 | if !done { 10 | fmt.Printf(format, next) 11 | } 12 | observe(next, err, done) 13 | }, scheduler, subscriber) 14 | } 15 | } 16 | } 17 | 18 | func (observable Observable[T]) Printf(format string) Observable[T] { 19 | return Printf[T](format)(observable) 20 | } 21 | -------------------------------------------------------------------------------- /println.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "fmt" 4 | 5 | func Println[T any]() Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 8 | observable(func(next T, err error, done bool) { 9 | if !done { 10 | fmt.Println(next) 11 | } 12 | observe(next, err, done) 13 | }, scheduler, subscriber) 14 | } 15 | } 16 | } 17 | 18 | func (observable Observable[T]) Println() Observable[T] { 19 | return Println[T]()(observable) 20 | } 21 | -------------------------------------------------------------------------------- /publish.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Publish returns a multicasting Observable[T] for an underlying Observable[T] as a Connectable[T] type. 4 | func (observable Observable[T]) Publish() Connectable[T] { 5 | observe, multicaster := Multicast[T](1) 6 | connector := func(scheduler Scheduler, subscriber Subscriber) { 7 | observer := func(next T, err error, done bool) { 8 | observe(next, err, done) 9 | if done { 10 | subscriber.Unsubscribe() 11 | } 12 | } 13 | observable(observer, scheduler, subscriber) 14 | } 15 | return Connectable[T]{Observable: multicaster, Connector: connector} 16 | } 17 | -------------------------------------------------------------------------------- /pull.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "iter" 4 | 5 | func Pull[T any](seq iter.Seq[T]) Observable[T] { 6 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 7 | next, stop := iter.Pull(seq) 8 | task := func(again func()) { 9 | if subscriber.Subscribed() { 10 | next, valid := next() 11 | if subscriber.Subscribed() { 12 | if valid { 13 | observe(next, nil, false) 14 | if subscriber.Subscribed() { 15 | again() 16 | } 17 | } else { 18 | observe(next, nil, true) 19 | } 20 | } 21 | } 22 | } 23 | runner := scheduler.ScheduleRecursive(task) 24 | subscriber.OnUnsubscribe(runner.Cancel) 25 | subscriber.OnUnsubscribe(stop) 26 | } 27 | } 28 | 29 | func Pull2[T, U any](seq iter.Seq2[T, U]) Observable[Tuple2[T, U]] { 30 | return func(observe Observer[Tuple2[T, U]], scheduler Scheduler, subscriber Subscriber) { 31 | next, stop := iter.Pull2(seq) 32 | task := func(again func()) { 33 | if subscriber.Subscribed() { 34 | first, second, valid := next() 35 | if subscriber.Subscribed() { 36 | if valid { 37 | observe(Tuple2[T, U]{first, second}, nil, false) 38 | if subscriber.Subscribed() { 39 | again() 40 | } 41 | } else { 42 | var zero Tuple2[T, U] 43 | observe(zero, nil, true) 44 | } 45 | } 46 | } 47 | } 48 | runner := scheduler.ScheduleRecursive(task) 49 | subscriber.OnUnsubscribe(runner.Cancel) 50 | subscriber.OnUnsubscribe(stop) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pull_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reactivego/rx" 7 | ) 8 | 9 | func TestPull(t *testing.T) { 10 | t.Run("Basic Pull with Seq", func(t *testing.T) { 11 | // Create a simple iterator sequence 12 | seq := func(yield func(int) bool) { 13 | for i := 1; i <= 5; i++ { 14 | if !yield(i) { 15 | break 16 | } 17 | } 18 | } 19 | 20 | // Convert to an Observable using Pull 21 | obs := rx.Pull(seq) 22 | 23 | // Check results 24 | var results []int 25 | err := obs.Append(&results).Wait() 26 | 27 | if err != nil { 28 | t.Errorf("Expected no error, got %v", err) 29 | } 30 | 31 | expected := []int{1, 2, 3, 4, 5} 32 | if len(results) != len(expected) { 33 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 34 | } 35 | 36 | for i, v := range expected { 37 | if results[i] != v { 38 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 39 | } 40 | } 41 | }) 42 | 43 | t.Run("Pull with early termination", func(t *testing.T) { 44 | // Create a sequence that would yield many values 45 | seq := func(yield func(int) bool) { 46 | for i := 1; i <= 100; i++ { 47 | if !yield(i) { 48 | break 49 | } 50 | } 51 | } 52 | 53 | // Convert to an Observable using Pull and take only a few 54 | obs := rx.Pull(seq).Take(3) 55 | 56 | // Check results 57 | var results []int 58 | err := obs.Append(&results).Wait() 59 | 60 | if err != nil { 61 | t.Errorf("Expected no error, got %v", err) 62 | } 63 | 64 | expected := []int{1, 2, 3} 65 | if len(results) != len(expected) { 66 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 67 | } 68 | }) 69 | 70 | t.Run("Empty sequence", func(t *testing.T) { 71 | // Create an empty sequence 72 | seq := func(yield func(int) bool) { 73 | // No yields 74 | } 75 | 76 | // Convert to an Observable using Pull 77 | obs := rx.Pull(seq) 78 | 79 | // Check results 80 | var results []int 81 | err := obs.Append(&results).Wait() 82 | 83 | if err != nil { 84 | t.Errorf("Expected no error, got %v", err) 85 | } 86 | 87 | if len(results) != 0 { 88 | t.Errorf("Expected 0 items, got %v", len(results)) 89 | } 90 | }) 91 | } 92 | 93 | func TestPull2(t *testing.T) { 94 | t.Run("Basic Pull2 with Seq2", func(t *testing.T) { 95 | // Create a simple key-value iterator sequence 96 | seq := func(yield func(string, int) bool) { 97 | data := map[string]int{ 98 | "one": 1, 99 | "two": 2, 100 | "three": 3, 101 | } 102 | 103 | for k, v := range data { 104 | if !yield(k, v) { 105 | break 106 | } 107 | } 108 | } 109 | 110 | // Convert to an Observable using Pull2 111 | obs := rx.Pull2(seq) 112 | 113 | // Check results 114 | var results []rx.Tuple2[string, int] 115 | err := obs.Append(&results).Wait() 116 | 117 | if err != nil { 118 | t.Errorf("Expected no error, got %v", err) 119 | } 120 | 121 | if len(results) != 3 { 122 | t.Errorf("Expected 3 items, got %v", len(results)) 123 | } 124 | 125 | // Convert results to a map for easy checking 126 | resultMap := make(map[string]int) 127 | for _, tuple := range results { 128 | resultMap[tuple.First] = tuple.Second 129 | } 130 | 131 | expectedMap := map[string]int{ 132 | "one": 1, 133 | "two": 2, 134 | "three": 3, 135 | } 136 | 137 | for k, v := range expectedMap { 138 | if resultMap[k] != v { 139 | t.Errorf("Expected %v for key %v, got %v", v, k, resultMap[k]) 140 | } 141 | } 142 | }) 143 | 144 | t.Run("Pull2 with filtering", func(t *testing.T) { 145 | // Create a simple key-value iterator sequence 146 | seq := func(yield func(string, int) bool) { 147 | data := map[string]int{ 148 | "one": 1, 149 | "two": 2, 150 | "three": 3, 151 | "four": 4, 152 | "five": 5, 153 | } 154 | 155 | for k, v := range data { 156 | if !yield(k, v) { 157 | break 158 | } 159 | } 160 | } 161 | 162 | // Convert to an Observable using Pull2 and filter even values 163 | obs := rx.Pull2(seq).Filter(func(tuple rx.Tuple2[string, int]) bool { 164 | return tuple.Second%2 == 0 165 | }) 166 | 167 | // Check results 168 | var results []rx.Tuple2[string, int] 169 | err := obs.Append(&results).Wait() 170 | 171 | if err != nil { 172 | t.Errorf("Expected no error, got %v", err) 173 | } 174 | 175 | // Map entries should only contain "two" and "four" 176 | if len(results) != 2 { 177 | t.Errorf("Expected 2 items, got %v", len(results)) 178 | } 179 | 180 | // Convert results to a map for easy checking 181 | resultMap := make(map[string]int) 182 | // Manually iterate over results since ToMap is not available 183 | for _, tuple := range results { 184 | resultMap[tuple.First] = tuple.Second 185 | } 186 | 187 | expectedMap := map[string]int{ 188 | "two": 2, 189 | "four": 4, 190 | } 191 | 192 | for k, v := range expectedMap { 193 | if resultMap[k] != v { 194 | t.Errorf("Expected %v for key %v, got %v", v, k, resultMap[k]) 195 | } 196 | } 197 | }) 198 | 199 | t.Run("Empty sequence with Pull2", func(t *testing.T) { 200 | // Create an empty sequence 201 | seq := func(yield func(int, string) bool) { 202 | // No yields 203 | } 204 | 205 | // Convert to an Observable using Pull2 206 | obs := rx.Pull2(seq) 207 | 208 | // Check results 209 | var results []rx.Tuple2[int, string] 210 | err := obs.Append(&results).Wait() 211 | 212 | if err != nil { 213 | t.Errorf("Expected no error, got %v", err) 214 | } 215 | 216 | if len(results) != 0 { 217 | t.Errorf("Expected 0 items, got %v", len(results)) 218 | } 219 | }) 220 | } 221 | -------------------------------------------------------------------------------- /race.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Race[T any](observables ...Observable[T]) Observable[T] { 4 | if len(observables) == 0 { 5 | return Empty[T]() 6 | } 7 | return observables[0].RaceWith(observables[1:]...) 8 | } 9 | -------------------------------------------------------------------------------- /racewith.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "sync" 4 | 5 | func RaceWith[T any](others ...Observable[T]) Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | if len(others) == 0 { 8 | return observable 9 | } 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | var race struct { 12 | sync.Mutex 13 | subscribers []Subscriber 14 | } 15 | race.subscribers = make([]Subscriber, 1+len(others)) 16 | for i := range race.subscribers { 17 | race.subscribers[i] = subscriber.Add() 18 | } 19 | subscribe := func(subscriber Subscriber) (Observer[T], Scheduler, Subscriber) { 20 | observer := func(next T, err error, done bool) { 21 | race.Lock() 22 | defer race.Unlock() 23 | if subscriber.Subscribed() { 24 | for i := range race.subscribers { 25 | if race.subscribers[i] != subscriber { 26 | race.subscribers[i].Unsubscribe() 27 | } 28 | } 29 | race.subscribers = nil 30 | observe(next, err, done) 31 | if done { 32 | subscriber.Unsubscribe() 33 | } 34 | } 35 | } 36 | return observer, scheduler, subscriber 37 | } 38 | race.Lock() 39 | defer race.Unlock() 40 | observable(subscribe(race.subscribers[0])) 41 | for i, other := range others { 42 | other(subscribe(race.subscribers[i+1])) 43 | } 44 | } 45 | } 46 | } 47 | 48 | func (observable Observable[T]) RaceWith(others ...Observable[T]) Observable[T] { 49 | return RaceWith[T](others...)(observable) 50 | } 51 | -------------------------------------------------------------------------------- /recv.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Recv[T any](ch <-chan T) Observable[T] { 4 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 5 | cancel := make(chan struct{}) 6 | runner := scheduler.ScheduleRecursive(func(again func()) { 7 | if !subscriber.Subscribed() { 8 | return 9 | } 10 | select { 11 | case next, ok := <-ch: 12 | if !subscriber.Subscribed() { 13 | return 14 | } 15 | if ok { 16 | err, ok := any(next).(error) 17 | if !ok { 18 | observe(next, nil, false) 19 | if !subscriber.Subscribed() { 20 | return 21 | } 22 | again() 23 | } else { 24 | var zero T 25 | observe(zero, err, true) 26 | } 27 | } else { 28 | var zero T 29 | observe(zero, nil, true) 30 | } 31 | case <-cancel: 32 | return 33 | } 34 | }) 35 | subscriber.OnUnsubscribe(func() { 36 | runner.Cancel() 37 | close(cancel) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /reduce.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Reduce[T, U any](observable Observable[T], seed U, accumulator func(acc U, next T) U) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | state := seed 6 | observer := func(next T, err error, done bool) { 7 | switch { 8 | case !done: 9 | state = accumulator(state, next) 10 | case err != nil: 11 | var zero U 12 | observe(zero, err, true) 13 | default: 14 | Of(state)(observe, scheduler, subscriber) 15 | } 16 | } 17 | observable(observer, scheduler, subscriber) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /reducee.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func ReduceE[T, U any](observable Observable[T], seed U, accumulator func(acc U, next T) (U, error)) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | state := seed 6 | observer := func(next T, err error, done bool) { 7 | switch { 8 | case !done: 9 | if state, err = accumulator(state, next); err != nil { 10 | var zero U 11 | observe(zero, err, true) 12 | } 13 | case err != nil: 14 | var zero U 15 | observe(zero, err, true) 16 | default: 17 | Of(state)(observe, scheduler, subscriber) 18 | } 19 | } 20 | observable(observer, scheduler, subscriber) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /refcount.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // RefCount converts a Connectable Observable into a standard Observable that automatically 9 | // connects when the first subscriber subscribes and disconnects when the last subscriber 10 | // unsubscribes. 11 | // 12 | // When the first subscriber subscribes to the resulting Observable, it automatically calls 13 | // Connect() on the source Connectable Observable. The connection is shared among all 14 | // subscribers. When the last subscriber unsubscribes, the connection is automatically 15 | // closed. 16 | // 17 | // This is useful for efficiently sharing expensive resources (like network connections) 18 | // among multiple subscribers. 19 | func (connectable Connectable[T]) RefCount() Observable[T] { 20 | var source struct { 21 | sync.Mutex 22 | refcount int32 23 | subscription *subscription 24 | } 25 | observable := func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 26 | source.Lock() 27 | if atomic.AddInt32(&source.refcount, 1) == 1 { 28 | source.subscription = newSubscription(scheduler) 29 | source.Unlock() 30 | connectable.Connector(scheduler, source.subscription) 31 | source.Lock() 32 | } 33 | source.Unlock() 34 | subscriber.OnUnsubscribe(func() { 35 | source.Lock() 36 | if atomic.AddInt32(&source.refcount, -1) == 0 { 37 | source.subscription.Unsubscribe() 38 | } 39 | source.Unlock() 40 | }) 41 | connectable.Observable(observe, scheduler, subscriber) 42 | } 43 | return observable 44 | } 45 | -------------------------------------------------------------------------------- /repeat.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "errors" 4 | 5 | var ErrRepeatCountInvalid = errors.Join(Err, errors.New("repeat count invalid")) 6 | 7 | // Repeat creates an Observable that emits the entire source sequence multiple times. 8 | // 9 | // Parameters: 10 | // - count: Optional. The number of repetitions: 11 | // - If omitted: The source Observable is repeated indefinitely 12 | // - If 0: Returns an empty Observable 13 | // - If negative: Returns an Observable that emits an error 14 | // - If multiple count values: Returns an Observable that emits an error 15 | // 16 | // The resulting Observable will subscribe to the source Observable repeatedly 17 | // each time the source completes, up to the specified count. 18 | func Repeat[T any](count ...int) Pipe[T] { 19 | return func(observable Observable[T]) Observable[T] { 20 | if len(count) == 1 && count[0] < 0 || len(count) > 1 { 21 | return Throw[T](ErrRepeatCountInvalid) 22 | } 23 | if len(count) == 1 && count[0] == 0 { 24 | return Empty[T]() 25 | } 26 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 27 | var repeated int 28 | var observer Observer[T] 29 | observer = func(next T, err error, done bool) { 30 | if !done || err != nil { 31 | observe(next, err, done) 32 | } else { 33 | repeated++ 34 | if len(count) == 0 || repeated < count[0] { 35 | observable(observer, scheduler, subscriber) 36 | } else { 37 | var zero T 38 | observe(zero, nil, true) 39 | } 40 | } 41 | } 42 | observable(observer, scheduler, subscriber) 43 | } 44 | } 45 | } 46 | 47 | // Repeat emits the items emitted by the source Observable repeatedly. 48 | // 49 | // Parameters: 50 | // - count: Optional. The number of repetitions: 51 | // - If omitted: The source Observable is repeated indefinitely 52 | // - If 0: Returns an empty Observable 53 | // - If negative: Returns an Observable that emits an error 54 | // - If multiple count values: Returns an Observable that emits an error 55 | // 56 | // The resulting Observable will subscribe to the source Observable repeatedly 57 | // each time the source completes, up to the specified count. 58 | func (observable Observable[T]) Repeat(count ...int) Observable[T] { 59 | return Repeat[T](count...)(observable) 60 | } 61 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "time" 4 | 5 | func Retry[T any](limit ...int) Pipe[T] { 6 | return func(observable Observable[T]) Observable[T] { 7 | backoff := func(int) time.Duration { return 1 * time.Millisecond } 8 | return observable.RetryTime(backoff, limit...) 9 | } 10 | } 11 | 12 | func (observable Observable[T]) Retry(limit ...int) Observable[T] { 13 | return Retry[T](limit...)(observable) 14 | } 15 | -------------------------------------------------------------------------------- /retrytime.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | func (observable Observable[T]) RetryTime(backoff func(int) time.Duration, limit ...int) Observable[T] { 9 | if len(limit) == 0 || limit[0] <= 0 { 10 | limit = []int{math.MaxInt} 11 | } 12 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 13 | var retry struct { 14 | observer Observer[T] 15 | count int 16 | subscriber Subscriber 17 | resubscribe func() 18 | } 19 | retry.observer = func(next T, err error, done bool) { 20 | switch { 21 | case !done: 22 | observe(next, nil, false) 23 | retry.count = 0 24 | case err != nil && backoff != nil && retry.count < limit[0]: 25 | retry.subscriber.Unsubscribe() 26 | scheduler.ScheduleFuture(backoff(retry.count), retry.resubscribe) 27 | retry.count++ 28 | default: 29 | observe(next, err, true) 30 | } 31 | } 32 | retry.resubscribe = func() { 33 | if subscriber.Subscribed() { 34 | retry.subscriber = subscriber.Add() 35 | observable(retry.observer, scheduler, retry.subscriber) 36 | } 37 | } 38 | retry.resubscribe() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sampletime.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // SampleTime emits the most recent item emitted by an Observable within periodic time intervals. 9 | func (observable Observable[T]) SampleTime(window time.Duration) Observable[T] { 10 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 11 | var sample struct { 12 | sync.Mutex 13 | at time.Time 14 | next T 15 | done bool 16 | } 17 | sampler := scheduler.ScheduleFutureRecursive(window, func(self func(time.Duration)) { 18 | if subscriber.Subscribed() { 19 | sample.Lock() 20 | if !sample.done { 21 | begin := scheduler.Now().Add(-window) 22 | if !sample.at.Before(begin) { 23 | observe(sample.next, nil, false) 24 | } 25 | if subscriber.Subscribed() { 26 | self(window) 27 | } 28 | } 29 | sample.Unlock() 30 | } 31 | }) 32 | subscriber.OnUnsubscribe(sampler.Cancel) 33 | observer := func(next T, err error, done bool) { 34 | if subscriber.Subscribed() { 35 | sample.Lock() 36 | sample.at = scheduler.Now() 37 | sample.next = next 38 | sample.done = done 39 | sample.Unlock() 40 | if done { 41 | var zero T 42 | observe(zero, err, true) 43 | } 44 | } 45 | } 46 | observable(observer, scheduler, subscriber) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scan.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Scan[T, U any](observable Observable[T], seed U, accumulator func(acc U, next T) U) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | state := seed 6 | observable(func(next T, err error, done bool) { 7 | if !done { 8 | state = accumulator(state, next) 9 | observe(state, nil, false) 10 | } else { 11 | var zero U 12 | observe(zero, err, true) 13 | } 14 | }, scheduler, subscriber) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scane.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func ScanE[T, U any](observable Observable[T], seed U, accumulator func(acc U, next T) (U, error)) Observable[U] { 4 | return func(observe Observer[U], scheduler Scheduler, subscriber Subscriber) { 5 | state := seed 6 | observable(func(next T, err error, done bool) { 7 | if !done { 8 | state, err = accumulator(state, next) 9 | observe(state, err, err != nil) 10 | } else { 11 | var zero U 12 | observe(zero, err, true) 13 | } 14 | }, scheduler, subscriber) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scheduler.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "github.com/reactivego/scheduler" 4 | 5 | type Scheduler = scheduler.Scheduler 6 | 7 | type ConcurrentScheduler = scheduler.ConcurrentScheduler 8 | 9 | var Goroutine = scheduler.Goroutine 10 | 11 | var NewScheduler = scheduler.New 12 | -------------------------------------------------------------------------------- /send.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Send[T any](ch chan<- T) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | if !done { 8 | ch <- next 9 | } 10 | observe(next, err, done) 11 | }, scheduler, subscriber) 12 | } 13 | } 14 | } 15 | 16 | func (observable Observable[T]) Send(ch chan<- T) Observable[T] { 17 | return Send(ch)(observable) 18 | } 19 | -------------------------------------------------------------------------------- /share.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Share returns a new Observable that multicasts (shares) the original 4 | // Observable. As long as there is at least one Subscriber this Observable 5 | // will be subscribed and emitting data. When all subscribers have unsubscribed 6 | // it will unsubscribe from the source Observable. Because the Observable is 7 | // multicasting it makes the stream hot. 8 | // 9 | // This method is useful when you have an Observable that is expensive to 10 | // create or has side-effects, but you want to share the results of that 11 | // Observable with multiple subscribers. By using `Share`, you can avoid 12 | // creating multiple instances of the Observable and ensure that all 13 | // subscribers receive the same data. 14 | func (observable Observable[T]) Share() Observable[T] { 15 | return observable.Publish().RefCount() 16 | } 17 | -------------------------------------------------------------------------------- /skip.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Skip[T any](n int) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | i := 0 7 | observable(func(next T, err error, done bool) { 8 | if done || i >= n { 9 | observe(next, err, done) 10 | } 11 | i++ 12 | }, scheduler, subscriber) 13 | } 14 | } 15 | } 16 | 17 | func (observable Observable[T]) Skip(n int) Observable[T] { 18 | return Skip[T](n)(observable) 19 | } 20 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Slice(schedulers ...Scheduler) (slice []T, err error) { 4 | err = observable.Append(&slice).Wait(schedulers...) 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /startwith.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func StartWith[T any](values ...T) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return From(values...).ConcatWith(observable) 6 | } 7 | } 8 | 9 | func (observable Observable[T]) StartWith(values ...T) Observable[T] { 10 | return StartWith(values...)(observable) 11 | } 12 | -------------------------------------------------------------------------------- /subject.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "runtime" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | var ErrOutOfSubjectSubscriptions = errors.Join(Err, errors.New("out of subject subscriptions")) 13 | 14 | // Subject returns both an Observer and and Observable. The returned Observer is 15 | // used to send items into the Subject. The returned Observable is used to subscribe 16 | // to the Subject. The Subject multicasts items send through the Observer to every 17 | // Subscriber of the Observable. 18 | // 19 | // age max age to keep items in order to replay them to a new Subscriber (0 = no max age). 20 | // [size] size of the item buffer, number of items kept to replay to a new Subscriber. 21 | // [cap] capacity of the item buffer, number of items that can be observed before blocking. 22 | // [scap] capacity of the subscription list, max number of simultaneous subscribers. 23 | func Subject[T any](age time.Duration, capacity ...int) (Observer[T], Observable[T]) { 24 | const ( 25 | ms = time.Millisecond 26 | us = time.Microsecond 27 | ) 28 | 29 | // cursor 30 | const ( 31 | maxuint64 uint64 = math.MaxUint64 // park unused cursor at maxuint64 32 | ) 33 | 34 | // state 35 | const ( 36 | active uint64 = iota 37 | canceled 38 | closing 39 | closed 40 | ) 41 | 42 | // access 43 | const ( 44 | unlocked uint32 = iota 45 | locked 46 | ) 47 | 48 | make := func(age time.Duration, capacity ...int) *_buffer[T] { 49 | size, cap, scap := 0, 0, 32 50 | switch { 51 | case len(capacity) == 3: 52 | size, cap, scap = capacity[0], capacity[1], capacity[2] 53 | case len(capacity) == 2: 54 | size, cap = capacity[0], capacity[1] 55 | case len(capacity) == 1: 56 | size = capacity[0] 57 | } 58 | if size < 0 { 59 | size = 0 60 | } 61 | if cap < size { 62 | cap = size 63 | } 64 | if cap == 0 { 65 | cap = 1 66 | } 67 | length := uint64(1) << uint(math.Ceil(math.Log2(float64(cap)))) // MUST(keep < length) 68 | 69 | if scap < 1 { 70 | scap = 1 71 | } 72 | buf := &_buffer[T]{ 73 | age: age, 74 | keep: uint64(size), 75 | mod: length - 1, 76 | size: length, 77 | 78 | items: make([]_item[T], length), 79 | end: length, 80 | subscriptions: _subscriptions{ 81 | entries: make([]_subscription, 0, scap), 82 | }, 83 | } 84 | buf.subscriptions.Cond = sync.NewCond(&buf.subscriptions.Mutex) 85 | return buf 86 | } 87 | buf := make(age, capacity...) 88 | 89 | accessSubscriptions := func(access func([]_subscription)) bool { 90 | gosched := false 91 | for !atomic.CompareAndSwapUint32(&buf.subscriptions.access, unlocked, locked) { 92 | runtime.Gosched() 93 | gosched = true 94 | } 95 | access(buf.subscriptions.entries) 96 | atomic.StoreUint32(&buf.subscriptions.access, unlocked) 97 | return gosched 98 | } 99 | 100 | send := func(value T) { 101 | for buf.commit == buf.end { 102 | full := false 103 | scheduler := Scheduler(nil) 104 | gosched := accessSubscriptions(func(subscriptions []_subscription) { 105 | slowest := maxuint64 106 | for i := range subscriptions { 107 | current := atomic.LoadUint64(&subscriptions[i].cursor) 108 | if current < slowest { 109 | slowest = current 110 | scheduler = subscriptions[i].scheduler 111 | } 112 | } 113 | end := atomic.LoadUint64(&buf.end) 114 | if atomic.LoadUint64(&buf.begin) < slowest && slowest <= end { 115 | if slowest+buf.keep > end { 116 | slowest = end - buf.keep + 1 117 | } 118 | atomic.StoreUint64(&buf.begin, slowest) 119 | atomic.StoreUint64(&buf.end, slowest+buf.size) 120 | } else { 121 | if slowest == maxuint64 { // no subscriptions 122 | atomic.AddUint64(&buf.begin, 1) 123 | atomic.AddUint64(&buf.end, 1) 124 | } else { 125 | full = true 126 | } 127 | } 128 | }) 129 | if full { 130 | if !gosched { 131 | if scheduler != nil { 132 | scheduler.Gosched() 133 | } else { 134 | runtime.Gosched() 135 | } 136 | } 137 | if atomic.LoadUint64(&buf.state) != active { 138 | return // buffer no longer active 139 | } 140 | } 141 | } 142 | buf.items[buf.commit&buf.mod] = _item[T]{Value: value, At: time.Now()} 143 | atomic.AddUint64(&buf.commit, 1) 144 | buf.subscriptions.Broadcast() 145 | } 146 | 147 | close := func(err error) { 148 | if atomic.CompareAndSwapUint64(&buf.state, active, closing) { 149 | buf.err = err 150 | if atomic.CompareAndSwapUint64(&buf.state, closing, closed) { 151 | accessSubscriptions(func(subscriptions []_subscription) { 152 | for i := range subscriptions { 153 | atomic.CompareAndSwapUint64(&subscriptions[i].state, active, closed) 154 | } 155 | }) 156 | } 157 | } 158 | buf.subscriptions.Broadcast() 159 | } 160 | 161 | observer := func(next T, err error, done bool) { 162 | if atomic.LoadUint64(&buf.state) == active { 163 | if !done { 164 | send(next) 165 | } else { 166 | close(err) 167 | } 168 | } 169 | } 170 | 171 | appendSubscription := func(scheduler Scheduler) (sub *_subscription, err error) { 172 | accessSubscriptions(func([]_subscription) { 173 | cursor := atomic.LoadUint64(&buf.begin) 174 | s := &buf.subscriptions 175 | if len(s.entries) < cap(s.entries) { 176 | s.entries = append(s.entries, _subscription{cursor: cursor, scheduler: scheduler}) 177 | sub = &s.entries[len(s.entries)-1] 178 | return 179 | } 180 | for i := range s.entries { 181 | sub = &s.entries[i] 182 | if atomic.CompareAndSwapUint64(&sub.cursor, maxuint64, cursor) { 183 | sub.scheduler = scheduler 184 | return 185 | } 186 | } 187 | sub = nil 188 | err = ErrOutOfSubjectSubscriptions 189 | }) 190 | return 191 | } 192 | 193 | observable := func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 194 | sub, err := appendSubscription(scheduler) 195 | if err != nil { 196 | runner := scheduler.Schedule(func() { 197 | if subscriber.Subscribed() { 198 | var zero T 199 | observe(zero, err, true) 200 | } 201 | }) 202 | subscriber.OnUnsubscribe(runner.Cancel) 203 | return 204 | } 205 | commit := atomic.LoadUint64(&buf.commit) 206 | if atomic.LoadUint64(&buf.begin)+buf.keep < commit { 207 | atomic.StoreUint64(&sub.cursor, commit-buf.keep) 208 | } 209 | atomic.StoreUint64(&sub.state, atomic.LoadUint64(&buf.state)) 210 | sub.activated = time.Now() 211 | 212 | receiver := scheduler.ScheduleFutureRecursive(0, func(self func(time.Duration)) { 213 | commit := atomic.LoadUint64(&buf.commit) 214 | 215 | // wait for commit to move away from cursor position, indicating fresh data 216 | if sub.cursor == commit { 217 | if atomic.CompareAndSwapUint64(&sub.state, canceled, canceled) { 218 | // subscription canceled 219 | atomic.StoreUint64(&sub.cursor, maxuint64) 220 | return 221 | } else { 222 | // subscription still active (not canceled) 223 | now := time.Now() 224 | if now.Before(sub.activated.Add(1 * ms)) { 225 | // spinlock for 1ms (in increments of 50us) when no data from sender is arriving 226 | self(50 * us) // 20kHz 227 | return 228 | } else if now.Before(sub.activated.Add(250 * ms)) { 229 | if atomic.CompareAndSwapUint64(&sub.state, closed, closed) { 230 | // buffer has been closed 231 | var zero T 232 | observe(zero, buf.err, true) 233 | atomic.StoreUint64(&sub.cursor, maxuint64) 234 | return 235 | } 236 | // spinlock between 1ms and 250ms (in increments of 500us) of no data from sender 237 | self(500 * us) // 2kHz 238 | return 239 | } else { 240 | if scheduler.IsConcurrent() { 241 | // Block goroutine on condition until notified. 242 | buf.subscriptions.Lock() 243 | buf.subscriptions.Wait() 244 | buf.subscriptions.Unlock() 245 | sub.activated = time.Now() 246 | self(0) 247 | return 248 | } else { 249 | // Spinlock rest of the time (in increments of 5ms). This wakes-up 250 | // slower than the condition based solution above, but can be used with 251 | // a trampoline scheduler. 252 | self(5 * ms) // 200Hz 253 | return 254 | } 255 | } 256 | } 257 | } 258 | 259 | // emit data and advance cursor to catch up to commit 260 | if atomic.LoadUint64(&sub.state) == canceled { 261 | atomic.StoreUint64(&sub.cursor, maxuint64) 262 | return 263 | } 264 | for ; sub.cursor != commit; atomic.AddUint64(&sub.cursor, 1) { 265 | item := &buf.items[sub.cursor&buf.mod] 266 | if buf.age == 0 || item.At.IsZero() || time.Since(item.At) < buf.age { 267 | observe(item.Value, nil, false) 268 | } 269 | if atomic.LoadUint64(&sub.state) == canceled { 270 | atomic.StoreUint64(&sub.cursor, maxuint64) 271 | return 272 | } 273 | } 274 | 275 | // all caught up; record time and loop back to wait for fresh data 276 | sub.activated = time.Now() 277 | self(0) 278 | }) 279 | subscriber.OnUnsubscribe(receiver.Cancel) 280 | 281 | subscriber.OnUnsubscribe(func() { 282 | atomic.CompareAndSwapUint64(&sub.state, active, canceled) 283 | buf.subscriptions.Broadcast() 284 | }) 285 | } 286 | return observer, observable 287 | } 288 | 289 | type _buffer[T any] struct { 290 | age time.Duration 291 | keep uint64 292 | mod uint64 293 | size uint64 294 | 295 | items []_item[T] 296 | begin uint64 297 | end uint64 298 | commit uint64 299 | state uint64 // active, closed 300 | 301 | subscriptions _subscriptions 302 | 303 | err error 304 | } 305 | 306 | type _item[T any] struct { 307 | Value T 308 | At time.Time 309 | } 310 | 311 | type _subscriptions struct { 312 | sync.Mutex 313 | *sync.Cond 314 | entries []_subscription 315 | access uint32 // unlocked, locked 316 | } 317 | 318 | type _subscription struct { 319 | cursor uint64 320 | state uint64 // active, canceled, closed 321 | activated time.Time // track activity to deterime backoff 322 | scheduler Scheduler 323 | } 324 | -------------------------------------------------------------------------------- /subscribe.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Subscribe(observe Observer[T], scheduler Scheduler) Subscription { 4 | subscription := newSubscription(scheduler) 5 | observer := func(next T, err error, done bool) { 6 | if !done { 7 | observe(next, err, done) 8 | } else { 9 | var zero T 10 | observe(zero, err, true) 11 | subscription.done(err) 12 | } 13 | } 14 | observable(observer, scheduler, subscription) 15 | return subscription 16 | } 17 | -------------------------------------------------------------------------------- /subscribeon.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) SubscribeOn(scheduler ConcurrentScheduler) Observable[T] { 4 | return func(observe Observer[T], _ Scheduler, subscriber Subscriber) { 5 | observable(observe, scheduler, subscriber) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /subscriber.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // Subscriber is a subscribable entity that allows construction of a Subscriber tree. 9 | type Subscriber interface { 10 | // Subscribed returns true if the subscriber is in a subscribed state. 11 | // Returns false once Unsubscribe has been called. 12 | Subscribed() bool 13 | 14 | // Unsubscribe changes the state to unsubscribed and executes all registered 15 | // callback functions. Does nothing if already unsubscribed. 16 | Unsubscribe() 17 | 18 | // Add creates and returns a new child Subscriber. 19 | // If the parent is already unsubscribed, the child will be created in an 20 | // unsubscribed state. Otherwise, the child will be unsubscribed when the parent 21 | // is unsubscribed. 22 | Add() Subscriber 23 | 24 | // OnUnsubscribe registers a callback function to be executed when Unsubscribe is called. 25 | // If the subscriber is already unsubscribed, the callback is executed immediately. 26 | // If callback is nil, this method does nothing. 27 | OnUnsubscribe(callback func()) 28 | } 29 | 30 | const ( 31 | subscribed = iota 32 | unsubscribed 33 | ) 34 | 35 | type subscriber struct { 36 | state atomic.Int32 37 | sync.Mutex 38 | callbacks []func() 39 | } 40 | 41 | func (s *subscriber) Subscribed() bool { 42 | return s.state.Load() == subscribed 43 | } 44 | 45 | func (s *subscriber) Unsubscribe() { 46 | if s.state.CompareAndSwap(subscribed, unsubscribed) { 47 | s.Lock() 48 | for _, cb := range s.callbacks { 49 | cb() 50 | } 51 | s.callbacks = nil 52 | s.Unlock() 53 | } 54 | } 55 | 56 | func (s *subscriber) Add() Subscriber { 57 | child := &subscriber{} 58 | s.Lock() 59 | if s.state.Load() != subscribed { 60 | child.Unsubscribe() 61 | } else { 62 | s.callbacks = append(s.callbacks, child.Unsubscribe) 63 | } 64 | s.Unlock() 65 | return child 66 | } 67 | 68 | func (s *subscriber) OnUnsubscribe(callback func()) { 69 | if callback == nil { 70 | return 71 | } 72 | s.Lock() 73 | if s.state.Load() == subscribed { 74 | s.callbacks = append(s.callbacks, callback) 75 | } else { 76 | callback() 77 | } 78 | s.Unlock() 79 | } 80 | -------------------------------------------------------------------------------- /subscription.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | // Subscription is an interface that allows monitoring and controlling a subscription. 9 | // It provides methods for tracking the subscription's lifecycle. 10 | type Subscription interface { 11 | // Subscribed returns true until Unsubscribe is called. 12 | Subscribed() bool 13 | 14 | // Unsubscribe will change the state to unsubscribed. 15 | Unsubscribe() 16 | 17 | // Done returns a channel that is closed when the subscription state changes to unsubscribed. 18 | // This channel can be used with select statements to react to subscription termination events. 19 | // If the scheduler is not concurrent, it will spawn a goroutine to wait for the scheduler. 20 | Done() <-chan struct{} 21 | 22 | // Err returns the subscription's terminal state: 23 | // - nil if the observable completed successfully 24 | // - the observable's error if it terminated with an error 25 | // - SubscriptionCanceled if the subscription was manually unsubscribed 26 | // - SubscriptionActive if the subscription is still active 27 | Err() error 28 | 29 | // Wait blocks until the subscription state becomes unsubscribed. 30 | // If the subscription is already unsubscribed, it returns immediately. 31 | // If the scheduler is not concurrent, it will wait for the scheduler to complete. 32 | // Returns: 33 | // - nil if the observable completed successfully 34 | // - the observable's error if it terminated with an error 35 | // - SubscriptionCanceled if the subscription was manually unsubscribed 36 | Wait() error 37 | } 38 | 39 | type subscription struct { 40 | subscriber 41 | scheduler Scheduler 42 | err error 43 | } 44 | 45 | // ErrSubscriptionActive is the error returned by Err() when the 46 | // subscription is still active and has not yet completed or been canceled. 47 | var ErrSubscriptionActive = errors.Join(Err, errors.New("subscription active")) 48 | 49 | // ErrSubscriptionCanceled is the error returned by Wait() and Err() when the 50 | // subscription was canceled by calling Unsubscribe() on the Subscription. 51 | // This indicates the subscription was terminated by the subscriber rather than 52 | // by the observable completing normally or with an error. 53 | var ErrSubscriptionCanceled = errors.Join(Err, errors.New("subscription canceled")) 54 | 55 | func newSubscription(scheduler Scheduler) *subscription { 56 | return &subscription{scheduler: scheduler, err: ErrSubscriptionCanceled} 57 | } 58 | 59 | func (s *subscription) done(err error) { 60 | s.Lock() 61 | s.err = err 62 | s.Unlock() 63 | 64 | s.Unsubscribe() 65 | } 66 | 67 | func (s *subscription) Done() <-chan struct{} { 68 | cancel := make(chan struct{}) 69 | s.OnUnsubscribe(func() { close(cancel) }) 70 | if !s.scheduler.IsConcurrent() { 71 | go s.scheduler.Wait() 72 | } 73 | return cancel 74 | } 75 | 76 | func (s *subscription) Err() (err error) { 77 | if s.Subscribed() { 78 | return ErrSubscriptionActive 79 | } 80 | s.Lock() 81 | err = s.err 82 | s.Unlock() 83 | return 84 | } 85 | 86 | func (s *subscription) Wait() error { 87 | if !s.scheduler.IsConcurrent() { 88 | s.scheduler.Wait() 89 | } 90 | 91 | if s.Subscribed() { 92 | var wg sync.WaitGroup 93 | wg.Add(1) 94 | s.OnUnsubscribe(wg.Done) 95 | wg.Wait() 96 | } 97 | 98 | return s.Err() 99 | } 100 | -------------------------------------------------------------------------------- /subscription_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/reactivego/rx" 10 | ) 11 | 12 | func TestSubscription(t *testing.T) { 13 | const msec = time.Millisecond 14 | 15 | t.Run("Context timeout before subscription completes", func(t *testing.T) { 16 | // Force the maybeLongOperation to always take a long time 17 | maybeLongOperation := func(i int) rx.Observable[int] { 18 | // Ensure it exceeds our timeout 19 | return rx.Of(i).Delay(2000 * msec) 20 | } 21 | 22 | isOne := func(i int) bool { return i == 1 } 23 | 24 | ctx, cancel := context.WithTimeout(context.Background(), 500*msec) 25 | defer cancel() 26 | 27 | s := rx.ConcatMap(rx.Of(1).Filter(isOne).Take(1), maybeLongOperation).Go() 28 | defer s.Unsubscribe() 29 | 30 | select { 31 | case <-ctx.Done(): 32 | // Success, timeout exceeded! 33 | case <-s.Done(): 34 | t.Errorf("expected context timeout, but observable completed with error: %v", s.Err()) 35 | } 36 | }) 37 | 38 | t.Run("Subscription completes before context timeout", func(t *testing.T) { 39 | // Force the maybeLongOperation to always be quick 40 | maybeLongOperation := func(i int) rx.Observable[int] { 41 | // No sleep, complete immediately 42 | return rx.Empty[int]() 43 | } 44 | 45 | isOne := func(i int) bool { return i == 1 } 46 | 47 | ctx, cancel := context.WithTimeout(context.Background(), 2000*msec) 48 | defer cancel() 49 | 50 | s := rx.ConcatMap(rx.From(1).Filter(isOne).Take(1), maybeLongOperation).Go() 51 | defer s.Unsubscribe() 52 | 53 | select { 54 | case <-ctx.Done(): 55 | t.Error("expected success, got timeout") 56 | case <-s.Done(): 57 | if err := s.Err(); err != nil { 58 | t.Errorf("expected nil error for successful completion, got: %v", err) 59 | } 60 | } 61 | }) 62 | 63 | t.Run("Unsubscribe terminates the subscription", func(t *testing.T) { 64 | // Create a subscription that would take a long time 65 | s := rx.Of(1).Delay(600 * msec).Go() 66 | 67 | // Immediately unsubscribe 68 | s.Unsubscribe() 69 | 70 | // Check that the Done channel closes quickly 71 | select { 72 | case <-s.Done(): 73 | // Success, the subscription completed after unsubscribing 74 | if err := s.Err(); err != rx.ErrSubscriptionCanceled { 75 | t.Errorf("expected %v error, got: %v", rx.ErrSubscriptionCanceled, err) 76 | } 77 | case <-time.After(500 * msec): 78 | t.Error("subscription did not complete in time after unsubscribing") 79 | } 80 | }) 81 | 82 | t.Run("Done channel closes after normal completion", func(t *testing.T) { 83 | s := rx.From(1, 2, 3).Take(3).Go() 84 | 85 | // Set a timeout to detect if Done channel doesn't close by itself. 86 | select { 87 | case <-s.Done(): 88 | // Success - the channel closed as expected 89 | if err := s.Err(); err != nil { 90 | t.Errorf("expected nil error for successful completion, got: %v", err) 91 | } 92 | case <-time.After(500 * msec): 93 | t.Error("subscription Done channel did not close within the expected timeframe") 94 | } 95 | }) 96 | 97 | t.Run("Wait blocks until completion", func(t *testing.T) { 98 | s := rx.Of(1).Delay(350 * msec).Go() 99 | 100 | // Wait should block until the observable completes 101 | start := time.Now() 102 | err := s.Wait() 103 | elapsed := time.Since(start) 104 | 105 | if err != nil { 106 | t.Errorf("expected nil error, got: %v", err) 107 | } 108 | 109 | if elapsed < 300*msec { 110 | t.Errorf("wait returned too quickly, expected at least 300ms, got: %v", elapsed) 111 | } 112 | }) 113 | 114 | t.Run("Wait returns immediately if already completed", func(t *testing.T) { 115 | s := rx.Of(1).Go() 116 | 117 | // Make sure it's done 118 | <-s.Done() 119 | 120 | // Wait should return immediately 121 | start := time.Now() 122 | err := s.Wait() 123 | elapsed := time.Since(start) 124 | 125 | if err != nil { 126 | t.Errorf("expected nil error, got: %v", err) 127 | } 128 | 129 | if elapsed > 50*msec { 130 | t.Errorf("wait took too long for completed subscription, got: %v", elapsed) 131 | } 132 | }) 133 | 134 | t.Run("Err returns error from observable", func(t *testing.T) { 135 | expectedErr := errors.New("test error") 136 | s := rx.Throw[int](expectedErr).Go() 137 | 138 | // Wait for completion 139 | <-s.Done() 140 | 141 | if err := s.Err(); err != expectedErr { 142 | t.Errorf("expected error %v, got: %v", expectedErr, err) 143 | } 144 | }) 145 | 146 | t.Run("Err returns SubscriptionActive while active", func(t *testing.T) { 147 | ch := make(chan struct{}) 148 | s := rx.Recv(ch).Go() 149 | 150 | // Subscription should be active 151 | if err := s.Err(); err != rx.ErrSubscriptionActive { 152 | t.Errorf("expected SubscriptionActive, got: %v", err) 153 | } 154 | 155 | // Complete the observable 156 | close(ch) 157 | <-s.Done() 158 | }) 159 | } 160 | -------------------------------------------------------------------------------- /switchall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | func SwitchAll[T any](observable Observable[Observable[T]]) Observable[T] { 9 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 10 | var switches struct { 11 | sync.Mutex 12 | done bool 13 | count int32 14 | subscriber Subscriber 15 | } 16 | observer := func(next T, err error, done bool) { 17 | switches.Lock() 18 | defer switches.Unlock() 19 | if !switches.done { 20 | switch { 21 | case !done: 22 | observe(next, nil, false) 23 | case err != nil: 24 | switches.done = true 25 | var zero T 26 | observe(zero, err, true) 27 | default: 28 | if atomic.AddInt32(&switches.count, -1) == 0 { 29 | var zero T 30 | observe(zero, nil, true) 31 | } 32 | } 33 | } 34 | } 35 | switcher := func(next Observable[T], err error, done bool) { 36 | if !done { 37 | switch { 38 | case atomic.CompareAndSwapInt32(&switches.count, 2, 3): 39 | switches.Lock() 40 | defer switches.Unlock() 41 | switches.subscriber.Unsubscribe() 42 | atomic.StoreInt32(&switches.count, 2) 43 | fallthrough 44 | case atomic.CompareAndSwapInt32(&switches.count, 1, 2): 45 | switches.subscriber = subscriber.Add() 46 | next.AutoUnsubscribe()(observer, scheduler, switches.subscriber) 47 | } 48 | } else { 49 | var zero T 50 | observer(zero, err, true) 51 | } 52 | } 53 | switches.count += 1 54 | observable.AutoUnsubscribe()(switcher, scheduler, subscriber) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /switchmap.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func SwitchMap[T, U any](o Observable[T], project func(T) Observable[U]) Observable[U] { 4 | return SwitchAll(Map(o, project)) 5 | } 6 | -------------------------------------------------------------------------------- /take.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | // Take returns an Observable that emits only the first count values emitted by 4 | // the source Observable. If the source emits fewer than count values then all 5 | // of its values are emitted. After that, it completes, regardless if the source 6 | // completes. 7 | func Take[T any](n int) Pipe[T] { 8 | return func(observable Observable[T]) Observable[T] { 9 | if n <= 0 { 10 | return Empty[T]() 11 | } 12 | return Observable[T](func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 13 | taken := 0 14 | observable(func(next T, err error, done bool) { 15 | if !done { 16 | observe(next, nil, false) 17 | if taken++; taken == n { 18 | var zero T 19 | observe(zero, nil, true) 20 | } 21 | } else { 22 | observe(next, err, true) 23 | } 24 | }, scheduler, subscriber) 25 | }).AutoUnsubscribe() 26 | } 27 | } 28 | 29 | // Take returns an Observable that emits only the first count values emitted by 30 | // the source Observable. If the source emits fewer than count values then all 31 | // of its values are emitted. After that, it completes, regardless if the source 32 | // completes. 33 | func (observable Observable[T]) Take(n int) Observable[T] { 34 | return Take[T](n)(observable) 35 | } 36 | -------------------------------------------------------------------------------- /take_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/reactivego/rx" 8 | ) 9 | 10 | func TestTake(t *testing.T) { 11 | t.Run("Basic take behavior", func(t *testing.T) { 12 | nums := rx.From(1, 2, 3, 4, 5) 13 | taken := nums.Take(3) 14 | 15 | var results []int 16 | taken.Append(&results).Wait() 17 | 18 | expected := []int{1, 2, 3} 19 | if len(results) != len(expected) { 20 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 21 | } 22 | 23 | for i, v := range expected { 24 | if results[i] != v { 25 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 26 | } 27 | } 28 | }) 29 | 30 | t.Run("Take more than available", func(t *testing.T) { 31 | nums := rx.From(1, 2, 3) 32 | taken := nums.Take(5) 33 | 34 | var results []int 35 | taken.Append(&results).Wait() 36 | 37 | expected := []int{1, 2, 3} 38 | if len(results) != len(expected) { 39 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 40 | } 41 | }) 42 | 43 | t.Run("Take zero", func(t *testing.T) { 44 | nums := rx.From(1, 2, 3) 45 | taken := nums.Take(0) 46 | 47 | var results []int 48 | taken.Append(&results).Wait() 49 | 50 | if len(results) != 0 { 51 | t.Errorf("Expected 0 items, got %v", len(results)) 52 | } 53 | }) 54 | 55 | t.Run("Take from channel", func(t *testing.T) { 56 | ch := make(chan int, 3) 57 | observable := rx.Recv(ch) 58 | 59 | completed := false 60 | var results []int 61 | 62 | // Use goroutine scheduler for async processing 63 | s := observable.Take(1).Subscribe(func(next int, err error, done bool) { 64 | if !done { 65 | results = append(results, next) 66 | } else if err == nil { 67 | completed = true 68 | } 69 | }, rx.Goroutine) 70 | 71 | // Send a value through the channel 72 | ch <- 1 73 | 74 | // Wait for subscription to complete 75 | err := s.Wait() 76 | 77 | // Verify results 78 | if err != nil { 79 | t.Errorf("Expected completion, got %v", err) 80 | } 81 | 82 | if !completed { 83 | t.Error("Observable didn't complete") 84 | } 85 | 86 | if len(results) != 1 || results[0] != 1 { 87 | t.Errorf("Expected [1], got %v", results) 88 | } 89 | }) 90 | 91 | t.Run("Take zero from channel", func(t *testing.T) { 92 | ch := make(chan int) 93 | observable := rx.Recv(ch) 94 | var results []int 95 | 96 | err := observable.Take(0).Append(&results).Wait() 97 | 98 | if err != nil { 99 | t.Errorf("Expected completion, got %v", err) 100 | } 101 | 102 | if len(results) != 0 { 103 | t.Errorf("Expected 0 items, got %v", len(results)) 104 | } 105 | }) 106 | 107 | t.Run("Take with multiple values from channel", func(t *testing.T) { 108 | ch := make(chan int, 5) 109 | observable := rx.Recv(ch) 110 | 111 | var results []int 112 | 113 | // Take only 3 values 114 | s := observable.Take(3).Subscribe(func(next int, err error, done bool) { 115 | if !done { 116 | results = append(results, next) 117 | } 118 | }, rx.Goroutine) 119 | 120 | // Send values 121 | ch <- 10 122 | ch <- 20 123 | ch <- 30 124 | ch <- 40 // This should not be received due to Take(3) 125 | ch <- 50 // This should not be received due to Take(3) 126 | 127 | // Allow some time for processing 128 | time.Sleep(10 * time.Millisecond) 129 | 130 | // Wait for subscription to complete 131 | s.Wait() 132 | 133 | // Verify results 134 | expected := []int{10, 20, 30} 135 | if len(results) != len(expected) { 136 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 137 | } 138 | 139 | for i, v := range expected { 140 | if results[i] != v { 141 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 142 | } 143 | } 144 | }) 145 | 146 | t.Run("Take with closed channel", func(t *testing.T) { 147 | ch := make(chan int, 1) 148 | observable := rx.Recv(ch) 149 | 150 | completed := false 151 | var results []int 152 | 153 | s := observable.Take(3).Subscribe(func(next int, err error, done bool) { 154 | if !done { 155 | results = append(results, next) 156 | } else if err == nil { 157 | completed = true 158 | } 159 | }, rx.Goroutine) 160 | 161 | // Send one value and close the channel 162 | ch <- 100 163 | close(ch) 164 | 165 | // Wait for subscription to complete 166 | s.Wait() 167 | 168 | // Verify results 169 | if !completed { 170 | t.Error("Observable didn't complete") 171 | } 172 | 173 | if len(results) != 1 || results[0] != 100 { 174 | t.Errorf("Expected [100], got %v", results) 175 | } 176 | }) 177 | 178 | t.Run("Take with Go method", func(t *testing.T) { 179 | ch := make(chan int, 1) 180 | observable := rx.Recv(ch) 181 | 182 | completed := false 183 | var results []int 184 | 185 | // Use Go method as in the provided main function 186 | s := observable.Take(1).Subscribe(func(next int, err error, done bool) { 187 | if !done { 188 | results = append(results, next) 189 | } else if err == nil { 190 | completed = true 191 | } 192 | }, rx.Goroutine) 193 | 194 | // Send a value through the channel 195 | ch <- 1 196 | 197 | // Wait for subscription to complete 198 | s.Wait() 199 | 200 | // Verify results 201 | if !completed { 202 | t.Error("Observable didn't complete") 203 | } 204 | 205 | if len(results) != 1 || results[0] != 1 { 206 | t.Errorf("Expected [1], got %v", results) 207 | } 208 | }) 209 | } 210 | -------------------------------------------------------------------------------- /takewhile.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func TakeWhile[T any](condition func(T) bool) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | if done || condition(next) { 8 | observe(next, err, done) 9 | } else { 10 | var zero T 11 | observe(zero, nil, true) 12 | } 13 | }, scheduler, subscriber) 14 | } 15 | } 16 | } 17 | 18 | func (observable Observable[T]) TakeWhile(condition func(T) bool) Observable[T] { 19 | return TakeWhile[T](condition)(observable) 20 | } 21 | -------------------------------------------------------------------------------- /tap.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Tap[T any](tap Observer[T]) Pipe[T] { 4 | return func(observable Observable[T]) Observable[T] { 5 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 6 | observable(func(next T, err error, done bool) { 7 | tap(next, err, done) 8 | observe(next, err, done) 9 | }, scheduler, subscriber) 10 | } 11 | } 12 | } 13 | 14 | func (observable Observable[T]) Tap(tap Observer[T]) Observable[T] { 15 | return Tap(tap)(observable) 16 | } 17 | -------------------------------------------------------------------------------- /throw.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Throw[T any](err error) Observable[T] { 4 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 5 | task := func() { 6 | if subscriber.Subscribed() { 7 | var zero T 8 | observe(zero, err, true) 9 | } 10 | } 11 | runner := scheduler.Schedule(task) 12 | subscriber.OnUnsubscribe(runner.Cancel) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "time" 4 | 5 | // Ticker creates an ObservableTime that emits a sequence of timestamps after 6 | // an initialDelay has passed. Subsequent timestamps are emitted using a 7 | // schedule of intervals passed in. If only the initialDelay is given, Ticker 8 | // will emit only once. 9 | func Ticker(initialDelay time.Duration, intervals ...time.Duration) Observable[time.Time] { 10 | observable := func(observe Observer[time.Time], scheduler Scheduler, subscriber Subscriber) { 11 | i := 0 12 | runner := scheduler.ScheduleFutureRecursive(initialDelay, func(again func(time.Duration)) { 13 | if subscriber.Subscribed() { 14 | if i == 0 || (i > 0 && len(intervals) > 0) { 15 | observe(scheduler.Now(), nil, false) 16 | } 17 | if subscriber.Subscribed() { 18 | if len(intervals) > 0 { 19 | again(intervals[i%len(intervals)]) 20 | } else { 21 | if i == 0 { 22 | again(0) 23 | } else { 24 | var zero time.Time 25 | observe(zero, nil, true) 26 | } 27 | } 28 | } 29 | i++ 30 | } 31 | }) 32 | subscriber.OnUnsubscribe(runner.Cancel) 33 | } 34 | return observable 35 | } 36 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "time" 4 | 5 | func Timer[T Integer | Float](initialDelay time.Duration, intervals ...time.Duration) Observable[T] { 6 | return func(observe Observer[T], scheduler Scheduler, subscriber Subscriber) { 7 | var i T 8 | task := func(again func(due time.Duration)) { 9 | if subscriber.Subscribed() { 10 | if i == 0 || (i > 0 && len(intervals) > 0) { 11 | observe(i, nil, false) 12 | } 13 | if subscriber.Subscribed() { 14 | if len(intervals) > 0 { 15 | again(intervals[int(i)%len(intervals)]) 16 | } else { 17 | if i == 0 { 18 | again(0) 19 | } else { 20 | var zero T 21 | observe(zero, nil, true) 22 | } 23 | } 24 | } 25 | i++ 26 | } 27 | } 28 | runner := scheduler.ScheduleFutureRecursive(initialDelay, task) 29 | subscriber.OnUnsubscribe(runner.Cancel) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tuple.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | type Tuple2[T, U any] struct { 4 | First T 5 | Second U 6 | } 7 | 8 | type Tuple3[T, U, V any] struct { 9 | First T 10 | Second U 11 | Third V 12 | } 13 | 14 | type Tuple4[T, U, V, W any] struct { 15 | First T 16 | Second U 17 | Third V 18 | Fourth W 19 | } 20 | 21 | type Tuple5[T, U, V, W, X any] struct { 22 | First T 23 | Second U 24 | Third V 25 | Fourth W 26 | Fifth X 27 | } 28 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import "iter" 4 | 5 | func (observable Observable[T]) Values(scheduler ...Scheduler) iter.Seq[T] { 6 | return func(yield func(T) bool) { 7 | err := observable.TakeWhile(yield).Wait(scheduler...) 8 | _ = err // ignores error! so will fail silently 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/reactivego/rx" 7 | ) 8 | 9 | func TestValues(t *testing.T) { 10 | t.Run("Basic values extraction", func(t *testing.T) { 11 | nums := rx.From(1, 2, 3, 4, 5) 12 | var results []int 13 | for v := range nums.Values() { 14 | results = append(results, v) 15 | } 16 | expected := []int{1, 2, 3, 4, 5} 17 | if len(results) != len(expected) { 18 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 19 | } 20 | 21 | for i, v := range expected { 22 | if results[i] != v { 23 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 24 | } 25 | } 26 | }) 27 | 28 | t.Run("Empty observable", func(t *testing.T) { 29 | nums := rx.Empty[int]() 30 | count := 0 31 | for range nums.Values() { 32 | count++ 33 | } 34 | if count != 0 { 35 | t.Errorf("Expected 0 items, got %v", count) 36 | } 37 | }) 38 | 39 | t.Run("Values with filtering", func(t *testing.T) { 40 | nums := rx.From(1, 2, 3, 4, 5, 6, 7) 41 | filtered := nums.Filter(func(i int) bool { 42 | return i%2 == 0 43 | }) 44 | var results []int 45 | for v := range filtered.Values() { 46 | results = append(results, v) 47 | } 48 | expected := []int{2, 4, 6} 49 | if len(results) != len(expected) { 50 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 51 | } 52 | 53 | for i, v := range expected { 54 | if results[i] != v { 55 | t.Errorf("Expected %v at index %v, got %v", v, i, results[i]) 56 | } 57 | } 58 | }) 59 | 60 | t.Run("Values with early termination", func(t *testing.T) { 61 | nums := rx.From(1, 2, 3, 4, 5) 62 | taken := nums.Take(3) 63 | var results []int 64 | for v := range taken.Values() { 65 | results = append(results, v) 66 | } 67 | expected := []int{1, 2, 3} 68 | if len(results) != len(expected) { 69 | t.Errorf("Expected %v items, got %v", len(expected), len(results)) 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /wait.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func (observable Observable[T]) Wait(schedulers ...Scheduler) error { 4 | if len(schedulers) == 0 { 5 | return observable.Subscribe(Ignore[T](), NewScheduler()).Wait() 6 | } else { 7 | return observable.Subscribe(Ignore[T](), schedulers[0]).Wait() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /withlatestfrom.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func WithLatestFrom[T any](observables ...Observable[T]) Observable[[]T] { 4 | return WithLatestFromAll(From(observables...)) 5 | } 6 | 7 | func WithLatestFrom2[T, U any](first Observable[T], second Observable[U]) Observable[Tuple2[T, U]] { 8 | return Map(WithLatestFromAll(From(first.AsObservable(), second.AsObservable())), func(next []any) Tuple2[T, U] { 9 | return Tuple2[T, U]{next[0].(T), next[1].(U)} 10 | }) 11 | } 12 | 13 | func WithLatestFrom3[T, U, V any](first Observable[T], second Observable[U], third Observable[V]) Observable[Tuple3[T, U, V]] { 14 | return Map(WithLatestFromAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable())), func(next []any) Tuple3[T, U, V] { 15 | return Tuple3[T, U, V]{next[0].(T), next[1].(U), next[2].(V)} 16 | }) 17 | } 18 | 19 | func WithLatestFrom4[T, U, V, W any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W]) Observable[Tuple4[T, U, V, W]] { 20 | return Map(WithLatestFromAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable())), func(next []any) Tuple4[T, U, V, W] { 21 | return Tuple4[T, U, V, W]{next[0].(T), next[1].(U), next[2].(V), next[3].(W)} 22 | }) 23 | } 24 | 25 | func WithLatestFrom5[T, U, V, W, X any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W], fifth Observable[X]) Observable[Tuple5[T, U, V, W, X]] { 26 | return Map(WithLatestFromAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable(), fifth.AsObservable())), func(next []any) Tuple5[T, U, V, W, X] { 27 | return Tuple5[T, U, V, W, X]{next[0].(T), next[1].(U), next[2].(V), next[3].(W), next[4].(X)} 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /withlatestfromall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func WithLatestFromAll[T any](observable Observable[Observable[T]]) Observable[[]T] { 8 | return func(observe Observer[[]T], scheduler Scheduler, subscriber Subscriber) { 9 | var sources []Observable[T] 10 | var buffers struct { 11 | sync.Mutex 12 | assigned []bool 13 | values []T 14 | initialized int 15 | done bool 16 | } 17 | makeObserver := func(sourceIndex int) Observer[T] { 18 | observer := func(next T, err error, done bool) { 19 | buffers.Lock() 20 | defer buffers.Unlock() 21 | if !buffers.done { 22 | switch { 23 | case !done: 24 | if !buffers.assigned[sourceIndex] { 25 | buffers.assigned[sourceIndex] = true 26 | buffers.initialized++ 27 | } 28 | buffers.values[sourceIndex] = next 29 | if sourceIndex == 0 && buffers.initialized == len(buffers.values) { 30 | observe(buffers.values, nil, false) 31 | } 32 | case err != nil: 33 | buffers.done = true 34 | var zero []T 35 | observe(zero, err, true) 36 | default: 37 | if sourceIndex == 0 { 38 | buffers.done = true 39 | var zero []T 40 | observe(zero, nil, true) 41 | } 42 | } 43 | } 44 | } 45 | return observer 46 | } 47 | observer := func(next Observable[T], err error, done bool) { 48 | switch { 49 | case !done: 50 | sources = append(sources, next) 51 | case err != nil: 52 | var zero []T 53 | observe(zero, err, true) 54 | default: 55 | if len(sources) == 0 { 56 | var zero []T 57 | observe(zero, nil, true) 58 | return 59 | } 60 | runner := scheduler.Schedule(func() { 61 | if subscriber.Subscribed() { 62 | numSources := len(sources) 63 | buffers.assigned = make([]bool, numSources) 64 | buffers.values = make([]T, numSources) 65 | for sourceIndex, source := range sources { 66 | if !subscriber.Subscribed() { 67 | return 68 | } 69 | source.AutoUnsubscribe()(makeObserver(sourceIndex), scheduler, subscriber) 70 | } 71 | } 72 | }) 73 | subscriber.OnUnsubscribe(runner.Cancel) 74 | } 75 | } 76 | observable(observer, scheduler, subscriber) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /zip.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | func Zip[T any](observables ...Observable[T]) Observable[[]T] { 4 | return ZipAll(From(observables...)) 5 | } 6 | 7 | func Zip2[T, U any](first Observable[T], second Observable[U], options ...MaxBufferSizeOption) Observable[Tuple2[T, U]] { 8 | return Map(ZipAll(From(first.AsObservable(), second.AsObservable()), options...), func(next []any) Tuple2[T, U] { 9 | return Tuple2[T, U]{next[0].(T), next[1].(U)} 10 | }) 11 | } 12 | 13 | func Zip3[T, U, V any](first Observable[T], second Observable[U], third Observable[V], options ...MaxBufferSizeOption) Observable[Tuple3[T, U, V]] { 14 | return Map(ZipAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable()), options...), func(next []any) Tuple3[T, U, V] { 15 | return Tuple3[T, U, V]{next[0].(T), next[1].(U), next[2].(V)} 16 | }) 17 | } 18 | 19 | func Zip4[T, U, V, W any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W], options ...MaxBufferSizeOption) Observable[Tuple4[T, U, V, W]] { 20 | return Map(ZipAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable()), options...), func(next []any) Tuple4[T, U, V, W] { 21 | return Tuple4[T, U, V, W]{next[0].(T), next[1].(U), next[2].(V), next[3].(W)} 22 | }) 23 | } 24 | 25 | func Zip5[T, U, V, W, X any](first Observable[T], second Observable[U], third Observable[V], fourth Observable[W], fifth Observable[X], options ...MaxBufferSizeOption) Observable[Tuple5[T, U, V, W, X]] { 26 | return Map(ZipAll(From(first.AsObservable(), second.AsObservable(), third.AsObservable(), fourth.AsObservable(), fifth.AsObservable()), options...), func(next []any) Tuple5[T, U, V, W, X] { 27 | return Tuple5[T, U, V, W, X]{next[0].(T), next[1].(U), next[2].(V), next[3].(W), next[4].(X)} 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /zipall.go: -------------------------------------------------------------------------------- 1 | package rx 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var ErrZipBufferOverflow = errors.Join(Err, errors.New("zip buffer overflow")) 9 | 10 | func ZipAll[T any](observable Observable[Observable[T]], options ...MaxBufferSizeOption) Observable[[]T] { 11 | var maxBufferSize = 0 12 | for _, option := range options { 13 | option(&maxBufferSize) 14 | } 15 | return func(observe Observer[[]T], scheduler Scheduler, subscriber Subscriber) { 16 | var sources []Observable[T] 17 | var buffers struct { 18 | sync.Mutex 19 | values [][]T // Buffer for each source 20 | completed []bool // Track which sources have completed 21 | done bool 22 | } 23 | makeObserver := func(sourceIndex int) Observer[T] { 24 | return func(next T, err error, done bool) { 25 | buffers.Lock() 26 | defer buffers.Unlock() 27 | if !buffers.done { 28 | switch { 29 | case !done: 30 | // Check if adding this item would exceed the buffer size limit 31 | if maxBufferSize > 0 && len(buffers.values[sourceIndex]) >= maxBufferSize { 32 | buffers.done = true 33 | var zero []T 34 | observe(zero, ErrZipBufferOverflow, true) 35 | return 36 | } 37 | buffers.values[sourceIndex] = append(buffers.values[sourceIndex], next) 38 | result := make([]T, len(buffers.values)) 39 | for _, values := range buffers.values { 40 | if len(values) == 0 { 41 | return 42 | } 43 | } 44 | for i := range buffers.values { 45 | result[i] = buffers.values[i][0] 46 | buffers.values[i] = buffers.values[i][1:] // Remove the used value 47 | } 48 | observe(result, nil, false) 49 | case err != nil: 50 | buffers.done = true 51 | var zero []T 52 | observe(zero, err, true) 53 | default: 54 | buffers.completed[sourceIndex] = true 55 | for i, completed := range buffers.completed { 56 | if completed && len(buffers.values[i]) == 0 { 57 | buffers.done = true 58 | var zero []T 59 | observe(zero, nil, true) 60 | return 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | observer := func(next Observable[T], err error, done bool) { 68 | switch { 69 | case !done: 70 | sources = append(sources, next) 71 | case err != nil: 72 | var zero []T 73 | observe(zero, err, true) 74 | default: 75 | if len(sources) == 0 { 76 | var zero []T 77 | observe(zero, nil, true) 78 | return 79 | } 80 | runner := scheduler.Schedule(func() { 81 | if subscriber.Subscribed() { 82 | numSources := len(sources) 83 | buffers.values = make([][]T, numSources) 84 | buffers.completed = make([]bool, numSources) 85 | for sourceIndex, source := range sources { 86 | if !subscriber.Subscribed() { 87 | return 88 | } 89 | source.AutoUnsubscribe()(makeObserver(sourceIndex), scheduler, subscriber) 90 | } 91 | } 92 | }) 93 | subscriber.OnUnsubscribe(runner.Cancel) 94 | } 95 | } 96 | observable.AutoUnsubscribe()(observer, scheduler, subscriber) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /zipall_test.go: -------------------------------------------------------------------------------- 1 | package rx_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/reactivego/rx" 9 | ) 10 | 11 | func TestZipAll(t *testing.T) { 12 | t.Run("Basic zipping behavior", func(t *testing.T) { 13 | // Create two source observables 14 | nums := rx.From(1, 2, 3) 15 | chars := rx.From("a", "b", "c") 16 | 17 | // Create an observable of observables 18 | sources := rx.From( 19 | nums.AsObservable(), 20 | chars.AsObservable(), 21 | ) 22 | 23 | // Use ZipAll to combine them 24 | zipped := rx.ZipAll[any](sources) 25 | 26 | // Collect results 27 | var results [][]any 28 | 29 | // Use non-concurrent scheduler for predictable test execution 30 | scheduler := rx.NewScheduler() 31 | zipped.Subscribe(func(next []any, err error, done bool) { 32 | if !done { 33 | results = append(results, next) 34 | } 35 | }, scheduler).Wait() 36 | 37 | // Verify expected results 38 | expected := [][]any{ 39 | {1, "a"}, 40 | {2, "b"}, 41 | {3, "c"}, 42 | } 43 | 44 | if !reflect.DeepEqual(results, expected) { 45 | t.Errorf("Expected %v, got %v", expected, results) 46 | } 47 | }) 48 | 49 | t.Run("Different length sources", func(t *testing.T) { 50 | // Source 1 has more elements than source 2 51 | nums := rx.From(1, 2, 3, 4, 5) 52 | chars := rx.From("a", "b", "c") 53 | 54 | sources := rx.From( 55 | nums.AsObservable(), 56 | chars.AsObservable(), 57 | ) 58 | 59 | zipped := rx.ZipAll[any](sources) 60 | 61 | var results [][]any 62 | scheduler := rx.NewScheduler() 63 | zipped.Subscribe(func(next []any, err error, done bool) { 64 | if !done { 65 | results = append(results, next) 66 | } 67 | }, scheduler).Wait() 68 | 69 | // ZipAll should stop when the shortest source completes 70 | expected := [][]any{ 71 | {1, "a"}, 72 | {2, "b"}, 73 | {3, "c"}, 74 | } 75 | 76 | if !reflect.DeepEqual(results, expected) { 77 | t.Errorf("Expected %v, got %v", expected, results) 78 | } 79 | }) 80 | 81 | t.Run("Empty source observable", func(t *testing.T) { 82 | // No source observables provided 83 | var sources rx.Observable[rx.Observable[any]] 84 | sources = rx.From[rx.Observable[any]]() // Empty source 85 | zipped := rx.ZipAll[any](sources) 86 | 87 | completed := false 88 | scheduler := rx.NewScheduler() 89 | zipped.Subscribe(func(next []any, err error, done bool) { 90 | if done && err == nil { 91 | completed = true 92 | } 93 | }, scheduler).Wait() 94 | 95 | if !completed { 96 | t.Error("Expected observable to complete when source is empty") 97 | } 98 | }) 99 | 100 | t.Run("Error in source observable", func(t *testing.T) { 101 | nums := rx.From(1, 2, 3) 102 | 103 | // Observable that emits an error 104 | errObs := rx.Create(func(index int) (string, error, bool) { 105 | switch index { 106 | case 0: 107 | return "a", nil, false 108 | case 1: 109 | return "", errors.New("test error"), true 110 | default: 111 | return "", nil, true 112 | } 113 | }) 114 | 115 | sources := rx.From( 116 | nums.AsObservable(), 117 | errObs.AsObservable(), 118 | ) 119 | 120 | zipped := rx.ZipAll[any](sources) 121 | 122 | var results [][]any 123 | var receivedErr error 124 | 125 | scheduler := rx.NewScheduler() 126 | zipped.Subscribe(func(next []any, err error, done bool) { 127 | if !done { 128 | results = append(results, next) 129 | } else if err != nil { 130 | receivedErr = err 131 | } 132 | }, scheduler).Wait() 133 | 134 | expected := [][]any{ 135 | {1, "a"}, 136 | } 137 | 138 | if !reflect.DeepEqual(results, expected) { 139 | t.Errorf("Expected %v, got %v", expected, results) 140 | } 141 | 142 | if receivedErr == nil || receivedErr.Error() != "test error" { 143 | t.Errorf("Expected 'test error', got %v", receivedErr) 144 | } 145 | }) 146 | 147 | t.Run("One source completes early with empty buffer", func(t *testing.T) { 148 | // Create a source that completes after emitting just one item 149 | earlyComplete := rx.Create(func(index int) (int, error, bool) { 150 | switch index { 151 | case 0: 152 | return 1, nil, false 153 | default: 154 | return 0, nil, true 155 | } 156 | }) 157 | 158 | // Create a source that emits multiple items 159 | multiValue := rx.From(10, 20, 30, 40) 160 | 161 | sources := rx.From( 162 | earlyComplete.AsObservable(), 163 | multiValue.AsObservable(), 164 | ) 165 | 166 | zipped := rx.ZipAll[any](sources) 167 | 168 | var results [][]any 169 | 170 | scheduler := rx.NewScheduler() 171 | zipped.Subscribe(func(next []any, err error, done bool) { 172 | if !done { 173 | results = append(results, next) 174 | } 175 | }, scheduler).Wait() 176 | 177 | // Should only emit one pair, then complete 178 | expected := [][]any{ 179 | {1, 10}, 180 | } 181 | 182 | if !reflect.DeepEqual(results, expected) { 183 | t.Errorf("Expected %v, got %v", expected, results) 184 | } 185 | }) 186 | 187 | t.Run("Multiple sources with different completion times", func(t *testing.T) { 188 | // Three sources with different numbers of elements 189 | src1 := rx.From(1, 2) 190 | src2 := rx.From("a", "b", "c", "d") 191 | src3 := rx.From(true, false, true) 192 | 193 | sources := rx.From( 194 | src1.AsObservable(), 195 | src2.AsObservable(), 196 | src3.AsObservable(), 197 | ) 198 | 199 | zipped := rx.ZipAll[any](sources) 200 | 201 | var results [][]any 202 | 203 | scheduler := rx.NewScheduler() 204 | zipped.Subscribe(func(next []any, err error, done bool) { 205 | if !done { 206 | results = append(results, next) 207 | } 208 | }, scheduler).Wait() 209 | 210 | // Should only emit based on the shortest source (src1) 211 | expected := [][]any{ 212 | {1, "a", true}, 213 | {2, "b", false}, 214 | } 215 | 216 | if !reflect.DeepEqual(results, expected) { 217 | t.Errorf("Expected %v, got %v", expected, results) 218 | } 219 | }) 220 | 221 | t.Run("Unsubscribe during emission", func(t *testing.T) { 222 | // Create sources 223 | src1 := rx.From(1, 2, 3, 4, 5) 224 | src2 := rx.From("a", "b", "c", "d", "e") 225 | 226 | sources := rx.From( 227 | src1.AsObservable(), 228 | src2.AsObservable(), 229 | ) 230 | 231 | zipped := rx.ZipAll[any](sources) 232 | 233 | var results [][]any 234 | var subscription rx.Subscription 235 | 236 | scheduler := rx.NewScheduler() 237 | subscription = zipped.Subscribe(func(next []any, err error, done bool) { 238 | if !done { 239 | results = append(results, next) 240 | // Unsubscribe after receiving 2 emissions 241 | if len(results) >= 2 { 242 | subscription.Unsubscribe() 243 | } 244 | } 245 | }, scheduler) 246 | 247 | subscription.Wait() 248 | 249 | // Should only emit two pairs due to unsubscription 250 | expected := [][]any{ 251 | {1, "a"}, 252 | {2, "b"}, 253 | } 254 | 255 | if !reflect.DeepEqual(results, expected) { 256 | t.Errorf("Expected %v, got %v", expected, results) 257 | } 258 | }) 259 | 260 | t.Run("Parallel execution test", func(t *testing.T) { 261 | // Using goroutine scheduler for true parallelism 262 | 263 | // Create sources 264 | src1 := rx.From(1, 2, 3) 265 | src2 := rx.From("a", "b", "c") 266 | 267 | sources := rx.From( 268 | src1.AsObservable(), 269 | src2.AsObservable(), 270 | ) 271 | 272 | zipped := rx.ZipAll[any](sources) 273 | 274 | var results [][]any 275 | 276 | // Use Goroutine scheduler for parallel execution 277 | scheduler := rx.Goroutine 278 | zipped.Subscribe(func(next []any, err error, done bool) { 279 | if !done { 280 | results = append(results, next) 281 | } 282 | }, scheduler).Wait() 283 | 284 | // Results should still be zipped in order despite parallel execution 285 | expected := [][]any{ 286 | {1, "a"}, 287 | {2, "b"}, 288 | {3, "c"}, 289 | } 290 | 291 | if !reflect.DeepEqual(results, expected) { 292 | t.Errorf("Expected %v, got %v", expected, results) 293 | } 294 | }) 295 | 296 | t.Run("Interleaved emissions test", func(t *testing.T) { 297 | // Create source that alternates between emitting and not emitting 298 | alternate := rx.Create(func(index int) (int, error, bool) { 299 | if index >= 6 { 300 | return 0, nil, true 301 | } 302 | // Only emit on even indices, to create "delays" between emissions 303 | if index%2 == 0 { 304 | return index/2 + 1, nil, false 305 | } 306 | // Skip odd indices (simulating delay) 307 | return 0, nil, false 308 | }).Filter(func(i int) bool { return i != 0 }) 309 | 310 | // Create source that emits on every call 311 | regular := rx.Create(func(index int) (string, error, bool) { 312 | if index >= 3 { 313 | return "", nil, true 314 | } 315 | return string('a' + byte(index)), nil, false 316 | }) 317 | 318 | sources := rx.From( 319 | alternate.AsObservable(), 320 | regular.AsObservable(), 321 | ) 322 | 323 | zipped := rx.ZipAll[any](sources) 324 | 325 | var results [][]any 326 | 327 | scheduler := rx.NewScheduler() 328 | zipped.Subscribe(func(next []any, err error, done bool) { 329 | if !done { 330 | results = append(results, next) 331 | } 332 | }, scheduler).Wait() 333 | 334 | expected := [][]any{ 335 | {1, "a"}, 336 | {2, "b"}, 337 | {3, "c"}, 338 | } 339 | 340 | if !reflect.DeepEqual(results, expected) { 341 | t.Errorf("Expected %v, got %v", expected, results) 342 | } 343 | }) 344 | } 345 | --------------------------------------------------------------------------------