├── .codebeatignore ├── .gitignore ├── go.mod ├── doc.go ├── state.go ├── .travis.yml ├── Makefile ├── examples └── multiple.go ├── state_test.go ├── LICENSE.txt ├── property.go ├── property_test.go ├── stream.go ├── README.md └── stream_test.go /.codebeatignore: -------------------------------------------------------------------------------- 1 | examples/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /coverage.txt 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/imkira/go-observer/v2 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package observer aims to simplify the problem of channel-based broadcasting 2 | // of events from one or more publishers to one or more observers. 3 | package observer 4 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | type state[T any] struct { 4 | value T 5 | next *state[T] 6 | done chan struct{} 7 | } 8 | 9 | func newState[T any](value T) *state[T] { 10 | return &state[T]{ 11 | value: value, 12 | done: make(chan struct{}), 13 | } 14 | } 15 | 16 | func (s *state[T]) update(value T) *state[T] { 17 | s.next = newState(value) 18 | close(s.done) 19 | return s.next 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | - 1.5 8 | - 1.6 9 | - 1.7 10 | - tip 11 | 12 | before_install: 13 | if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then make deps; fi 14 | 15 | script: 16 | - if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then make gometalinter; fi 17 | - make test 18 | - make cover 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all deps gometalinter test cover 2 | 3 | all: gometalinter test cover 4 | 5 | deps: 6 | go get -u github.com/alecthomas/gometalinter 7 | gometalinter --install 8 | 9 | gometalinter: 10 | gometalinter --vendor --deadline=1m --tests \ 11 | --enable=gofmt \ 12 | --enable=goimports \ 13 | --enable=lll \ 14 | --enable=misspell \ 15 | --enable=unused 16 | 17 | test: 18 | go test -v -race -cpu=1,2,4 -coverprofile=coverage.txt -covermode=atomic 19 | 20 | cover: 21 | go tool cover -html=coverage.txt -o coverage.html 22 | -------------------------------------------------------------------------------- /examples/multiple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/imkira/go-observer/v2" 8 | ) 9 | 10 | func runPublisher(prop observer.Property[int]) { 11 | val := prop.Value() 12 | for { 13 | time.Sleep(time.Second) 14 | // update property 15 | val++ 16 | prop.Update(val) 17 | } 18 | } 19 | 20 | func runObserver(id int, prop observer.Property[int]) { 21 | stream := prop.Observe() 22 | 23 | for { 24 | val := stream.Value() 25 | fmt.Printf("Observer: %d, Value: %d\n", id, val) 26 | 27 | select { 28 | // wait for changes 29 | case <-stream.Changes(): 30 | // advance to next value 31 | stream.Next() 32 | } 33 | } 34 | } 35 | 36 | func main() { 37 | // create a property with initial value 38 | prop := observer.NewProperty(1) 39 | 40 | // run 10 observers 41 | for i := 0; i < 10; i++ { 42 | go runObserver(i, prop) 43 | } 44 | 45 | // run one publisher 46 | go runPublisher(prop) 47 | 48 | // terminate program after 10 seconds 49 | time.Sleep(10 * time.Second) 50 | } 51 | -------------------------------------------------------------------------------- /state_test.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import "testing" 4 | 5 | func testStateNew[T comparable](t *testing.T, state *state[T], val T) { 6 | if state.value != val { 7 | t.Fatalf("Expecting %#v but got %#v\n", val, state.value) 8 | } 9 | if state.next != nil { 10 | t.Fatalf("Expecting no next but got %#v\n", state.next) 11 | } 12 | select { 13 | case <-state.done: 14 | t.Fatalf("Expecting not done\n") 15 | default: 16 | } 17 | } 18 | 19 | func TestStateNew(t *testing.T) { 20 | state := newState(10) 21 | testStateNew(t, state, 10) 22 | } 23 | 24 | func TestStateUpdate(t *testing.T) { 25 | state1 := newState(10) 26 | state2 := state1.update(15) 27 | if state1 == state2 { 28 | t.Fatalf("Expecting different states\n") 29 | } 30 | if state2.value != 15 { 31 | t.Fatalf("Expecting 15 but got %#v\n", state1.value) 32 | } 33 | if state1.next == nil { 34 | t.Fatalf("Expecting next but got %#v\n", state1.next) 35 | } 36 | select { 37 | case <-state1.done: 38 | default: 39 | t.Fatalf("Expecting done\n") 40 | } 41 | testStateNew(t, state2, 15) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Mario Freitas (imkira@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /property.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import "sync" 4 | 5 | // Property is an object that is continuously updated by one or more 6 | // publishers. It is completely goroutine safe: you can use Property 7 | // concurrently from multiple goroutines. 8 | type Property[T any] interface { 9 | // Value returns the current value for this property. 10 | Value() T 11 | 12 | // Update sets a new value for this property. 13 | Update(value T) 14 | 15 | // Observe returns a newly created Stream for this property. 16 | Observe() Stream[T] 17 | } 18 | 19 | // NewProperty creates a new Property with the initial value value. 20 | // It returns the created Property. 21 | func NewProperty[T any](value T) Property[T] { 22 | return &property[T]{state: newState(value)} 23 | } 24 | 25 | type property[T any] struct { 26 | sync.RWMutex 27 | state *state[T] 28 | } 29 | 30 | func (p *property[T]) Value() T { 31 | p.RLock() 32 | defer p.RUnlock() 33 | return p.state.value 34 | } 35 | 36 | func (p *property[T]) Update(value T) { 37 | p.Lock() 38 | defer p.Unlock() 39 | p.state = p.state.update(value) 40 | } 41 | 42 | func (p *property[T]) Observe() Stream[T] { 43 | p.RLock() 44 | defer p.RUnlock() 45 | return &stream[T]{state: p.state} 46 | } 47 | -------------------------------------------------------------------------------- /property_test.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestPropertyInitialValue(t *testing.T) { 9 | prop := NewProperty(10) 10 | if val := prop.Value(); val != 10 { 11 | t.Fatalf("Expecting 10 but got %#v\n", val) 12 | } 13 | stream := prop.Observe() 14 | for i := 0; i <= 100; i++ { 15 | if val := stream.Value(); val != 10 { 16 | t.Fatalf("Expecting 10 but got %#v\n", val) 17 | } 18 | } 19 | } 20 | 21 | func TestPropertyInitialObserve(t *testing.T) { 22 | prop := NewProperty(10) 23 | var prevStream Stream[int] 24 | for i := 0; i <= 100; i++ { 25 | stream := prop.Observe() 26 | if stream == prevStream { 27 | t.Fatalf("Expecting different stream\n") 28 | } 29 | if val := stream.Value(); val != 10 { 30 | t.Fatalf("Expecting 10 but got %#v\n", val) 31 | } 32 | prevStream = stream 33 | } 34 | } 35 | 36 | func TestPropertyObserveAfterUpdate(t *testing.T) { 37 | prop := NewProperty(10) 38 | if val := prop.Value(); val != 10 { 39 | t.Fatalf("Expecting 10 but got %#v\n", val) 40 | } 41 | prop.Update(15) 42 | if val := prop.Value(); val != 15 { 43 | t.Fatalf("Expecting 15 but got %#v\n", val) 44 | } 45 | stream := prop.Observe() 46 | if val := stream.Value(); val != 15 { 47 | t.Fatalf("Expecting 15 but got %#v\n", val) 48 | } 49 | } 50 | 51 | func TestPropertyMultipleConcurrentReaders(t *testing.T) { 52 | initial := 1000 53 | final := 2000 54 | prop := NewProperty(initial) 55 | var cherrs []chan error 56 | for i := 0; i < 1000; i++ { 57 | cherr := make(chan error, 1) 58 | cherrs = append(cherrs, cherr) 59 | go testStreamRead(prop.Observe(), initial, final, cherr) 60 | } 61 | done := make(chan bool) 62 | go func(prop Property[int], initial, final int, done chan bool) { 63 | defer close(done) 64 | for i := initial + 1; i <= final; i++ { 65 | prop.Update(i) 66 | } 67 | }(prop, initial, final, done) 68 | for _, cherr := range cherrs { 69 | if err := <-cherr; err != nil { 70 | t.Fatal(err) 71 | } 72 | } 73 | <-done 74 | } 75 | 76 | func TestPropertyMultipleConcurrentReadersWriters(t *testing.T) { 77 | wg := &sync.WaitGroup{} 78 | writer := func(prop Property[int], times int) { 79 | defer wg.Done() 80 | for i := 0; i <= times; i++ { 81 | val := prop.Value() 82 | prop.Update(val + 1) 83 | prop.Observe() 84 | } 85 | } 86 | prop := NewProperty(0) 87 | times := 1000 88 | for i := 0; i < 1000; i++ { 89 | wg.Add(1) 90 | go writer(prop, times) 91 | } 92 | wg.Wait() 93 | } 94 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import "context" 4 | 5 | // Stream represents the list of values a property is updated to. For every 6 | // property update, that value is appended to the list in the order they 7 | // happen. The value is discarded once you advance the stream. Please note 8 | // that Stream is not goroutine safe: you cannot use the same stream on 9 | // multiple goroutines concurrently. If you want to use multiple streams for 10 | // the same property, either use Property.Observe (goroutine-safe) or use 11 | // Stream.Clone (before passing it to another goroutine). 12 | type Stream[T any] interface { 13 | // Value returns the current value for this stream. 14 | Value() T 15 | 16 | // Changes returns the channel that is closed when a new value is available. 17 | Changes() chan struct{} 18 | 19 | // Next advances this stream to the next state. 20 | // You should never call this unless Changes channel is closed. 21 | Next() T 22 | 23 | // HasNext checks whether there is a new value available. 24 | HasNext() bool 25 | 26 | // WaitNext waits for Changes to be closed, advances the stream and returns 27 | // the current value. 28 | WaitNext() T 29 | 30 | // WaitNextCtx does the same as WaitNext but returns earlier with an error if the given context is cancelled first. 31 | WaitNextCtx(ctx context.Context) (T, error) 32 | 33 | // Clone creates a new independent stream from this one but sharing the same 34 | // Property. Updates to the property will be reflected in both streams but 35 | // they may have different values depending on when they advance the stream 36 | // with Next. 37 | Clone() Stream[T] 38 | 39 | // Peek return the value in the next state 40 | // You should never call this unless Changes channel is closed. 41 | Peek() T 42 | } 43 | 44 | type stream[T any] struct { 45 | state *state[T] 46 | } 47 | 48 | func (s *stream[T]) Clone() Stream[T] { 49 | return &stream[T]{state: s.state} 50 | } 51 | 52 | func (s *stream[T]) Value() T { 53 | return s.state.value 54 | } 55 | 56 | func (s *stream[T]) Changes() chan struct{} { 57 | return s.state.done 58 | } 59 | 60 | func (s *stream[T]) Next() T { 61 | s.state = s.state.next 62 | return s.state.value 63 | } 64 | 65 | func (s *stream[T]) HasNext() bool { 66 | select { 67 | case <-s.state.done: 68 | return true 69 | default: 70 | return false 71 | } 72 | } 73 | 74 | func (s *stream[T]) WaitNext() T { 75 | <-s.state.done 76 | s.state = s.state.next 77 | return s.state.value 78 | } 79 | 80 | func (s *stream[T]) WaitNextCtx(ctx context.Context) (T, error) { 81 | select { 82 | case <-s.Changes(): 83 | // ensure that context is not canceled, only then advance the stream 84 | if ctx.Err() == nil { 85 | return s.Next(), nil 86 | } 87 | 88 | case <-ctx.Done(): 89 | } 90 | 91 | var zeroVal T 92 | return zeroVal, ctx.Err() 93 | } 94 | 95 | func (s *stream[T]) Peek() T { 96 | return s.state.next.value 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # observer 2 | 3 | [![License](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://github.com/imkira/go-observer/blob/master/LICENSE.txt) 4 | [![GoDoc](https://godoc.org/github.com/imkira/go-observer?status.svg)](https://godoc.org/github.com/imkira/go-observer) 5 | [![Build Status](http://img.shields.io/travis/imkira/go-observer.svg?style=flat)](https://travis-ci.org/imkira/go-observer) 6 | [![Coverage](https://codecov.io/gh/imkira/go-observer/branch/master/graph/badge.svg)](https://codecov.io/gh/imkira/go-observer) 7 | [![codebeat badge](https://codebeat.co/badges/28bdd579-8b34-4940-a3e0-35ac52794a42)](https://codebeat.co/projects/github-com-imkira-go-observer) 8 | [![goreportcard](https://goreportcard.com/badge/github.com/imkira/go-observer)](https://goreportcard.com/report/github.com/imkira/go-observer) 9 | 10 | observer is a [Go](http://golang.org) package that aims to simplify the problem 11 | of channel-based broadcasting of events from one or more publishers to one or 12 | more observers. 13 | 14 | # Problem 15 | 16 | The typical quick-and-dirty approach to notifying a set of observers in go is 17 | to use channels and call each in a for loop, like the following: 18 | 19 | ```go 20 | for _, channel := range channels { 21 | channel <- value 22 | } 23 | ``` 24 | 25 | There are two problems with this approach: 26 | 27 | - The broadcaster blocks every time some channel is not ready to be written to. 28 | - If the broadcaster blocks for some channel, the remaining channels will not 29 | be written to (and therefore not receive the event) until the blocking 30 | channel is finally ready. 31 | - It is O(N). The more observers you have, the worse this loop will behave. 32 | 33 | Of course, this could be solved by creating one goroutine for each channel so 34 | the broadcaster doesn't block. Unfortunately, this is heavy and 35 | resource-consuming. This is especially bad if you have events being raised 36 | frequently and a considerable number of observers. 37 | 38 | # Approach 39 | 40 | The way observer package tackles this problem is very simple. For every event, 41 | a state object containing information about the event, and a channel is 42 | created. State objects are managed using a singly linked list structure: every 43 | state points to the next. When a new event is raised, a new state object is 44 | appended to the list and the channel of the previous state is closed (this 45 | helps notify all observers that the previous state is outdated). 46 | 47 | Package observer defines 2 concepts: 48 | 49 | - Property: An object that is continuously updated by one or more publishers. 50 | - Stream: The list of values a property is updated to. For every property 51 | update, that value is appended to the list in the order they happen, and is 52 | only discarded when you advance to the next value. 53 | 54 | # Memory Usage 55 | 56 | The amount of memory used for one property is not dependent on the number of 57 | observers. It should be proportional to the number of value updates since the 58 | value last obtained by the slowest observer. As long as you keep advancing all 59 | your observers, garbage collection will take place and keep memory usage 60 | stable. 61 | 62 | # How to Use 63 | 64 | First, you need to install the package: 65 | 66 | ``` 67 | go get -u github.com/imkira/go-observer 68 | ``` 69 | 70 | Then, you need to include it in your source: 71 | 72 | ```go 73 | import "github.com/imkira/go-observer" 74 | ``` 75 | 76 | The package will be imported with ```observer``` as name. 77 | 78 | The following example creates one property that is updated every second by one 79 | or more publishers, and observed by one or more observers. 80 | 81 | ## Documentation 82 | 83 | For advanced usage, make sure to check the 84 | [available documentation here](http://godoc.org/github.com/imkira/go-observer). 85 | 86 | ## Example: Creating a Property 87 | 88 | The following code creates a property with initial value ```1```. 89 | 90 | ```go 91 | val := 1 92 | prop := observer.NewProperty(val) 93 | ``` 94 | 95 | After creating the property, you can pass it around to publishers or 96 | observers as you want. 97 | 98 | ## Example: Publisher 99 | 100 | The following code represents a publisher that increments the value of the 101 | property by one every second. 102 | 103 | ```go 104 | val := 1 105 | for { 106 | time.Sleep(time.Second) 107 | val += 1 108 | fmt.Printf("will publish value: %d\n", val) 109 | prop.Update(val) 110 | } 111 | ``` 112 | 113 | Note: 114 | 115 | - Property is goroutine safe: you can use it concurrently from multiple 116 | goroutines. 117 | 118 | ## Example: Observer 119 | 120 | The following code represents an observer that prints the initial value of a 121 | property and waits indefinitely for changes to its value. When there is a 122 | change, the stream is advanced and the current value of the property is 123 | printed. 124 | 125 | ```go 126 | stream := prop.Observe() 127 | val := stream.Value() 128 | fmt.Printf("initial value: %d\n", val) 129 | for { 130 | select { 131 | // wait for changes 132 | case <-stream.Changes(): 133 | // advance to next value 134 | stream.Next() 135 | // new value 136 | val = stream.Value() 137 | fmt.Printf("got new value: %d\n", val) 138 | } 139 | } 140 | ``` 141 | 142 | Note: 143 | 144 | - Stream is not goroutine safe: You must create one stream by calling 145 | ```Property.Observe()``` or ```Stream.Clone()``` if you want to have 146 | concurrent observers for the same property or stream. 147 | 148 | ## Example 149 | 150 | Please check 151 | [examples/multiple.go](https://github.com/imkira/go-observer/blob/master/examples/multiple.go) 152 | for a simple example on how to use multiple observers with a single updater. 153 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestStreamInitialValue(t *testing.T) { 11 | state := newState(10) 12 | stream := &stream[int]{state: state} 13 | if val := stream.Value(); val != 10 { 14 | t.Fatalf("Expecting 10 but got %#v\n", val) 15 | } 16 | } 17 | 18 | func TestStreamUpdate(t *testing.T) { 19 | state1 := newState(10) 20 | state2 := state1.update(15) 21 | stream := &stream[int]{state: state1} 22 | if val := stream.Value(); val != 10 { 23 | t.Fatalf("Expecting 10 but got %#v\n", val) 24 | } 25 | state2.update(15) 26 | if val := stream.Value(); val != 10 { 27 | t.Fatalf("Expecting 10 but got %#v\n", val) 28 | } 29 | } 30 | 31 | func TestStreamNextValue(t *testing.T) { 32 | state1 := newState(10) 33 | stream := &stream[int]{state: state1} 34 | state2 := state1.update(15) 35 | if val := stream.Next(); val != 15 { 36 | t.Fatalf("Expecting 15 but got %#v\n", val) 37 | } 38 | state2.update(20) 39 | if val := stream.Next(); val != 20 { 40 | t.Fatalf("Expecting 20 but got %#v\n", val) 41 | } 42 | } 43 | 44 | func TestStreamDetectsChanges(t *testing.T) { 45 | state := newState(10) 46 | stream := &stream[int]{state: state} 47 | select { 48 | case <-stream.Changes(): 49 | t.Fatalf("Expecting no changes\n") 50 | default: 51 | } 52 | go func() { 53 | time.Sleep(1 * time.Second) 54 | state.update(15) 55 | }() 56 | select { 57 | case <-stream.Changes(): 58 | case <-time.After(2 * time.Second): 59 | t.Fatalf("Expecting changes\n") 60 | } 61 | select { 62 | case <-stream.Changes(): 63 | default: 64 | t.Fatalf("Expecting changes\n") 65 | } 66 | if val := stream.Next(); val != 15 { 67 | t.Fatalf("Expecting 15 but got %#v\n", val) 68 | } 69 | select { 70 | case <-stream.Changes(): 71 | t.Fatalf("Expecting no changes\n") 72 | default: 73 | } 74 | } 75 | 76 | func TestStreamHasChanges(t *testing.T) { 77 | state := newState(10) 78 | stream := &stream[int]{state: state} 79 | if stream.HasNext() { 80 | t.Fatalf("Expecting no changes\n") 81 | } 82 | state.update(15) 83 | if !stream.HasNext() { 84 | t.Fatalf("Expecting changes\n") 85 | } 86 | if val := stream.Next(); val != 15 { 87 | t.Fatalf("Expecting 15 but got %#v\n", val) 88 | } 89 | } 90 | 91 | func TestStreamWaitsNext(t *testing.T) { 92 | state := newState(10) 93 | stream := &stream[int]{state: state} 94 | for i := 15; i <= 100; i++ { 95 | state = state.update(i) 96 | if val := stream.WaitNext(); val != i { 97 | t.Fatalf("Expecting %#v but got %#v\n", i, val) 98 | } 99 | } 100 | if stream.HasNext() { 101 | t.Fatalf("Expecting no changes\n") 102 | } 103 | } 104 | 105 | func TestStreamWaitNextBackgroundCtx(t *testing.T) { 106 | state := newState(0) 107 | stream := &stream[int]{state: state} 108 | for i := 7; i <= 133; i++ { 109 | state = state.update(i) 110 | got, err := stream.WaitNextCtx(context.Background()) 111 | if err != nil { 112 | t.Fatalf("Expecting no error\n") 113 | } 114 | if got != i { 115 | t.Fatalf("Expecting %#v but got %#v\n", i, got) 116 | } 117 | } 118 | 119 | if stream.HasNext() { 120 | t.Fatalf("Expecting no changes\n") 121 | } 122 | } 123 | 124 | func TestStreamWaitNextCanceledCtx(t *testing.T) { 125 | ctx, cancel := context.WithCancel(context.Background()) 126 | initialVal := 99 127 | state := newState(initialVal) 128 | stream := &stream[int]{state: state} 129 | 130 | // cancel the context 131 | cancel() 132 | 133 | updateVal := initialVal + 17 134 | state = state.update(updateVal) 135 | 136 | for i := 0; i < 100; i++ { 137 | // ensure the method returns an error when a canceled context is used and doesn't advance the stream 138 | _, err := stream.WaitNextCtx(ctx) 139 | if err == nil { 140 | t.Fatalf("Expecting error but got none\n") 141 | } 142 | if stream.Value() != initialVal { 143 | t.Fatalf("Expecting stream's current value to be %#v but it is %#v\n", initialVal, stream.Value()) 144 | } 145 | } 146 | 147 | // check that a call with a non-canceled context works 148 | got, err := stream.WaitNextCtx(context.Background()) 149 | if err != nil { 150 | t.Fatalf("Expecting no error\n") 151 | } 152 | if got != updateVal { 153 | t.Fatalf("Expecting %#v but got %#v\n", updateVal, got) 154 | } 155 | 156 | if stream.HasNext() { 157 | t.Fatalf("Expecting no changes\n") 158 | } 159 | } 160 | 161 | func TestStreamClone(t *testing.T) { 162 | state := newState(10) 163 | stream1 := &stream[int]{state: state} 164 | stream2 := stream1.Clone() 165 | if stream2.HasNext() { 166 | t.Fatalf("Expecting no changes\n") 167 | } 168 | if val := stream2.Value(); val != 10 { 169 | t.Fatalf("Expecting 10 but got %#v\n", val) 170 | } 171 | state.update(15) 172 | if !stream1.HasNext() { 173 | t.Fatalf("Expecting changes\n") 174 | } 175 | if !stream2.HasNext() { 176 | t.Fatalf("Expecting changes\n") 177 | } 178 | stream1.Next() 179 | if val := stream1.Value(); val != 15 { 180 | t.Fatalf("Expecting 15 but got %#v\n", val) 181 | } 182 | if val := stream2.Value(); val != 10 { 183 | t.Fatalf("Expecting 10 but got %#v\n", val) 184 | } 185 | stream2.Next() 186 | if val := stream2.Value(); val != 15 { 187 | t.Fatalf("Expecting 15 but got %#v\n", val) 188 | } 189 | if stream1.HasNext() { 190 | t.Fatalf("Expecting no changes\n") 191 | } 192 | if stream2.HasNext() { 193 | t.Fatalf("Expecting no changes\n") 194 | } 195 | } 196 | 197 | func TestStreamPeek(t *testing.T) { 198 | state := newState(10) 199 | stream := &stream[int]{state: state} 200 | state = state.update(15) 201 | if val := stream.Peek(); val != 15 { 202 | t.Fatalf("Expecting 15 but got %#v\n", val) 203 | } 204 | state = state.update(20) 205 | if val := stream.Peek(); val != 15 { 206 | t.Fatalf("Expecting 15 but got %#v\n", val) 207 | } 208 | stream.Next() 209 | if val := stream.Peek(); val != 20 { 210 | t.Fatalf("Expecting 20 but got %#v\n", val) 211 | } 212 | } 213 | 214 | func TestStreamConcurrencyWithClones(t *testing.T) { 215 | initial := 1000 216 | final := 2000 217 | prop := NewProperty(initial) 218 | stream := prop.Observe() 219 | var cherrs []chan error 220 | for i := 0; i < 1000; i++ { 221 | cherr := make(chan error, 1) 222 | cherrs = append(cherrs, cherr) 223 | go testStreamRead(stream.Clone(), initial, final, cherr) 224 | } 225 | done := make(chan bool) 226 | go func(prop Property[int], initial, final int, done chan bool) { 227 | defer close(done) 228 | for i := initial + 1; i <= final; i++ { 229 | prop.Update(i) 230 | } 231 | }(prop, initial, final, done) 232 | for _, cherr := range cherrs { 233 | if err := <-cherr; err != nil { 234 | t.Fatal(err) 235 | } 236 | } 237 | <-done 238 | } 239 | 240 | func testStreamRead(s Stream[int], initial, final int, err chan error) { 241 | val := s.Value() 242 | if val != initial { 243 | err <- fmt.Errorf("Expecting %#v but got %#v\n", initial, val) 244 | return 245 | } 246 | for i := initial + 1; i <= final; i++ { 247 | prevVal := val 248 | val = s.WaitNext() 249 | expected := prevVal + 1 250 | if val != expected { 251 | err <- fmt.Errorf("Expecting %#v but got %#v\n", expected, val) 252 | return 253 | } 254 | } 255 | close(err) 256 | } 257 | --------------------------------------------------------------------------------