├── .circleci └── config.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── base_signal.go ├── example ├── README.md ├── example │ ├── async.go │ ├── records.go │ ├── signals.go │ └── sync.go └── main.go ├── go.mod ├── go.sum ├── new.go ├── signal_listener.go ├── signals.go ├── signals_async.go ├── signals_sync.go ├── signals_test.go └── test-coverage.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Define a job to be invoked later in a workflow. 6 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 7 | jobs: 8 | build: 9 | working_directory: ~/repo 10 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 11 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 12 | docker: 13 | - image: cimg/go:1.18 14 | # Add steps to the job 15 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | keys: 20 | - go-mod-v4-{{ checksum "go.sum" }} 21 | - run: 22 | name: Install Dependencies 23 | command: go mod download 24 | - save_cache: 25 | key: go-mod-v4-{{ checksum "go.sum" }} 26 | paths: 27 | - "/go/pkg/mod" 28 | - run: 29 | name: Run tests 30 | command: | 31 | mkdir -p /tmp/test-reports 32 | gotestsum --junitfile /tmp/test-reports/unit-tests.xml 33 | - store_test_results: 34 | path: /tmp/test-reports 35 | 36 | # Invoke jobs via workflows 37 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 38 | workflows: 39 | sample: # This is the name of the workflow, feel free to change it to better match your workflow. 40 | # Inside the workflow, you define the jobs you want to run. 41 | jobs: 42 | - build 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .coverages 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Sync", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/example/main.go", 13 | }, 14 | { 15 | "name": "Run Async", 16 | "type": "go", 17 | "request": "launch", 18 | "mode": "auto", 19 | "program": "${workspaceFolder}/example/main.go", 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ManiarTech®️ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signals 2 | 3 | The `signals` a robust, dependency-free go library that provides simple, thin, and user-friendly pub-sub kind of in-process event system for your Go applications. It allows you to generate and emit signals (synchronously or asynchronously) as well as manage listeners. 4 | 5 | 💯 **100% test coverage** 💯 6 | 7 | ⚠️ **Note:** This project is stable, production-ready and complete. It is used in production by [ManiarTech®️](https://maniartech.com) and other companies. Hence, we won't be adding any new features to this project. However, we will continue to maintain it by fixing bugs and keeping it up-to-date with the latest Go versions. We shall however, be adding new features when the need arises and / or requested by the community. 8 | 9 | [![GoReportCard example](https://goreportcard.com/badge/github.com/nanomsg/mangos)](https://goreportcard.com/report/github.com/maniartech/signals) 10 | [![](https://circleci.com/gh/maniartech/signals.svg?style=shield)](https://circleci.com/gh/maniartech/signals) 11 | [![made-with-Go](https://img.shields.io/badge/Made%20with-Go-1f425f.svg)](https://go.dev/) 12 | [![GoDoc reference example](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/maniartech/signals) 13 | 14 | ## Installation 15 | 16 | ```bash 17 | go get github.com/maniartech/signals 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "context" 27 | "fmt" 28 | "github.com/maniartech/signals" 29 | ) 30 | 31 | var RecordCreated = signals.New[Record]() 32 | var RecordUpdated = signals.New[Record]() 33 | var RecordDeleted = signals.New[Record]() 34 | 35 | func main() { 36 | 37 | // Add a listener to the RecordCreated signal 38 | RecordCreated.AddListener(func(ctx context.Context, record Record) { 39 | fmt.Println("Record created:", record) 40 | }, "key1") // <- Key is optional useful for removing the listener later 41 | 42 | // Add a listener to the RecordUpdated signal 43 | RecordUpdated.AddListener(func(ctx context.Context, record Record) { 44 | fmt.Println("Record updated:", record) 45 | }) 46 | 47 | // Add a listener to the RecordDeleted signal 48 | RecordDeleted.AddListener(func(ctx context.Context, record Record) { 49 | fmt.Println("Record deleted:", record) 50 | }) 51 | 52 | ctx := context.Background() 53 | 54 | // Emit the RecordCreated signal 55 | RecordCreated.Emit(ctx, Record{ID: 1, Name: "John"}) 56 | 57 | // Emit the RecordUpdated signal 58 | RecordUpdated.Emit(ctx, Record{ID: 1, Name: "John Doe"}) 59 | 60 | // Emit the RecordDeleted signal 61 | RecordDeleted.Emit(ctx, Record{ID: 1, Name: "John Doe"}) 62 | } 63 | ``` 64 | 65 | ## Documentation 66 | 67 | [![GoDoc](https://godoc.org/github.com/maniartech/signals?status.svg)](https://godoc.org/github.com/maniartech/signals) 68 | 69 | ## License 70 | 71 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 72 | 73 | ## ✨You Need Some Go Experts, Right? ✨ 74 | 75 | As a software development firm, ManiarTech® specializes in Golang-based projects. Our team has an in-depth understanding of Enterprise Process Automation, Open Source, and SaaS. Also, we have extensive experience porting code from Python and Node.js to Golang. We have a team of Golang experts here at ManiarTech® that is well-versed in all aspects of the language and its ecosystem. 76 | At ManiarTech®, we have a team of Golang experts who are well-versed in all facets of the technology. 77 | 78 | In short, if you're looking for experts to assist you with Golang-related projects, don't hesitate to get in touch with us. Send an email to to get in touch. 79 | 80 | ## 👉🏼 Do you consider yourself an "Expert Golang Developer"? 👈🏼 81 | 82 | If so, you may be interested in the challenging and rewarding work that is waiting for you. Use to submit your resume. 83 | -------------------------------------------------------------------------------- /base_signal.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import "context" 4 | 5 | // keyedListener represents a combination of a listener and an optional key used for identification. 6 | type keyedListener[T any] struct { 7 | key string 8 | listener SignalListener[T] 9 | } 10 | 11 | // BaseSignal provides the base implementation of the Signal interface. 12 | // It is intended to be used as an abstract base for underlying signal mechanisms. 13 | // 14 | // Example: 15 | // type MyDerivedSignal[T any] struct { 16 | // BaseSignal[T] 17 | // // Additional fields or methods specific to MyDerivedSignal 18 | // } 19 | // 20 | // func (s *MyDerivedSignal[T]) Emit(ctx context.Context, payload T) { 21 | // // Custom implementation for emitting the signal 22 | // } 23 | type BaseSignal[T any] struct { 24 | subscribers []keyedListener[T] 25 | subscribersMap map[string]SignalListener[T] 26 | } 27 | 28 | // AddListener adds a listener to the signal. The listener will be called 29 | // whenever the signal is emitted. It returns the number of subscribers after 30 | // the listener was added. It accepts an optional key that can be used to remove 31 | // the listener later or to check if the listener was already added. It returns 32 | // -1 if the listener with the same key was already added to the signal. 33 | // 34 | // Example: 35 | // signal := signals.New[int]() 36 | // count := signal.AddListener(func(ctx context.Context, payload int) { 37 | // // Listener implementation 38 | // // ... 39 | // }, "key1") 40 | // fmt.Println("Number of subscribers after adding listener:", count) 41 | func (s *BaseSignal[T]) AddListener(listener SignalListener[T], key ...string) int { 42 | if len(key) > 0 { 43 | if _, ok := s.subscribersMap[key[0]]; ok { 44 | return -1 45 | } 46 | s.subscribersMap[key[0]] = listener 47 | s.subscribers = append(s.subscribers, keyedListener[T]{ 48 | key: key[0], 49 | listener: listener, 50 | }) 51 | } else { 52 | s.subscribers = append(s.subscribers, keyedListener[T]{ 53 | listener: listener, 54 | }) 55 | } 56 | 57 | return len(s.subscribers) 58 | } 59 | 60 | // RemoveListener removes a listener from the signal. It returns the number 61 | // of subscribers after the listener was removed. It returns -1 if the 62 | // listener was not found. 63 | // 64 | // Example: 65 | // signal := signals.New[int]() 66 | // signal.AddListener(func(ctx context.Context, payload int) { 67 | // // Listener implementation 68 | // // ... 69 | // }, "key1") 70 | // count := signal.RemoveListener("key1") 71 | // fmt.Println("Number of subscribers after removing listener:", count) 72 | func (s *BaseSignal[T]) RemoveListener(key string) int { 73 | if _, ok := s.subscribersMap[key]; ok { 74 | delete(s.subscribersMap, key) 75 | 76 | for i, sub := range s.subscribers { 77 | if sub.key == key { 78 | s.subscribers = append(s.subscribers[:i], s.subscribers[i+1:]...) 79 | break 80 | } 81 | } 82 | return len(s.subscribers) 83 | } 84 | 85 | return -1 86 | } 87 | 88 | // Reset resets the signal by removing all subscribers from the signal, 89 | // effectively clearing the list of subscribers. 90 | // This can be used when you want to stop all listeners from receiving 91 | // further signals. 92 | // 93 | // Example: 94 | // signal := signals.New[int]() 95 | // signal.AddListener(func(ctx context.Context, payload int) { 96 | // // Listener implementation 97 | // // ... 98 | // }) 99 | // signal.Reset() // Removes all listeners 100 | // fmt.Println("Number of subscribers after resetting:", signal.Len()) 101 | func (s *BaseSignal[T]) Reset() { 102 | s.subscribers = make([]keyedListener[T], 0) 103 | s.subscribersMap = make(map[string]SignalListener[T]) 104 | } 105 | 106 | // Len returns the number of listeners subscribed to the signal. 107 | // This can be used to check how many listeners are currently waiting for a signal. 108 | // The returned value is of type int. 109 | // 110 | // Example: 111 | // signal := signals.New[int]() 112 | // signal.AddListener(func(ctx context.Context, payload int) { 113 | // // Listener implementation 114 | // // ... 115 | // }) 116 | // fmt.Println("Number of subscribers:", signal.Len()) 117 | func (s *BaseSignal[T]) Len() int { 118 | return len(s.subscribers) 119 | } 120 | 121 | // IsEmpty checks if the signal has any subscribers. 122 | // It returns true if the signal has no subscribers, and false otherwise. 123 | // This can be used to check if there are any listeners before emitting a signal. 124 | // 125 | // Example: 126 | // signal := signals.New[int]() 127 | // fmt.Println("Is signal empty?", signal.IsEmpty()) // Should print true 128 | // signal.AddListener(func(ctx context.Context, payload int) { 129 | // // Listener implementation 130 | // // ... 131 | // }) 132 | // fmt.Println("Is signal empty?", signal.IsEmpty()) // Should print false 133 | func (s *BaseSignal[T]) IsEmpty() bool { 134 | return len(s.subscribers) == 0 135 | } 136 | 137 | // Emit is not implemented in BaseSignal and panics if called. It should be 138 | // implemented by a derived type. 139 | // 140 | // Example: 141 | // type MyDerivedSignal[T any] struct { 142 | // BaseSignal[T] 143 | // // Additional fields or methods specific to MyDerivedSignal 144 | // } 145 | // 146 | // func (s *MyDerivedSignal[T]) Emit(ctx context.Context, payload T) { 147 | // // Custom implementation for emitting the signal 148 | // } 149 | func (s *BaseSignal[T]) Emit(ctx context.Context, payload T) { 150 | panic("implement me in derived type") 151 | } 152 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Run Signal Example 2 | 3 | This example shows how to use the `signals` package for managing and handling synchronous and asynchronous signals. Sync and async code is written in `sync.go` and `async.go` files respectively. To run the example, execute the following command: 4 | 5 | ```bash 6 | # Run async example 7 | go run example/main.go 8 | ``` 9 | 10 | ```bash 11 | # Run sync example 12 | go run example/main.go -sync 13 | ``` 14 | -------------------------------------------------------------------------------- /example/example/async.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | func RunAsync() { 9 | 10 | // Add a listener to the RecordCreatedAsync signal 11 | RecordCreated.AddListener(func(ctx context.Context, record Record) { 12 | fmt.Println("Record created:", record) 13 | }, "key1") // <- Key is optional useful for removing the listener later 14 | 15 | // Add a listener to the RecordUpdatedAsync signal 16 | RecordUpdated.AddListener(func(ctx context.Context, record Record) { 17 | fmt.Println("Record updated:", record) 18 | }) 19 | 20 | // Add a listener to the RecordDeleted signal 21 | RecordDeleted.AddListener(func(ctx context.Context, record Record) { 22 | fmt.Println("Record deleted:", record) 23 | }) 24 | 25 | ctx := context.Background() 26 | 27 | // Emit the RecordCreatedAsync signal 28 | RecordCreated.Emit(ctx, Record{ID: 3, Name: "Record C"}) 29 | 30 | // Emit the RecordUpdated signal 31 | RecordUpdated.Emit(ctx, Record{ID: 2, Name: "Record B"}) 32 | 33 | // Emit the RecordDeleted signal 34 | RecordDeleted.Emit(ctx, Record{ID: 1, Name: "Record A"}) 35 | } 36 | -------------------------------------------------------------------------------- /example/example/records.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | // Record is a payload for the signals 4 | type Record struct { 5 | ID int 6 | 7 | Name string 8 | } 9 | -------------------------------------------------------------------------------- /example/example/signals.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "github.com/maniartech/signals" 4 | 5 | // Asynchonous signals 6 | var RecordCreated = signals.New[Record]() 7 | var RecordUpdated = signals.New[Record]() 8 | var RecordDeleted = signals.New[Record]() 9 | 10 | // Synchonous signals 11 | var RecordCreatedSync = signals.NewSync[Record]() 12 | var RecordUpdatedSync = signals.NewSync[Record]() 13 | var RecordDeletedSync = signals.NewSync[Record]() 14 | -------------------------------------------------------------------------------- /example/example/sync.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | func RunSync() { 9 | 10 | // Add a listener to the RecordCreated signal 11 | RecordCreatedSync.AddListener(func(ctx context.Context, record Record) { 12 | fmt.Println("Record created:", record) 13 | }, "key1") // <- Key is optional useful for removing the listener later 14 | 15 | // Add a listener to the RecordUpdated signal 16 | RecordUpdatedSync.AddListener(func(ctx context.Context, record Record) { 17 | fmt.Println("Record updated:", record) 18 | }) 19 | 20 | // Add a listener to the RecordDeleted signal 21 | RecordDeletedSync.AddListener(func(ctx context.Context, record Record) { 22 | fmt.Println("Record deleted:", record) 23 | }) 24 | 25 | ctx := context.Background() 26 | 27 | // Emit the RecordCreated signal 28 | RecordCreatedSync.Emit(ctx, Record{ID: 3, Name: "Record C"}) 29 | 30 | // Emit the RecordUpdated signal 31 | RecordUpdatedSync.Emit(ctx, Record{ID: 2, Name: "Record B"}) 32 | 33 | // Emit the RecordDeleted signal 34 | RecordDeletedSync.Emit(ctx, Record{ID: 1, Name: "Record A"}) 35 | } 36 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/maniartech/signals/example/example" 8 | ) 9 | 10 | func main() { 11 | 12 | // If the first argument is "-sync" then run the async example 13 | if len(os.Args) > 1 && os.Args[1] == "-sync" { 14 | example.RunSync() 15 | } else { 16 | example.RunAsync() 17 | } 18 | 19 | // Wait for a second to let the signals to be processed 20 | time.Sleep(time.Second) 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maniartech/signals 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maniartech/signals/75ea4d4a75b21812b5fc91bfd9597c9b6a79c600/go.sum -------------------------------------------------------------------------------- /new.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | // NewSync creates a new signal that can be used to emit and listen to events 4 | // synchronously. 5 | // 6 | // Example: 7 | // signal := signals.NewSync[int]() 8 | // signal.AddListener(func(ctx context.Context, payload int) { 9 | // // Listener implementation 10 | // // ... 11 | // }) 12 | // signal.Emit(context.Background(), 42) 13 | func NewSync[T any]() Signal[T] { 14 | s := &SyncSignal[T]{} 15 | s.Reset() 16 | return s 17 | } 18 | 19 | // New creates a new signal that can be used to emit and listen to events 20 | // asynchronously. 21 | // 22 | // Example: 23 | // signal := signals.New[int]() 24 | // signal.AddListener(func(ctx context.Context, payload int) { 25 | // // Listener implementation 26 | // // ... 27 | // }) 28 | // signal.Emit(context.Background(), 42) 29 | func New[T any]() Signal[T] { 30 | s := &AsyncSignal[T]{} 31 | s.Reset() // Reset the signal 32 | return s 33 | } 34 | -------------------------------------------------------------------------------- /signal_listener.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import "context" 4 | 5 | // SignalListener is a type definition for a function that will act as a 6 | // listener for signals. This function takes two parameters: 7 | // 1. A context of type `context.Context`. This is typically used for timeout 8 | // and cancellation signals, and can carry request-scoped values across API 9 | // boundaries and between processes. 10 | // 2. A payload of generic type `T`. This can be any type, and represents the 11 | // data or signal that the listener function will process. 12 | // 13 | // The function does not return any value. 14 | type SignalListener[T any] func(context.Context, T) 15 | -------------------------------------------------------------------------------- /signals.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import "context" 4 | 5 | // Signal is the interface that represents a signal that can be subscribed to 6 | // emitting a payload of type T. 7 | type Signal[T any] interface { 8 | // Emit notifies all subscribers of the signal and passes the context and the payload. 9 | // 10 | // If the context has a deadline or cancellable property, the listeners 11 | // must respect it. If the signal is async (default), the listeners are called 12 | // in a separate goroutine. 13 | // 14 | // Example: 15 | // signal := signals.New[int]() 16 | // signal.AddListener(func(ctx context.Context, payload int) { 17 | // // Listener implementation 18 | // // ... 19 | // }) 20 | // signal.Emit(context.Background(), 42) 21 | Emit(ctx context.Context, payload T) 22 | 23 | // AddListener adds a listener to the signal. 24 | // 25 | // The listener will be called whenever the signal is emitted. It returns the 26 | // number of subscribers after the listener was added. It accepts an optional key 27 | // that can be used to remove the listener later or to check if the listener 28 | // was already added. It returns -1 if the listener with the same key 29 | // was already added to the signal. 30 | // 31 | // Example: 32 | // signal := signals.NewSync[int]() 33 | // count := signal.AddListener(func(ctx context.Context, payload int) { 34 | // // Listener implementation 35 | // // ... 36 | // }) 37 | // fmt.Println("Number of subscribers after adding listener:", count) 38 | AddListener(handler SignalListener[T], key ...string) int 39 | 40 | // RemoveListener removes a listener from the signal. 41 | // 42 | // It returns the number of subscribers after the listener was removed. 43 | // It returns -1 if the listener was not found. 44 | // 45 | // Example: 46 | // signal := signals.NewSync[int]() 47 | // signal.AddListener(func(ctx context.Context, payload int) { 48 | // // Listener implementation 49 | // // ... 50 | // }, "key1") 51 | // count := signal.RemoveListener("key1") 52 | // fmt.Println("Number of subscribers after removing listener:", count) 53 | RemoveListener(key string) int 54 | 55 | // Reset resets the signal by removing all subscribers from the signal, 56 | // effectively clearing the list of subscribers. 57 | // 58 | // This can be used when you want to stop all listeners from receiving 59 | // further signals. 60 | // 61 | // Example: 62 | // signal := signals.New[int]() 63 | // signal.AddListener(func(ctx context.Context, payload int) { 64 | // // Listener implementation 65 | // // ... 66 | // }) 67 | // signal.Reset() // Removes all listeners 68 | // fmt.Println("Number of subscribers after resetting:", signal.Len()) 69 | Reset() 70 | 71 | // Len returns the number of listeners subscribed to the signal. 72 | // 73 | // This can be used to check how many listeners are currently waiting for a signal. 74 | // The returned value is of type int. 75 | // 76 | // Example: 77 | // signal := signals.NewSync[int]() 78 | // signal.AddListener(func(ctx context.Context, payload int) { 79 | // // Listener implementation 80 | // // ... 81 | // }) 82 | // fmt.Println("Number of subscribers:", signal.Len()) 83 | Len() int 84 | 85 | // IsEmpty checks if the signal has any subscribers. 86 | // 87 | // It returns true if the signal has no subscribers, and false otherwise. 88 | // This can be used to check if there are any listeners before emitting a signal. 89 | // 90 | // Example: 91 | // signal := signals.New[int]() 92 | // fmt.Println("Is signal empty?", signal.IsEmpty()) // Should print true 93 | // signal.AddListener(func(ctx context.Context, payload int) { 94 | // // Listener implementation 95 | // // ... 96 | // }) 97 | // fmt.Println("Is signal empty?", signal.IsEmpty()) // Should print false 98 | IsEmpty() bool 99 | } 100 | -------------------------------------------------------------------------------- /signals_async.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // AsyncSignal is a struct that implements the Signal interface. 9 | // This is the default implementation. It provides the same functionality as 10 | // the SyncSignal but the listeners are called in a separate goroutine. 11 | // This means that all listeners are called asynchronously. However, the method 12 | // waits for all the listeners to finish before returning. If you don't want 13 | // to wait for the listeners to finish, you can call the Emit method 14 | // in a separate goroutine. 15 | type AsyncSignal[T any] struct { 16 | BaseSignal[T] 17 | 18 | mu sync.Mutex 19 | } 20 | 21 | // Emit notifies all subscribers of the signal and passes the payload in a 22 | // asynchronous way. 23 | // 24 | // If the context has a deadline or cancellable property, the listeners 25 | // must respect it. This means that the listeners should stop processing when 26 | // the context is cancelled. While emtting it calls the listeners in separate 27 | // goroutines, so the listeners are called asynchronously. However, it 28 | // waits for all the listeners to finish before returning. If you don't want 29 | // to wait for the listeners to finish, you can call the Emit method. Also, 30 | // you must know that Emit does not guarantee the type safety of the emitted value. 31 | // 32 | // Example: 33 | // 34 | // signal := signals.New[string]() 35 | // signal.AddListener(func(ctx context.Context, payload string) { 36 | // // Listener implementation 37 | // // ... 38 | // }) 39 | // 40 | // signal.Emit(context.Background(), "Hello, world!") 41 | func (s *AsyncSignal[T]) Emit(ctx context.Context, payload T) { 42 | s.mu.Lock() 43 | defer s.mu.Unlock() 44 | 45 | var wg sync.WaitGroup 46 | 47 | for _, sub := range s.subscribers { 48 | wg.Add(1) 49 | go func(listener func(context.Context, T)) { 50 | defer wg.Done() 51 | listener(ctx, payload) 52 | }(sub.listener) 53 | } 54 | 55 | wg.Wait() 56 | } 57 | -------------------------------------------------------------------------------- /signals_sync.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import "context" 4 | 5 | // SyncSignal is a struct that implements the Signal interface. 6 | // It provides a synchronous way of notifying all subscribers of a signal. 7 | // The type parameter `T` is a placeholder for any type. 8 | type SyncSignal[T any] struct { 9 | BaseSignal[T] 10 | } 11 | 12 | // Emit notifies all subscribers of the signal and passes the payload in a 13 | // synchronous way. 14 | // 15 | // The payload is of the same type as the SyncSignal's type parameter `T`. 16 | // The method iterates over the subscribers slice of the SyncSignal, 17 | // and for each subscriber, it calls the subscriber's listener function, 18 | // passing the context and the payload. 19 | // If the context has a deadline or cancellable property, the listeners 20 | // must respect it. This means that the listeners should stop processing when 21 | // the context is cancelled. Unlike the AsyncSignal's Emit method, this method 22 | // does not call the listeners in separate goroutines, so the listeners are 23 | // called synchronously, one after the other. 24 | // 25 | // Example: 26 | // signal := signals.NewSync[string]() 27 | // signal.AddListener(func(ctx context.Context, payload string) { 28 | // // Listener implementation 29 | // // ... 30 | // }) 31 | // 32 | // signal.Emit(context.Background(), "Hello, world!") 33 | func (s *SyncSignal[T]) Emit(ctx context.Context, payload T) { 34 | for _, sub := range s.subscribers { 35 | sub.listener(ctx, payload) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /signals_test.go: -------------------------------------------------------------------------------- 1 | package signals_test 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/maniartech/signals" 11 | ) 12 | 13 | func TestSignal(t *testing.T) { 14 | testSignal := signals.NewSync[int]() 15 | 16 | results := make([]int, 0) 17 | testSignal.AddListener(func(ctx context.Context, v int) { 18 | results = append(results, v) 19 | }) 20 | 21 | testSignal.AddListener(func(ctx context.Context, v int) { 22 | results = append(results, v) 23 | }) 24 | 25 | ctx := context.Background() 26 | testSignal.Emit(ctx, 1) 27 | testSignal.Emit(ctx, 2) 28 | testSignal.Emit(ctx, 3) 29 | 30 | if len(results) != 6 { 31 | t.Error("Count must be 6") 32 | } 33 | 34 | if reflect.DeepEqual(results, []int{1, 1, 2, 2, 3, 3}) == false { 35 | t.Error("Results must be [1, 1, 2, 2, 3, 3]") 36 | } 37 | } 38 | 39 | func TestSignalAsync(t *testing.T) { 40 | 41 | var count int 42 | wg := &sync.WaitGroup{} 43 | wg.Add(6) 44 | 45 | testSignal := signals.New[int]() 46 | testSignal.AddListener(func(ctx context.Context, v int) { 47 | time.Sleep(100 * time.Millisecond) 48 | count += 1 49 | wg.Done() 50 | }) 51 | testSignal.AddListener(func(ctx context.Context, v int) { 52 | time.Sleep(100 * time.Millisecond) 53 | count += 1 54 | wg.Done() 55 | }) 56 | 57 | ctx := context.Background() 58 | go testSignal.Emit(ctx, 1) 59 | go testSignal.Emit(ctx, 2) 60 | go testSignal.Emit(ctx, 3) 61 | 62 | if count >= 6 { 63 | t.Error("Not asynchronus! count must be less than 6") 64 | } 65 | 66 | wg.Wait() 67 | 68 | if count != 6 { 69 | t.Error("Count must be 6") 70 | } 71 | } 72 | 73 | // Test Async with Timeout Context. After the context is cancelled, the 74 | // listeners should cancel their execution. 75 | func TestSignalAsyncWithTimeout(t *testing.T) { 76 | 77 | var count int 78 | 79 | timeoutCount := 0 80 | 81 | testSignal := signals.New[int]() 82 | testSignal.AddListener(func(ctx context.Context, v int) { 83 | time.Sleep(100 * time.Millisecond) 84 | select { 85 | case <-ctx.Done(): 86 | timeoutCount += 1 87 | default: 88 | count += 1 89 | } 90 | }) 91 | testSignal.AddListener(func(ctx context.Context, v int) { 92 | time.Sleep(500 * time.Millisecond) 93 | select { 94 | case <-ctx.Done(): 95 | timeoutCount += 1 96 | default: 97 | count += 1 98 | } 99 | }) 100 | 101 | ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) 102 | defer cancel() 103 | testSignal.Emit(ctx, 1) 104 | 105 | ctx2, cancel2 := context.WithTimeout(context.Background(), 50*time.Millisecond) 106 | defer cancel2() 107 | testSignal.Emit(ctx2, 1) 108 | 109 | ctx3, cancel3 := context.WithTimeout(context.Background(), 1000*time.Millisecond) 110 | defer cancel3() 111 | testSignal.Emit(ctx3, 3) 112 | 113 | // The code is checking if the value of the `count` variable is equal to 3 and if 114 | // the value of the `timeoutCount` variable is equal to 3. If either of these 115 | // conditions is not met, an error message is printed. 116 | if count != 3 { 117 | t.Error("Count must be 3") 118 | } 119 | 120 | if timeoutCount != 3 { 121 | t.Error("timeoutCount must be 3") 122 | } 123 | } 124 | 125 | func TestAddRemoveListener(t *testing.T) { 126 | testSignal := signals.New[int]() 127 | 128 | t.Run("AddListener", func(t *testing.T) { 129 | testSignal.AddListener(func(ctx context.Context, v int) { 130 | // Do something 131 | }) 132 | 133 | testSignal.AddListener(func(ctx context.Context, v int) { 134 | // Do something 135 | }, "test-key") 136 | 137 | if testSignal.Len() != 2 { 138 | t.Error("Count must be 2") 139 | } 140 | 141 | if count := testSignal.AddListener(func(ctx context.Context, v int) { 142 | 143 | }, "test-key"); count != -1 { 144 | t.Error("Count must be -1") 145 | } 146 | }) 147 | 148 | t.Run("RemoveListener", func(t *testing.T) { 149 | if count := testSignal.RemoveListener("test-key"); count != 1 { 150 | t.Error("Count must be 1") 151 | } 152 | 153 | if count := testSignal.RemoveListener("test-key"); count != -1 { 154 | t.Error("Count must be -1") 155 | } 156 | }) 157 | 158 | t.Run("Reset", func(t *testing.T) { 159 | testSignal.Reset() 160 | if !testSignal.IsEmpty() { 161 | t.Error("Count must be 0") 162 | } 163 | }) 164 | 165 | } 166 | 167 | // TestBaseSignal tests the BaseSignal to make sure 168 | // Emit throws a panic because it is a base class. 169 | func TestBaseSignal(t *testing.T) { 170 | testSignal := signals.BaseSignal[int]{} 171 | 172 | defer func() { 173 | if r := recover(); r == nil { 174 | t.Error("Emit should throw a panic") 175 | } 176 | }() 177 | 178 | testSignal.Emit(context.Background(), 1) 179 | } 180 | -------------------------------------------------------------------------------- /test-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is used to generate test coverage reports for the 4 | # go project. It is intended to be run from the root of the project 5 | # directory. 6 | 7 | # filename is a timestamped filename for the coverage report in the 8 | # .coverages directory. 9 | filename=".coverages/coverage-$(date +%s).out" 10 | 11 | # If the .coverages directory does not exist, create it. 12 | if [ ! -d ".coverages" ]; then 13 | mkdir .coverages 14 | fi 15 | 16 | echo "Running tests and generating coverage report..." 17 | go test -coverprofile=$filename 18 | 19 | echo "Opening coverage report..." 20 | go tool cover -html=$filename 21 | 22 | echo "Done." 23 | --------------------------------------------------------------------------------