├── book.json ├── example ├── chatterbox │ ├── etoe │ │ ├── etoe.go │ │ └── etoe_test.go │ ├── chatterbox.go │ ├── messages │ │ ├── servermsgtype_string.go │ │ ├── clientmsgtype_string.go │ │ └── messages.go │ ├── server │ │ ├── state │ │ │ ├── state.go │ │ │ ├── actions │ │ │ │ └── actions.go │ │ │ ├── data │ │ │ │ └── data.go │ │ │ ├── modifiers │ │ │ │ ├── modifiers.go │ │ │ │ └── modifiers_test.go │ │ │ └── middleware │ │ │ │ └── middleware.go │ │ └── server.go │ └── client │ │ ├── chatterbox │ │ └── cli │ │ │ ├── chatterbox.go │ │ │ └── ui │ │ │ └── ui.go │ │ └── client.go ├── notifier │ ├── state │ │ ├── state.go │ │ ├── data │ │ │ └── data.go │ │ ├── actions │ │ │ └── actions.go │ │ └── modifiers │ │ │ ├── modifiers.go │ │ │ └── modifiers_test.go │ ├── README.md │ └── notifier.go └── basic │ └── basic.go ├── LICENSE ├── benchmarks_test.go ├── boutique_test.go ├── boutique.go └── docs └── README.md /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "./docs" 3 | } -------------------------------------------------------------------------------- /example/chatterbox/etoe/etoe.go: -------------------------------------------------------------------------------- 1 | package etoe 2 | -------------------------------------------------------------------------------- /example/chatterbox/chatterbox.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | 7 | "github.com/golang/glog" 8 | "github.com/johnsiilver/boutique/example/chatterbox/server" 9 | ) 10 | 11 | var addr = flag.String("addr", ":6024", "websocket address") 12 | 13 | func main() { 14 | flag.Parse() 15 | cb := server.New() 16 | 17 | http.HandleFunc("/", cb.Handler) 18 | 19 | err := http.ListenAndServe(*addr, nil) 20 | if err != nil { 21 | glog.Fatal("ListenAndServe: ", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/chatterbox/messages/servermsgtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ServerMsgType messages.go"; DO NOT EDIT. 2 | 3 | package messages 4 | 5 | import "fmt" 6 | 7 | const _ServerMsgType_name = "SMUnknownSMError" 8 | 9 | var _ServerMsgType_index = [...]uint8{0, 9, 16} 10 | 11 | func (i ServerMsgType) String() string { 12 | if i < 0 || i >= ServerMsgType(len(_ServerMsgType_index)-1) { 13 | return fmt.Sprintf("ServerMsgType(%d)", i) 14 | } 15 | return _ServerMsgType_name[_ServerMsgType_index[i]:_ServerMsgType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /example/chatterbox/messages/clientmsgtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ClientMsgType messages.go"; DO NOT EDIT. 2 | 3 | package messages 4 | 5 | import "fmt" 6 | 7 | const _ClientMsgType_name = "CMUnknownCMSubscribeCMDropCMSendText" 8 | 9 | var _ClientMsgType_index = [...]uint8{0, 9, 20, 26, 36} 10 | 11 | func (i ClientMsgType) String() string { 12 | if i < 0 || i >= ClientMsgType(len(_ClientMsgType_index)-1) { 13 | return fmt.Sprintf("ClientMsgType(%d)", i) 14 | } 15 | return _ClientMsgType_name[_ClientMsgType_index[i]:_ClientMsgType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /example/notifier/state/state.go: -------------------------------------------------------------------------------- 1 | // Package state contains our Hub, which is used to store data for a particular 2 | // channel users are communicating on. 3 | package state 4 | 5 | import ( 6 | "github.com/johnsiilver/boutique" 7 | "github.com/johnsiilver/boutique/example/notifier/state/data" 8 | "github.com/johnsiilver/boutique/example/notifier/state/modifiers" 9 | ) 10 | 11 | // New is the constructor for a boutique.Store 12 | func New() (*boutique.Store, error) { 13 | d := data.State{ 14 | Tracking: map[string]data.Stock{}, 15 | } 16 | 17 | return boutique.New(d, modifiers.All, nil) 18 | } 19 | -------------------------------------------------------------------------------- /example/notifier/README.md: -------------------------------------------------------------------------------- 1 | # Notifier - A stock notifier program. 2 | 3 | ## One line summary 4 | 5 | Tracks stock buys and sells, notifying the user when those points are reached. 6 | 7 | ## The long summary 8 | 9 | This is a simple example application I made using Boutique as the central store 10 | and updating listeners when changes occurred. 11 | 12 | The application simply takes in some stock symbols, when you wish to buy 13 | and when you wish to sell, and notifies you through some desktop notification 14 | library of the event. 15 | 16 | It is not very sophisticated and doesn't contain a bunch of tests. 17 | -------------------------------------------------------------------------------- /example/notifier/state/data/data.go: -------------------------------------------------------------------------------- 1 | // Package data holds the Store object that is used by our boutique instances. 2 | package data 3 | 4 | // Stock represents a stock we are tracking. 5 | type Stock struct { 6 | // Symbol is the stock symbol: googl or msft. 7 | Symbol string 8 | // Current is the current price. 9 | Current float64 10 | // Buy is a point in which we want to buy this stock. 11 | Buy float64 12 | // Sell is a point when we want to sell this stock. 13 | Sell float64 14 | } 15 | 16 | // State holds the data stored in boutique.Store. 17 | type State struct { 18 | // Tracking is the current stocks we are tracking. 19 | Tracking map[string]Stock 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 John G. Doak Jr. 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 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/state.go: -------------------------------------------------------------------------------- 1 | // Package state contains our Hub, which is used to store data for a particular 2 | // channel users are communicating on. 3 | package state 4 | 5 | import ( 6 | "github.com/johnsiilver/boutique" 7 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 8 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/middleware" 9 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/modifiers" 10 | "github.com/pborman/uuid" 11 | ) 12 | 13 | // Hub contains the central store and our middleware. 14 | type Hub struct { 15 | // Store is our boutique.Store. 16 | Store *boutique.Store 17 | 18 | // Logging holds middleware that is used to log changes. 19 | Logging *middleware.Logging 20 | } 21 | 22 | // New is the constructor for Hub. 23 | func New(channelName string) (*Hub, error) { 24 | l := &middleware.Logging{} 25 | d := data.State{ 26 | ServerID: uuid.New(), 27 | Channel: channelName, 28 | Users: make([]string, 0, 1), 29 | Messages: make([]data.Message, 0, 10), 30 | } 31 | 32 | mw := []boutique.Middleware{middleware.CleanMessages, middleware.EnforceMsgLength, l.DebugLog, l.ChannelLog} 33 | 34 | s, err := boutique.New(d, modifiers.All, mw) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &Hub{Store: s, Logging: l}, nil 40 | } 41 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/actions/actions.go: -------------------------------------------------------------------------------- 1 | // Package actions details boutique.Actions that are used by modifiers to modify the store. 2 | package actions 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/johnsiilver/boutique" 8 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 9 | ) 10 | 11 | const ( 12 | // ActSendMessage indicates we want to send a message via the store. 13 | ActSendMessage boutique.ActionType = iota 14 | // ActDeleteMessages indicates we want to delete messages from the store. 15 | ActDeleteMessages 16 | // ActAddUser indicates the Action wants to add a user to the store. 17 | ActAddUser 18 | // ActRemoveUser indicates the Action wants to remove a user from the store. 19 | ActRemoveUser 20 | ) 21 | 22 | // SendMessage sends a message via the store. 23 | func SendMessage(user string, s string) boutique.Action { 24 | m := data.Message{Timestamp: time.Now(), User: user, Text: s} 25 | return boutique.Action{Type: ActSendMessage, Update: m} 26 | } 27 | 28 | // DeleteMessages deletes messages in our .Messages slice from the front until 29 | // we reach lastMsgID (inclusive). 30 | func DeleteMessages(lastMsgID int) boutique.Action { 31 | return boutique.Action{Type: ActDeleteMessages, Update: lastMsgID} 32 | } 33 | 34 | // AddUser adds a user to the store, indicating a new user is in the room. 35 | func AddUser(u string) boutique.Action { 36 | return boutique.Action{Type: ActAddUser, Update: u} 37 | } 38 | 39 | // RemoveUser removes a user from the store, indicating a user has left the room. 40 | func RemoveUser(u string) boutique.Action { 41 | return boutique.Action{Type: ActRemoveUser, Update: u} 42 | } 43 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/data/data.go: -------------------------------------------------------------------------------- 1 | // Package data holds the Store object that is used by our boutique instances. 2 | package data 3 | 4 | import ( 5 | "os" 6 | "time" 7 | ) 8 | 9 | // OpenFile contains access to a file and the last time we accessed it. 10 | type OpenFile struct { 11 | *os.File 12 | 13 | // LastAccess is the last time the file was accessed. 14 | LastAccess time.Time 15 | } 16 | 17 | // IsZero indicates that OpenFile has not been initialized. 18 | func (o OpenFile) IsZero() bool { 19 | if o.File == nil { 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | // Message represents a message sent. 26 | type Message struct { 27 | // ID is the ID of the message in the order it was sent. 28 | ID int 29 | // Timestamp is the time in which the message was written. 30 | Timestamp time.Time 31 | // User is the user who sent the message. 32 | User string 33 | // Text is the text of the message. 34 | Text string 35 | } 36 | 37 | // State holds our state data for each communication channel that is open. 38 | type State struct { 39 | // ServerID is a UUID that uniquely represents this server instance. 40 | ServerID string 41 | 42 | // Channel is the channel this represents. 43 | Channel string 44 | // Users are the users in the Channel. 45 | Users []string 46 | // Messages in the current messages. 47 | Messages []Message 48 | // NextMsgID is the ID of the next message to be sent. 49 | NextMsgID int 50 | 51 | // LogDebug indicates to start logging debug information. 52 | // LogChan indicates to log chat messages. 53 | LogDebug, LogChan bool 54 | // DebugFile holds access to the a debug file we opened for this channel. 55 | DebugFile OpenFile 56 | // ChanFile holds access to the chat log for the channel. 57 | ChanFile OpenFile 58 | } 59 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/modifiers/modifiers.go: -------------------------------------------------------------------------------- 1 | // Package modifiers holds all the boutique.Updaters and the boutique.Modifer for the state store. 2 | package modifiers 3 | 4 | import ( 5 | "sort" 6 | 7 | "github.com/johnsiilver/boutique" 8 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/actions" 9 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 10 | ) 11 | 12 | // All is a boutique.Modifiers made up of all Modifier(s) in this file. 13 | var All = boutique.NewModifiers(SendMessage, AddUser, RemoveUser) 14 | 15 | // SendMessage handles an Action of type ActSendMessage. 16 | func SendMessage(state interface{}, action boutique.Action) interface{} { 17 | s := state.(data.State) 18 | 19 | switch action.Type { 20 | case actions.ActSendMessage: 21 | msg := action.Update.(data.Message) 22 | msg.ID = s.NextMsgID 23 | s.Messages = boutique.CopyAppendSlice(s.Messages, msg).([]data.Message) 24 | s.NextMsgID = s.NextMsgID + 1 25 | } 26 | return s 27 | } 28 | 29 | // AddUser handles an Action of type ActAddUser. 30 | func AddUser(state interface{}, action boutique.Action) interface{} { 31 | s := state.(data.State) 32 | 33 | switch action.Type { 34 | case actions.ActAddUser: 35 | s.Users = boutique.CopyAppendSlice(s.Users, action.Update).([]string) 36 | sort.Strings(s.Users) 37 | } 38 | return s 39 | } 40 | 41 | // RemoveUser handles an Action of type ActRemoveUser. 42 | func RemoveUser(state interface{}, action boutique.Action) interface{} { 43 | s := state.(data.State) 44 | 45 | switch action.Type { 46 | case actions.ActRemoveUser: 47 | n := make([]string, 0, len(s.Users)-1) 48 | for _, u := range s.Users { 49 | if u != action.Update.(string) { 50 | n = append(n, u) 51 | } 52 | } 53 | s.Users = n 54 | } 55 | return s 56 | } 57 | -------------------------------------------------------------------------------- /example/notifier/state/actions/actions.go: -------------------------------------------------------------------------------- 1 | // Package actions details boutique.Actions that are used by modifiers to modify the store. 2 | package actions 3 | 4 | import ( 5 | "github.com/johnsiilver/boutique" 6 | "github.com/johnsiilver/boutique/example/notifier/state/data" 7 | ) 8 | 9 | const ( 10 | // ActTrack indicates we want to add a new stock to track. 11 | ActTrack boutique.ActionType = iota 12 | // ActUntrack indicates we want to remove a stock from tracking. 13 | ActUntrack 14 | // ActChangePoint indicates we want to change the buy/sell for a stock. 15 | ActChangePoint 16 | ) 17 | 18 | // Track tells the application to track a new stock. 19 | func Track(symbol string, buy, sell float64) boutique.Action { 20 | m := data.Stock{Symbol: symbol, Buy: buy, Sell: sell} 21 | return boutique.Action{Type: ActTrack, Update: m} 22 | } 23 | 24 | // Untrack tells the application to stop tracking a stock. 25 | func Untrack(symbol string) boutique.Action { 26 | return boutique.Action{Type: ActUntrack, Update: symbol} 27 | } 28 | 29 | // ChangeBuy indicates we wish to change the buy point for a stock. 30 | func ChangeBuy(symbol string, buy float64) boutique.Action { 31 | return boutique.Action{Type: ActChangePoint, Update: data.Stock{Symbol: symbol, Current: -1.0, Buy: buy, Sell: -1.0}} 32 | } 33 | 34 | // ChangeSell indicates we wish to change the sell point for a stock. 35 | func ChangeSell(symbol string, sell float64) boutique.Action { 36 | return boutique.Action{Type: ActChangePoint, Update: data.Stock{Symbol: symbol, Current: -1.0, Buy: -1.0, Sell: sell}} 37 | } 38 | 39 | // ChangeCurrent indicates we wish to change the current price of the stock. 40 | func ChangeCurrent(symbol string, current float64) boutique.Action { 41 | return boutique.Action{Type: ActChangePoint, Update: data.Stock{Symbol: symbol, Current: current, Buy: -1.0, Sell: -1}} 42 | } 43 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/modifiers/modifiers_test.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/johnsiilver/boutique" 8 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/actions" 9 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 10 | "github.com/kylelemons/godebug/pretty" 11 | "github.com/lukechampine/freeze" 12 | ) 13 | 14 | // supportedOS prevents tests from running on non-unix systems. 15 | // Windows cannot freeze memory. 16 | func supportedOS() bool { 17 | switch runtime.GOOS { 18 | case "linux", "darwin": 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | func TestSendMessage(t *testing.T) { 25 | if !supportedOS() { 26 | return 27 | } 28 | 29 | // This validates that we didn't mutate our map. 30 | msgs := []data.Message{data.Message{ID: -1, User: "dave"}} // Need to have an initial entry. 31 | msgs = freeze.Slice(msgs).([]data.Message) 32 | d := data.State{Messages: msgs} 33 | 34 | newState := SendMessage(d, boutique.Action{Type: actions.ActSendMessage, Update: data.Message{User: "john"}}) 35 | 36 | if diff := pretty.Compare([]data.Message{{ID: -1, User: "dave"}, {ID: 0, User: "john"}}, newState.(data.State).Messages); diff != "" { 37 | t.Errorf("TestSendMessage: -want/+got:\n%s", diff) 38 | } 39 | } 40 | 41 | func TestAddUser(t *testing.T) { 42 | if !supportedOS() { 43 | return 44 | } 45 | 46 | // This validates that we didn't mutate our map. 47 | users := []string{"dave"} 48 | users = freeze.Slice(users).([]string) 49 | d := data.State{Users: users} 50 | 51 | newState := AddUser(d, boutique.Action{Type: actions.ActAddUser, Update: "john"}) 52 | 53 | if diff := pretty.Compare([]string{"dave", "john"}, newState.(data.State).Users); diff != "" { 54 | t.Errorf("TestAddUser: -want/+got:\n%s", diff) 55 | } 56 | } 57 | 58 | func TestRemoveUser(t *testing.T) { 59 | if !supportedOS() { 60 | return 61 | } 62 | 63 | // This validates that we didn't mutate our map. 64 | users := []string{"dave", "john"} 65 | users = freeze.Slice(users).([]string) 66 | d := data.State{Users: users} 67 | 68 | newState := RemoveUser(d, boutique.Action{Type: actions.ActRemoveUser, Update: "john"}) 69 | 70 | if diff := pretty.Compare([]string{"dave"}, newState.(data.State).Users); diff != "" { 71 | t.Errorf("TestAddUser: -want/+got:\n%s", diff) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/notifier/state/modifiers/modifiers.go: -------------------------------------------------------------------------------- 1 | // Package modifiers holds all the boutique.Updaters and the boutique.Modifer for the state store. 2 | package modifiers 3 | 4 | import ( 5 | "github.com/johnsiilver/boutique" 6 | "github.com/johnsiilver/boutique/example/notifier/state/actions" 7 | "github.com/johnsiilver/boutique/example/notifier/state/data" 8 | ) 9 | 10 | // All is a boutique.Modifiers made up of all Modifier(s) in this file. 11 | var All = boutique.NewModifiers(Tracking, ChangePoint) 12 | 13 | // Tracking handles an Action of type action.ActTrack or action.ActUntrack. 14 | func Tracking(state interface{}, action boutique.Action) interface{} { 15 | s := state.(data.State) 16 | 17 | switch action.Type { 18 | case actions.ActTrack: 19 | msg := action.Update.(data.Stock) 20 | 21 | if _, ok := s.Tracking[msg.Symbol]; ok { 22 | break 23 | } 24 | 25 | to := map[string]data.Stock{} 26 | if err := boutique.DeepCopy(s.Tracking, &to); err != nil { 27 | panic(err) 28 | } 29 | 30 | to[msg.Symbol] = msg 31 | 32 | s.Tracking = to 33 | case actions.ActUntrack: 34 | symbol := action.Update.(string) 35 | 36 | if _, ok := s.Tracking[symbol]; !ok { 37 | break 38 | } 39 | 40 | to := map[string]data.Stock{} 41 | if err := boutique.DeepCopy(s.Tracking, &to); err != nil { 42 | panic(err) 43 | } 44 | 45 | delete(to, symbol) 46 | s.Tracking = to 47 | } 48 | return s 49 | } 50 | 51 | // ChangePoint handles an Action of type action.ActChangePoint. 52 | func ChangePoint(state interface{}, action boutique.Action) interface{} { 53 | s := state.(data.State) 54 | 55 | switch action.Type { 56 | case actions.ActChangePoint: 57 | u := action.Update.(data.Stock) 58 | var ( 59 | v data.Stock 60 | ok bool 61 | ) 62 | if v, ok = s.Tracking[u.Symbol]; !ok { 63 | break 64 | } 65 | 66 | switch { 67 | case u.Current > 0: 68 | to := map[string]data.Stock{} 69 | if err := boutique.DeepCopy(s.Tracking, &to); err != nil { 70 | panic(err) 71 | } 72 | v.Current = u.Current 73 | to[u.Symbol] = v 74 | s.Tracking = to 75 | case u.Buy > 0: 76 | to := map[string]data.Stock{} 77 | if err := boutique.DeepCopy(s.Tracking, &to); err != nil { 78 | panic(err) 79 | } 80 | v.Buy = u.Buy 81 | to[u.Symbol] = v 82 | s.Tracking = to 83 | case u.Sell > 0: 84 | to := map[string]data.Stock{} 85 | if err := boutique.DeepCopy(s.Tracking, &to); err != nil { 86 | panic(err) 87 | } 88 | v.Sell = u.Sell 89 | to[u.Symbol] = v 90 | s.Tracking = to 91 | } 92 | } 93 | return s 94 | } 95 | -------------------------------------------------------------------------------- /example/chatterbox/etoe/etoe_test.go: -------------------------------------------------------------------------------- 1 | package etoe 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/golang/glog" 11 | "github.com/johnsiilver/boutique/example/chatterbox/client" 12 | "github.com/johnsiilver/boutique/example/chatterbox/server" 13 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/middleware" 14 | ) 15 | 16 | func TestClientServer(t *testing.T) { 17 | // Set our timer to happen faster than normal. 18 | middleware.CleanTimer = 5 * time.Second 19 | 20 | cb := server.New() 21 | 22 | http.HandleFunc("/", cb.Handler) 23 | 24 | go func() { 25 | err := http.ListenAndServe("localhost:6024", nil) 26 | if err != nil { 27 | glog.Fatal("ListenAndServe: ", err) 28 | } 29 | }() 30 | 31 | cli1, err := startClient("john") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | cli2, err := startClient("beck") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | waitingFor := []string{} 42 | for i := 0; i < 1000; i++ { 43 | i := i 44 | var cli *client.ChatterBox 45 | if i%2 == 0 { 46 | cli = cli1 47 | } else { 48 | cli = cli2 49 | } 50 | 51 | text := fmt.Sprintf("%d", i) 52 | waitingFor = append(waitingFor, text) 53 | go func() { 54 | glog.Infof("sending %s", text) 55 | if err := cli.SendText(text); err != nil { 56 | t.Fatal(err) 57 | } 58 | }() 59 | } 60 | 61 | waitFor(cli1, waitingFor) 62 | waitFor(cli2, waitingFor) 63 | } 64 | 65 | func startClient(user string) (*client.ChatterBox, error) { 66 | var ( 67 | cli *client.ChatterBox 68 | err error 69 | ) 70 | 71 | start := time.Now() 72 | for { 73 | if time.Now().Sub(start) > 10*time.Second { 74 | return nil, fmt.Errorf("could not reach server in < 10 seconds") 75 | } 76 | 77 | cli, err = client.New("localhost:6024") 78 | if err != nil { 79 | glog.Error(err) 80 | time.Sleep(1 * time.Second) 81 | continue 82 | } 83 | break 84 | } 85 | if _, err = cli.Subscribe("empty", user); err != nil { 86 | return nil, err 87 | } 88 | return cli, nil 89 | } 90 | 91 | func waitFor(cli *client.ChatterBox, text []string) error { 92 | waiting := make(map[string]bool, len(text)) 93 | for _, t := range text { 94 | waiting[t] = true 95 | } 96 | 97 | for { 98 | if len(waiting) == 0 { 99 | return nil 100 | } 101 | select { 102 | case m := <-cli.Messages: 103 | delete(waiting, strings.TrimSpace(m.Text.Text)) 104 | continue 105 | case <-time.After(2 * time.Second): 106 | return fmt.Errorf("timeout waiting for: %+v", waiting) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /example/notifier/state/modifiers/modifiers_test.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "runtime" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/johnsiilver/boutique" 9 | "github.com/johnsiilver/boutique/example/notifier/state/actions" 10 | "github.com/johnsiilver/boutique/example/notifier/state/data" 11 | "github.com/kylelemons/godebug/pretty" 12 | "github.com/lukechampine/freeze" 13 | ) 14 | 15 | // supportedOS prevents tests from running on non-unix systems. 16 | // Windows cannot freeze memory. 17 | func supportedOS() bool { 18 | switch runtime.GOOS { 19 | case "linux", "darwin": 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | func TestTracking(t *testing.T) { 26 | if !supportedOS() { 27 | return 28 | } 29 | 30 | tests := []struct { 31 | desc string 32 | action boutique.Action 33 | keys []string 34 | }{ 35 | { 36 | desc: "Add stock", 37 | action: boutique.Action{Type: actions.ActTrack, Update: data.Stock{Symbol: "googl"}}, 38 | keys: []string{"aapl", "googl"}, 39 | }, 40 | { 41 | desc: "Remove stock", 42 | action: boutique.Action{Type: actions.ActUntrack, Update: "googl"}, 43 | keys: []string{"aapl"}, 44 | }, 45 | } 46 | 47 | for _, test := range tests { 48 | // This validates that we didn't mutate our map. 49 | tr := map[string]data.Stock{"aapl": data.Stock{}} // bug(https://github.com/lukechampine/freeze/issues/4) 50 | tr = freeze.Map(tr).(map[string]data.Stock) 51 | d := data.State{Tracking: tr} 52 | 53 | newState := Tracking(d, test.action) 54 | 55 | keys := []string{} 56 | for k := range newState.(data.State).Tracking { 57 | keys = append(keys, k) 58 | } 59 | sort.Strings(keys) 60 | if diff := pretty.Compare(test.keys, keys); diff != "" { 61 | t.Errorf("TestTracking(%s): -want/+got:\n%s", test.desc, diff) 62 | } 63 | } 64 | } 65 | 66 | func TestChangePoint(t *testing.T) { 67 | if !supportedOS() { 68 | return 69 | } 70 | 71 | const ( 72 | apple = "aapl" 73 | val = 3.40 74 | ) 75 | 76 | tests := []struct { 77 | desc string 78 | update data.Stock 79 | want data.Stock 80 | }{ 81 | { 82 | desc: "Change buy", 83 | update: data.Stock{Symbol: apple, Buy: val}, 84 | want: data.Stock{Symbol: apple, Buy: val}, 85 | }, 86 | { 87 | desc: "Change sell", 88 | update: data.Stock{Symbol: apple, Sell: val}, 89 | want: data.Stock{Symbol: apple, Sell: val}, 90 | }, 91 | { 92 | desc: "Change current", 93 | update: data.Stock{Symbol: apple, Current: val}, 94 | want: data.Stock{Symbol: apple, Current: val}, 95 | }, 96 | } 97 | 98 | for _, test := range tests { 99 | // This validates that we didn't mutate our map. 100 | tr := map[string]data.Stock{apple: data.Stock{Symbol: apple}} // bug(https://github.com/lukechampine/freeze/issues/4) 101 | tr = freeze.Map(tr).(map[string]data.Stock) 102 | d := data.State{Tracking: tr} 103 | 104 | newState := ChangePoint(d, boutique.Action{Type: actions.ActChangePoint, Update: test.update}) 105 | 106 | if diff := pretty.Compare(test.want, newState.(data.State).Tracking[apple]); diff != "" { 107 | t.Errorf("TestChangePoint(%s): -want/+got:\n%s", test.desc, diff) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /example/chatterbox/client/chatterbox/cli/chatterbox.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | 11 | "github.com/johnsiilver/boutique/example/chatterbox/client" 12 | "github.com/johnsiilver/boutique/example/chatterbox/client/chatterbox/cli/ui" 13 | ) 14 | 15 | var ( 16 | comm atomic.Value // string 17 | errors []error 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | comm.Store("") 24 | 25 | defer func() { 26 | for _, err := range errors { 27 | fmt.Println(err) 28 | } 29 | if len(errors) > 0 { 30 | os.Exit(1) 31 | } 32 | }() 33 | 34 | if len(os.Args) != 2 { 35 | fmt.Println("usage error: chatterbox ") 36 | fmt.Println("got: ", strings.Join(os.Args, " ")) 37 | return 38 | } 39 | 40 | server := os.Args[1] 41 | 42 | // Connection to server setup. 43 | c, err := client.New(server) 44 | if err != nil { 45 | errors = append(errors, fmt.Errorf("error connecting to server: %s", err)) 46 | return 47 | } 48 | 49 | wg := &sync.WaitGroup{} 50 | stop := make(chan struct{}) 51 | 52 | // Create our UI. 53 | term, display, input := ui.New(stop) 54 | if err := term.Start(); err != nil { 55 | errors = append(errors, fmt.Errorf("Error: problem initing the terminal UI: %s", err)) 56 | return 57 | } 58 | defer term.Stop() 59 | 60 | wg.Add(1) 61 | go inputRouter(c, display, input, stop) 62 | go fromServer(c, wg, display, stop) 63 | 64 | // We will wait until the ui.Input() gets a /quit. 65 | wg.Wait() 66 | } 67 | 68 | func subscribe(c *client.ChatterBox, channel string, user string, displays ui.Displays) error { 69 | if comm.Load().(string) != "" { 70 | if err := c.Drop(); err != nil { 71 | return fmt.Errorf("could not drop from existing channel %s", comm.Load().(string)) 72 | } 73 | } 74 | // Must subscribe before doing anything else. 75 | users, err := c.Subscribe(channel, user) 76 | if err != nil { 77 | return fmt.Errorf("could not subscribe to channel %s: %s", channel, err) 78 | } 79 | comm.Store(channel) 80 | comm.Store(user) 81 | 82 | displays.Users <- users 83 | return nil 84 | } 85 | 86 | func inputRouter(c *client.ChatterBox, displays ui.Displays, inputs ui.Inputs, stop chan struct{}) { 87 | defer close(stop) 88 | for { 89 | select { 90 | case msg := <-inputs.Msgs: 91 | if comm.Load().(string) == "" { 92 | displays.Msgs <- fmt.Sprintf("not subscribed to channel") 93 | } else if err := c.SendText(msg); err != nil { 94 | // TODO(johnsiilver): This isn't going to work, because the UI is going to die. 95 | // Add a channel for after UI messages. Or maybe don't kill the UI? 96 | displays.Msgs <- fmt.Sprintf("Error: %s, lost connection to server. Please quit", err) 97 | return 98 | } 99 | case cmd := <-inputs.Cmds: 100 | switch cmd.Name { 101 | case "quit": 102 | displays.Msgs <- fmt.Sprintf("quiting...") 103 | return 104 | case "subscribe": 105 | displays.Msgs <- fmt.Sprintf("setting comm to %s, user to %s", cmd.Args[0], cmd.Args[1]) // debug, remove 106 | if err := subscribe(c, cmd.Args[0], cmd.Args[1], displays); err != nil { 107 | displays.Msgs <- fmt.Sprintf("Subscribe error: %s", err) 108 | return 109 | } 110 | cmd.Resp <- fmt.Sprintf("Subscribed to channel: %s as user %s", cmd.Args[0], cmd.Args[1]) 111 | } 112 | } 113 | } 114 | } 115 | 116 | func fromServer(c *client.ChatterBox, wg *sync.WaitGroup, displays ui.Displays, stop <-chan struct{}) { 117 | defer wg.Done() 118 | 119 | for { 120 | select { 121 | case <-stop: 122 | return 123 | case m := <-c.Messages: 124 | displays.Msgs <- fmt.Sprintf("%s: %s", m.User, strings.TrimSpace(m.Text.Text)) 125 | case e := <-c.ServerErrors: 126 | displays.Msgs <- fmt.Sprintf("Server Error: %s", e) 127 | case u := <-c.UserUpdates: 128 | displays.Users <- u 129 | case <-c.Done: 130 | errors = append(errors, fmt.Errorf("connection broken with server")) 131 | return 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /example/chatterbox/server/state/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // Package middleware provides middleware to our boutique.Container. 2 | package middleware 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "time" 8 | 9 | "github.com/golang/glog" 10 | "github.com/johnsiilver/boutique" 11 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/actions" 12 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 13 | "github.com/kylelemons/godebug/pretty" 14 | ) 15 | 16 | var pConfig = &pretty.Config{ 17 | Diffable: true, 18 | 19 | // Field and value options 20 | IncludeUnexported: false, 21 | PrintStringers: true, 22 | PrintTextMarshalers: true, 23 | 24 | Formatter: map[reflect.Type]interface{}{ 25 | reflect.TypeOf(data.OpenFile{}): nil, 26 | reflect.TypeOf((*Logging)(nil)).Elem(): nil, 27 | }, 28 | } 29 | 30 | // Logging provides middleware for logging channels and state data debugging. 31 | type Logging struct { 32 | lastData boutique.State 33 | } 34 | 35 | // DebugLog implements boutique.Middleware. 36 | func (l *Logging) DebugLog(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 37 | go func() { 38 | defer args.WG.Done() 39 | state := <-args.Committed 40 | if state.IsZero() { // This indicates that another middleware killed the commit. No need to log. 41 | return 42 | } 43 | d := state.Data.(data.State) 44 | if !d.LogDebug { 45 | return 46 | } 47 | if d.DebugFile.IsZero() { 48 | glog.Errorf("asked to do a debug log, but data.State.DebufFile is zero vlaue") 49 | return 50 | } 51 | _, err := d.DebugFile.WriteString(fmt.Sprintf("%s\n\n", pConfig.Compare(l.lastData, state))) 52 | if err != nil { 53 | glog.Errorf("problem writing to debug file: %s", err) 54 | } 55 | l.lastData = state 56 | }() 57 | return nil, false, nil 58 | } 59 | 60 | // ChannelLog implements boutique.Middleware. 61 | func (l *Logging) ChannelLog(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 62 | if args.Action.Type != actions.ActSendMessage { 63 | args.WG.Done() 64 | return nil, false, nil 65 | } 66 | 67 | d := args.GetState().Data.(data.State) 68 | if !d.LogChan { 69 | args.WG.Done() 70 | return nil, false, nil 71 | } 72 | 73 | go func() { 74 | defer args.WG.Done() // Signal when we are done. Not doing this will cause the program to stall. 75 | state := <-args.Committed 76 | if state.IsZero() { // This indicates that another middleware killed the commit. No need to log. 77 | return 78 | } 79 | 80 | d := state.Data.(data.State) 81 | if d.ChanFile.IsZero() { 82 | glog.Errorf("log channel messages, but data.State.ChanFile is zero vlaue") 83 | return 84 | } 85 | 86 | lmsg := d.Messages[len(d.Messages)-1] 87 | _, err := d.ChanFile.WriteString(fmt.Sprintf("%d:::%s:::%s:::%s", lmsg.ID, lmsg.Timestamp, lmsg.User, lmsg.Text)) 88 | if err != nil { 89 | glog.Errorf("problem writing to channel file: %s", err) 90 | } 91 | 92 | }() 93 | return nil, false, nil 94 | } 95 | 96 | // CleanTimer is how old a message must be before it is deleted on the next Perform(). 97 | var CleanTimer = 1 * time.Minute 98 | 99 | // CleanMessages deletes data.State.Messages older than 1 Minute. 100 | func CleanMessages(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 101 | defer args.WG.Done() 102 | 103 | d := args.NewData.(data.State) 104 | 105 | var ( 106 | i int 107 | m data.Message 108 | deleteAll = true 109 | now = time.Now() 110 | ) 111 | for i, m = range d.Messages { 112 | if now.Sub(m.Timestamp) < CleanTimer { 113 | deleteAll = false 114 | break 115 | } 116 | } 117 | 118 | switch { 119 | case i == 0: 120 | return nil, false, nil 121 | case deleteAll: 122 | d.Messages = []data.Message{} 123 | case len(d.Messages[i:]) > 0: 124 | newMsg := make([]data.Message, len(d.Messages[i:])) 125 | copy(newMsg, d.Messages[i:]) 126 | d.Messages = newMsg 127 | } 128 | return d, false, nil 129 | } 130 | 131 | // EnforceMsgLength tests that an actions.ActSendMessage does not contain a 132 | // message longer than 500 characters. 133 | func EnforceMsgLength(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 134 | defer args.WG.Done() 135 | 136 | if args.Action.Type == actions.ActSendMessage { 137 | m := args.Action.Update.(data.Message) 138 | if len(m.Text) > 500 { 139 | return nil, false, fmt.Errorf("cannot send a Message > 500 characters") 140 | } 141 | } 142 | return nil, false, nil 143 | } 144 | -------------------------------------------------------------------------------- /example/chatterbox/messages/messages.go: -------------------------------------------------------------------------------- 1 | // Package messages holds the client/erver messages that are sent on the write in JSON format. 2 | package messages 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // ClientMsgType is the type of message being sent from a client. 10 | type ClientMsgType int 11 | 12 | const ( 13 | // CMUnknown indicates that the message type is unknown. This means the code did not set 14 | // the MessgeType. 15 | CMUnknown ClientMsgType = 0 16 | // CMSubscribe indicates that the user is trying to subscribe to a channel. This should be the 17 | // first message we see in a connection. 18 | CMSubscribe ClientMsgType = 1 19 | // CMDrop indicates they wish to stop communicating on a channel and we should remove them. 20 | CMDrop ClientMsgType = 2 21 | // CMSendText indicates they are sending a text to everyone. 22 | CMSendText ClientMsgType = 3 23 | ) 24 | 25 | // Client represents a message from the client. 26 | type Client struct { 27 | // Type is the type of message. 28 | Type ClientMsgType 29 | 30 | // User is the user's name. 31 | User string 32 | 33 | // Channel is the channel they wish to connect to. If it doesn't exist it will be created. 34 | Channel string 35 | 36 | // Text is textual data that will be used if the Type == CMSendText. 37 | Text Text 38 | } 39 | 40 | // Validate validates that the messsage is valid. 41 | func (m Client) Validate() error { 42 | if m.Type == CMUnknown { 43 | return fmt.Errorf("client did not set the message type") 44 | } 45 | 46 | if m.User == "" { 47 | return fmt.Errorf("client did not set user") 48 | } 49 | 50 | if m.Channel == "" { 51 | return fmt.Errorf("client did not send channel") 52 | } 53 | 54 | switch m.Type { 55 | case CMSendText: 56 | if err := m.Text.Validate(); err != nil { 57 | return err 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | // Marshal turns our message into JSON. 64 | func (m Client) Marshal() []byte { 65 | b, err := json.Marshal(m) 66 | if err != nil { 67 | panic(err) // This should never happen. 68 | } 69 | return b 70 | } 71 | 72 | // Unmarshal takes a binary version of a message and turns it into a struct. 73 | func (m *Client) Unmarshal(b []byte) error { 74 | return json.Unmarshal(b, m) 75 | } 76 | 77 | // Text contains textual information that is being sent. 78 | type Text struct { 79 | // Text is the text the user is sending. 80 | Text string 81 | } 82 | 83 | // Validate validates the TextMessage. 84 | func (t Text) Validate() error { 85 | if len(t.Text) > 500 { 86 | return fmt.Errorf("cannot send more than 500 characters in a single TextMessage") 87 | } 88 | return nil 89 | } 90 | 91 | // ServerMsgType indicates the type of message being sent from the server. 92 | type ServerMsgType int 93 | 94 | const ( 95 | // SMUnknown indicates that the message type is unknown. 96 | SMUnknown ServerMsgType = 0 97 | // SMError indicates that the server had some type of error. 98 | SMError ServerMsgType = 1 99 | // SMSendText is text intended for the client. 100 | SMSendText = 2 101 | // SMSubAck indicates that we successfully subscribed a user to the channel. 102 | SMSubAck = 3 103 | // SMUserUpdate updates a client with the users in the room. 104 | SMUserUpdate = 4 105 | // SMChannelDrop indicates that the we have received the new subscription 106 | // request and dropped the person out of the existing channel. 107 | SMChannelDrop = 5 108 | ) 109 | 110 | // Server is a message that is sent from the server. 111 | type Server struct { 112 | // Type is the type of message we are sending. 113 | Type ServerMsgType 114 | 115 | // User is the user who sent some type of message. 116 | User string 117 | 118 | // Text is the text of the message if Type == SMSendText. 119 | Text Text 120 | 121 | // Users is a list of of users in a comm channel if Type == SMUserUpdate or 122 | // SMSubAck. 123 | Users []string 124 | } 125 | 126 | // Validate validates that the messsage is valid. 127 | func (m Server) Validate() error { 128 | switch m.Type { 129 | case SMUnknown: 130 | return fmt.Errorf("client did not set the message type") 131 | case SMSendText, SMError: 132 | if err := m.Text.Validate(); err != nil { 133 | return err 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | // Marshal turns our message into JSON. 140 | func (m Server) Marshal() []byte { 141 | b, err := json.Marshal(m) 142 | if err != nil { 143 | panic(err) // This should never happen. 144 | } 145 | return b 146 | } 147 | 148 | // Unmarshal takes a binary version of a message and turns it into a struct. 149 | func (m *Server) Unmarshal(b []byte) error { 150 | return json.Unmarshal(b, m) 151 | } 152 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package boutique 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "testing" 7 | 8 | "net/http" 9 | _ "net/http/pprof" 10 | ) 11 | 12 | /* 13 | BenchmarkConcurrentPerform-8 3000 542119 ns/op 14 | BenchmarkMultifieldPerform-8 500 3387714 ns/op 15 | BenchmarkStaticallyWithMutation-8 5000 295028 ns/op 16 | BenchmarkStaticallyWithoutMutation-8 5000 300650 ns/op 17 | BenchmarkStaticallyMultifieldWithoutMutation-8 1000 1719393 ns/op 18 | 19 | Single field changing with a big lock: 20 | 21 | Perform is 1.8 times slower than a version using mutation 22 | Perform is 1.8 times slower than a version doing copies 23 | 24 | If we move up to changing multiple fields simultaneously with a big lock, this becomes: 25 | 26 | Perform is 1.9 times slower than a version doing copies 27 | 28 | These benchmarks don't take into account the biggest cost in using this module, 29 | which is calculating the changes for subscribers. 30 | */ 31 | 32 | func init() { 33 | go func() { 34 | log.Println(http.ListenAndServe("localhost:6060", nil)) 35 | }() 36 | } 37 | 38 | const iterations = 500 39 | 40 | func BenchmarkConcurrentPerform(b *testing.B) { 41 | initial := MyState{Counter: 0} 42 | 43 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 44 | if err != nil { 45 | b.Fatalf("BenchmarkConcurrentPerform: %s", err) 46 | } 47 | wg := &sync.WaitGroup{} 48 | b.ResetTimer() 49 | 50 | for i := 0; i < b.N; i++ { 51 | for i := 0; i < iterations; i++ { 52 | wg.Add(1) 53 | go s.Perform(IncrCounter(), WaitForCommit(wg)) 54 | } 55 | wg.Wait() 56 | } 57 | } 58 | 59 | func BenchmarkMultifieldPerform(b *testing.B) { 60 | initial := MyState{Counter: 0, List: []string{}} 61 | 62 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 63 | if err != nil { 64 | b.Fatalf("BenchmarkMultifieldConcurrentPerform: %s", err) 65 | } 66 | wg := &sync.WaitGroup{} 67 | b.ResetTimer() 68 | 69 | for i := 0; i < b.N; i++ { 70 | for i := 0; i < iterations; i++ { 71 | wg.Add(1) 72 | //go s.Perform(IncrCounter(), wg) 73 | go s.Perform(AppendList("a"), WaitForCommit(wg)) 74 | } 75 | wg.Wait() 76 | 77 | // We have to reset the store, otherwise the List will just keep growning on 78 | // each test and the benchamrk will not stop. 79 | b.StopTimer() 80 | s.state.Store(MyState{Counter: 0, List: []string{}}) 81 | b.StartTimer() 82 | } 83 | } 84 | 85 | func BenchmarkStaticallyWithMutation(b *testing.B) { 86 | initial := &MyState{Counter: 0} 87 | mu := &sync.Mutex{} 88 | wg := &sync.WaitGroup{} 89 | 90 | for i := 0; i < b.N; i++ { 91 | for i := 0; i < iterations; i++ { 92 | wg.Add(1) 93 | go func() { 94 | mu.Lock() 95 | defer mu.Unlock() 96 | defer wg.Done() 97 | initial.Counter++ 98 | }() 99 | } 100 | wg.Wait() 101 | } 102 | } 103 | 104 | func BenchmarkStaticallyWithoutMutation(b *testing.B) { 105 | // NOTE: If you make changes here, make sure to use freeze.Object() on MyState 106 | // before submitting. Remove freeze afterwards. 107 | 108 | initial := MyState{Counter: 0} 109 | 110 | mu := &sync.Mutex{} 111 | wg := &sync.WaitGroup{} 112 | 113 | sc := func(ms MyState) MyState { 114 | return ms 115 | } 116 | 117 | for i := 0; i < b.N; i++ { 118 | for i := 0; i < iterations; i++ { 119 | wg.Add(1) 120 | go func() { 121 | mu.Lock() 122 | defer mu.Unlock() 123 | defer wg.Done() 124 | ms := sc(initial) 125 | ms.Counter++ 126 | initial = ms 127 | }() 128 | } 129 | wg.Wait() 130 | } 131 | } 132 | 133 | func BenchmarkStaticallyMultifieldWithoutMutation(b *testing.B) { 134 | initial := MyState{Counter: 0, List: []string{}} 135 | 136 | mu := &sync.Mutex{} 137 | wg := &sync.WaitGroup{} 138 | 139 | sc := func(ms MyState) MyState { 140 | return ms 141 | } 142 | 143 | copyAppendSlice := func(s []string, item string) []string { 144 | cp := make([]string, len(s)+1) 145 | copy(cp, s) 146 | cp[len(cp)-1] = item 147 | return cp 148 | } 149 | 150 | for i := 0; i < b.N; i++ { 151 | for i := 0; i < iterations; i++ { 152 | wg.Add(2) 153 | go func() { 154 | mu.Lock() 155 | defer mu.Unlock() 156 | defer wg.Done() 157 | ms := sc(initial) 158 | ms.Counter++ 159 | initial = ms 160 | }() 161 | go func() { 162 | mu.Lock() 163 | defer mu.Unlock() 164 | defer wg.Done() 165 | ms := sc(initial) 166 | ms.List = copyAppendSlice(ms.List, "a") 167 | initial = ms 168 | }() 169 | } 170 | wg.Wait() 171 | 172 | // We have to reset the store, otherwise the List will just keep growning on 173 | // each test and the benchamrk will not stop. 174 | b.StopTimer() 175 | initial = MyState{Counter: 0, List: []string{}} 176 | b.StartTimer() 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /example/basic/basic.go: -------------------------------------------------------------------------------- 1 | /* 2 | This program is a simplistic example of using Boutique as a state store. 3 | This is not a practical example, it just shows how Boutique works. 4 | 5 | This example spins up 1000 goroutines that sleep for 1-5 seconds. 6 | Our state store simply needs to track how many goroutines are running at a given 7 | time and print that number out as the store is updated. 8 | 9 | Boutique allows us to subscribe to state changes and we only receive the 10 | latest update, not all updates. This again is not a practical example, because 11 | we only have 1 subscriber to our changes. This would be easier to accomplish 12 | normally with atomic.Value for this counter. 13 | */ 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "math/rand" 19 | "sync" 20 | "time" 21 | 22 | "github.com/johnsiilver/boutique" 23 | ) 24 | 25 | // State is our state data for Boutique. This is what data we want to store. 26 | type State struct { 27 | // Goroutines is how many goroutines we are running. 28 | Goroutines int 29 | } 30 | 31 | // These are our ActionTypes. This inform us of what kind of change we want 32 | // to do with an Action. 33 | const ( 34 | // ActIncr indicates we are incrementing the Goroutines field. 35 | ActIncr boutique.ActionType = iota 36 | 37 | // ActDecr indicates we are decrementing the Gorroutines field. 38 | ActDecr 39 | ) 40 | 41 | /////////////////////////////////////////////////////////////////////////////// 42 | // These are our Action creators. They create an Action to update the Store 43 | // and are always used with a boutique.Store.Perform() call. 44 | /////////////////////////////////////////////////////////////////////////////// 45 | 46 | // IncrGoroutines creates an ActIncr boutique.Action. 47 | func IncrGoroutines(n int) boutique.Action { 48 | return boutique.Action{Type: ActIncr, Update: n} 49 | } 50 | 51 | // DecrGoroutines creates and ActDecr boutique.Action. 52 | func DecrGoroutines() boutique.Action { 53 | return boutique.Action{Type: ActDecr} 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | 58 | /////////////////////////////////////////////////////////////////////////////// 59 | // This is our single Modifier. It looks at an Action and determines if it 60 | // was meant to handle it. If not, it just returns the State that was passed in. 61 | // Otherwise it makes the modification to the State. You must be careful not to 62 | // mutate data here. In this example, .Goroutines is not a pointer or reference 63 | // type, so we do not have to worry about this. 64 | /////////////////////////////////////////////////////////////////////////////// 65 | 66 | // HandleIncrDecr is a boutique.Modifier for handling ActIncr and ActDecr boutique.Actions. 67 | func HandleIncrDecr(state interface{}, action boutique.Action) interface{} { 68 | s := state.(State) 69 | 70 | switch action.Type { 71 | case ActIncr: 72 | s.Goroutines = s.Goroutines + action.Update.(int) 73 | case ActDecr: 74 | s.Goroutines = s.Goroutines - 1 75 | } 76 | 77 | return s 78 | } 79 | 80 | /////////////////////////////////////////////////////////////////////////////// 81 | 82 | // printer prints out changes to State.Goroutines, but with a maximum of 83 | // 1 second intervals. 84 | func printer(killMe, done chan struct{}, store *boutique.Store) { 85 | defer close(done) 86 | defer store.Perform(DecrGoroutines()) 87 | 88 | // Subscribe to the .Goroutines field changes. 89 | ch, cancel, err := store.Subscribe("Goroutines") 90 | if err != nil { 91 | panic(err) 92 | } 93 | defer cancel() // Cancel our subscription when this goroutine ends. 94 | 95 | for { 96 | select { 97 | case sig := <-ch: // This is the latest change to the .Goroutines field. 98 | fmt.Println(sig.State.Data.(State).Goroutines) 99 | // Put a 1 second pause in. Remember, we won't receive 1000 increment 100 | // signals and 1000 decrement signals. We will always receive the 101 | // latest data, which may be far less than 2000. 102 | time.Sleep(1 * time.Second) 103 | case <-killMe: // We were told to die. 104 | return 105 | } 106 | } 107 | } 108 | 109 | func main() { 110 | // Create our new Store with our default State{} object and our only 111 | // Modifier. We are not going to define Middleware, so we pass nil. 112 | store, err := boutique.New(State{}, boutique.NewModifiers(HandleIncrDecr), nil) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | // killPrinter lets us signal our printer goroutine that we no longer need 118 | // its services. 119 | killPrinter := make(chan struct{}) 120 | // printerKilled informs us that the printer goroutine has exited. 121 | printerKilled := make(chan struct{}) 122 | 123 | // Write out our goroutine count as it changes. Include this goroutine 124 | // in the count. 125 | store.Perform(IncrGoroutines(1)) 126 | go printer(killPrinter, printerKilled, store) 127 | 128 | wg := sync.WaitGroup{} 129 | 130 | // Spin up a 1000 goroutines that sleep between 0 and 5 seconds. 131 | // Pause after generating every 100 for 0 - 8 seconds. 132 | for i := 0; i < 1000; i++ { 133 | if i%100 == 0 { 134 | time.Sleep(time.Duration(rand.Intn(8)) * time.Second) 135 | } 136 | store.Perform(IncrGoroutines(1)) 137 | wg.Add(1) 138 | go func() { 139 | defer wg.Done() 140 | defer store.Perform(DecrGoroutines()) 141 | time.Sleep(time.Duration(rand.Intn(5)) * time.Second) 142 | }() 143 | } 144 | 145 | wg.Wait() // Wait for the goroutines to finish. 146 | close(killPrinter) // kill the printer. 147 | <-printerKilled // wait for the printer to die. 148 | 149 | fmt.Printf("Final goroutine count: %d\n", store.State().Data.(State).Goroutines) 150 | fmt.Printf("Final Boutique.Store version: %d\n", store.Version()) 151 | } 152 | -------------------------------------------------------------------------------- /example/chatterbox/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package client provides a ChatterBox client for communicating with a ChatterBox server. 3 | 4 | Usage is simple: 5 | user := "some user" 6 | channel := "some channel" 7 | c, err := New("", user) 8 | if err != nil { 9 | // Do something 10 | } 11 | 12 | // Must subscribe before doing anything else. 13 | if err := c.Subscribe(""); err != nil { 14 | // Do something 15 | } 16 | 17 | stop := make(chan struct{}) 18 | // Receive messages. 19 | go func() { 20 | for { 21 | select { 22 | case <-stop: 23 | fmt.Println("Exiting channel") 24 | return 25 | case m := <-c.Messages: 26 | // Ignore messages from yourself, we don't have separate panes to display in. 27 | if m.User == user { 28 | continue 29 | } 30 | fmt.Println("%s: %s", m.User, m.Text.Text) 31 | } 32 | } 33 | }() 34 | 35 | for { 36 | reader := bufio.NewReader(os.Stdin) 37 | fmt.Print(">") 38 | text, _ := reader.ReadString('\n') 39 | if text == "exit" { 40 | fmt.Printf("Exiting comm channel %s\n", channel) 41 | break 42 | } 43 | if err := c.SendText(text); err != nil { 44 | fmt.Printf("Error: %s, exiting....\n", err) 45 | break 46 | } 47 | } 48 | */ 49 | package client 50 | 51 | import ( 52 | "errors" 53 | "fmt" 54 | "sync" 55 | "sync/atomic" 56 | "time" 57 | 58 | "github.com/golang/glog" 59 | "github.com/gorilla/websocket" 60 | "github.com/johnsiilver/boutique/example/chatterbox/messages" 61 | ) 62 | 63 | // ChatterBox is a client for the ChatterBox service. 64 | // You must service all publically available channels or the client might 65 | // freeze. 66 | // TODO(johnsiilver): Fix that, its ridiculous. 67 | type ChatterBox struct { 68 | // The websocket connection. 69 | conn *websocket.Conn 70 | 71 | mu sync.Mutex 72 | channel atomic.Value 73 | kill chan struct{} 74 | 75 | user atomic.Value // holds a string 76 | dead atomic.Value // holds a bool 77 | deadOnce sync.Once 78 | 79 | // Messages are text messages arriving from the server to this client. 80 | Messages chan messages.Server 81 | // ServerErrors are errors sent from the server to the client. 82 | ServerErrors chan error 83 | // UserUpdates are updates to the users who are in the comm channel. 84 | UserUpdates chan []string 85 | // ChannelDrop are updates saying we've been unsubscribed to a comm channel. 86 | ChannelDrop chan struct{} 87 | // Subscribed is an update saying we've been subscribed to a comm channel. 88 | Subscribed chan messages.Server 89 | // Done indicates that the server connection is dead, this client is Done. 90 | Done chan struct{} 91 | } 92 | 93 | // New is the constructor for ChatterBox. 94 | func New(addr string) (*ChatterBox, error) { 95 | d := websocket.Dialer{ 96 | HandshakeTimeout: 10 * time.Second, 97 | ReadBufferSize: 1024, 98 | WriteBufferSize: 1024, 99 | EnableCompression: true, 100 | } 101 | conn, resp, err := d.Dial(fmt.Sprintf("ws://%s/", addr), nil) 102 | if err != nil { 103 | if resp != nil { 104 | return nil, fmt.Errorf("problem connecting to server: %s status: %s", resp.Status, err) 105 | } 106 | return nil, fmt.Errorf("problem connecting to server: %s", err) 107 | } 108 | 109 | c := &ChatterBox{ 110 | conn: conn, 111 | kill: make(chan struct{}), 112 | ServerErrors: make(chan error, 10), 113 | Messages: make(chan messages.Server, 10), 114 | UserUpdates: make(chan []string, 10), 115 | ChannelDrop: make(chan struct{}, 1), 116 | Subscribed: make(chan messages.Server, 1), 117 | Done: make(chan struct{}), 118 | } 119 | c.dead.Store(false) 120 | c.channel.Store("") 121 | 122 | go c.serverReceiver() 123 | return c, nil 124 | } 125 | 126 | // serverReceiver receives messages from the ChatterBox server. 127 | func (c *ChatterBox) serverReceiver() { 128 | defer c.makeDead() 129 | 130 | for { 131 | var sm messages.Server 132 | 133 | select { 134 | case v := <-c.readConn(): 135 | switch t := v.(type) { 136 | case error: 137 | glog.Errorf("client error receiving from server, client is dead: %s", t) 138 | return 139 | case messages.Server: 140 | sm = t 141 | default: 142 | glog.Errorf("readConn is broken") 143 | return 144 | } 145 | } 146 | 147 | switch sm.Type { 148 | case messages.SMError: 149 | glog.Infof("server sent an error back: %s", sm.Text.Text) 150 | c.ServerErrors <- errors.New(sm.Text.Text) 151 | case messages.SMSendText: 152 | c.Messages <- sm 153 | case messages.SMSubAck: 154 | glog.Infof("server acknowledged subscription to channel") 155 | c.Subscribed <- sm 156 | c.UserUpdates <- sm.Users 157 | case messages.SMUserUpdate: 158 | c.UserUpdates <- sm.Users 159 | case messages.SMChannelDrop: 160 | c.ChannelDrop <- struct{}{} 161 | default: 162 | glog.Infof("dropping message of type %v, I don't understand the type", sm.Type) 163 | } 164 | } 165 | } 166 | 167 | // readConn reads a single entry off the websocket and returns the result on a chan. 168 | // The result is either an error or messages.Server. 169 | func (c *ChatterBox) readConn() chan interface{} { 170 | ch := make(chan interface{}, 1) 171 | 172 | go func() { 173 | sm := messages.Server{} 174 | if err := c.conn.ReadJSON(&sm); err != nil { 175 | glog.Errorf("problem reading message from server, killing the client connection: %s", err) 176 | ch <- err 177 | } 178 | ch <- sm 179 | }() 180 | return ch 181 | } 182 | 183 | func (c *ChatterBox) makeDead() { 184 | c.deadOnce.Do(func() { 185 | c.dead.Store(true) 186 | close(c.Done) 187 | }) 188 | } 189 | 190 | // Subscribe to a new comm channel. 191 | func (c *ChatterBox) Subscribe(comm, user string) (users []string, err error) { 192 | if comm == "" { 193 | return nil, fmt.Errorf("cannot subscribe to an unnamed comm channel") 194 | } 195 | 196 | if user == "" { 197 | return nil, fmt.Errorf("cannot subscribe with an empty user name") 198 | } 199 | 200 | if c.dead.Load().(bool) { 201 | return nil, fmt.Errorf("this client's connection is dead, can't subscribe") 202 | } 203 | 204 | c.mu.Lock() 205 | defer c.mu.Unlock() 206 | 207 | if c.channel.Load().(string) != "" { 208 | return nil, fmt.Errorf("cannot subscribe to channel %s, you must unsubscribe to channel %s", comm, c.channel.Load().(string)) 209 | } 210 | 211 | msg := messages.Client{Type: messages.CMSubscribe, Channel: comm, User: user} 212 | if err := c.conn.WriteJSON(msg); err != nil { 213 | c.makeDead() 214 | return nil, fmt.Errorf("connection to server is broken, this client is dead: %s", err) 215 | } 216 | 217 | select { 218 | case m := <-c.Subscribed: 219 | c.user.Store(user) 220 | c.channel.Store(comm) 221 | return m.Users, nil 222 | case <-time.After(5 * time.Second): 223 | return nil, fmt.Errorf("never received subscribe acknowledge") 224 | } 225 | } 226 | 227 | // Drop disconnects from a comm channel channel. 228 | func (c *ChatterBox) Drop() error { 229 | if c.dead.Load().(bool) { 230 | return fmt.Errorf("this client's connection is dead, can't drop from a channel") 231 | } 232 | 233 | c.mu.Lock() 234 | defer c.mu.Unlock() 235 | 236 | comm := c.channel.Load().(string) 237 | user := c.user.Load().(string) 238 | if c.channel.Load().(string) == "" { 239 | return nil 240 | } 241 | 242 | msg := messages.Client{Type: messages.CMDrop, Channel: comm, User: user} 243 | if err := c.conn.WriteJSON(msg); err != nil { 244 | c.makeDead() 245 | return fmt.Errorf("connection to server is broken, this client is dead: %s", err) 246 | } 247 | 248 | select { 249 | case _ = <-c.ChannelDrop: 250 | c.user.Store("") 251 | c.channel.Store("") 252 | return nil 253 | case <-time.After(5 * time.Second): 254 | c.makeDead() 255 | return fmt.Errorf("never received drop acknowledge") 256 | } 257 | } 258 | 259 | // SendText sends a text message to others on our comm channel. 260 | func (c *ChatterBox) SendText(t string) error { 261 | c.mu.Lock() 262 | defer c.mu.Unlock() 263 | 264 | if c.channel.Load().(string) == "" { 265 | return fmt.Errorf("you must be subscribed to a channel before sending a message") 266 | } 267 | 268 | msg := messages.Client{ 269 | Type: messages.CMSendText, 270 | Channel: c.channel.Load().(string), 271 | User: c.user.Load().(string), 272 | Text: messages.Text{ 273 | Text: t, 274 | }, 275 | } 276 | if err := msg.Validate(); err != nil { 277 | glog.Errorf("client message had validation error: %s", err) 278 | return err 279 | } 280 | 281 | if err := c.conn.WriteJSON(msg); err != nil { 282 | return fmt.Errorf("connection to server is broken, this client is dead: %s", err) 283 | } 284 | 285 | return nil 286 | } 287 | -------------------------------------------------------------------------------- /example/chatterbox/client/chatterbox/cli/ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | ui "github.com/gizak/termui" 9 | ) 10 | 11 | // Command represents a command sent by the user, which is proceeded with "/". 12 | type Command struct { 13 | // Name is the name of the command. If the user typed /quit, it would be quit. 14 | Name string 15 | // Args is arguments to the command. 16 | Args []string 17 | // Resp is the response from trying the command. 18 | Resp chan string 19 | } 20 | 21 | // NewCommand is the constuctor for Command. 22 | func NewCommand(name string, args []string) Command { 23 | return Command{ 24 | Name: name, 25 | Args: args, 26 | Resp: make(chan string, 1), 27 | } 28 | } 29 | 30 | // Displays contains channels that update the display. 31 | type Displays struct { 32 | Users chan []string 33 | Input chan string 34 | Msgs chan string 35 | } 36 | 37 | // Inputs holds channels that may communicate with various subsystems. 38 | type Inputs struct { 39 | // Msgs are used to send messages to the server. 40 | Msgs chan string 41 | // Cmds are used to tell other client methods to do something, like quit 42 | // or subscribe to a channel. 43 | Cmds chan Command 44 | } 45 | 46 | // Terminal represents the terminal UI. 47 | type Terminal struct { 48 | display Displays 49 | input Inputs 50 | stop chan struct{} 51 | reRenders map[int]chan struct{} 52 | } 53 | 54 | const ( 55 | usersRender = iota 56 | msgsRender 57 | inputRender 58 | ) 59 | 60 | // New returns the Terminal and sets of channels for sending or receiving 61 | // to the display or subysystems. 62 | func New(stop chan struct{}) (*Terminal, Displays, Inputs) { 63 | t := &Terminal{ 64 | stop: stop, 65 | reRenders: map[int]chan struct{}{ 66 | usersRender: make(chan struct{}, 1), 67 | msgsRender: make(chan struct{}, 1), 68 | inputRender: make(chan struct{}, 1), 69 | }, 70 | display: Displays{ 71 | Users: make(chan []string, 1), 72 | Input: make(chan string, 1), 73 | Msgs: make(chan string, 1), 74 | }, 75 | input: Inputs{ 76 | Msgs: make(chan string, 1), 77 | Cmds: make(chan Command, 1), 78 | }, 79 | } 80 | return t, t.display, t.input 81 | } 82 | 83 | // Start is a non-blocking call that starts all the displays. 84 | func (t *Terminal) Start() error { 85 | err := ui.Init() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | go t.UsersDisplay() 91 | go t.InputDisplay() 92 | go t.MessagesDisplay() 93 | go t.Input() 94 | return nil 95 | } 96 | 97 | // Stop stops our terminal UI. 98 | func (t *Terminal) Stop() { 99 | ui.StopLoop() 100 | ui.Close() 101 | } 102 | 103 | // UsersDisplay provides the UI part for the usering listing in the comm channel. 104 | func (t *Terminal) UsersDisplay() { 105 | ls := ui.NewList() 106 | 107 | ls.ItemFgColor = ui.ColorYellow 108 | ls.BorderLabel = "Users" 109 | ls.Height = ui.TermHeight() 110 | ls.Width = 25 111 | ls.Y = 0 112 | ls.X = ui.TermWidth() - 25 113 | 114 | go func() { 115 | ui.Render(ls) 116 | for { 117 | select { 118 | case <-t.stop: 119 | return 120 | case <-t.reRenders[usersRender]: 121 | ls.X = ui.TermWidth() - 25 122 | ls.Height = ui.TermHeight() 123 | ls.Width = 25 124 | case u := <-t.display.Users: 125 | ls.Items = u 126 | } 127 | ui.Render(ls) 128 | } 129 | }() 130 | } 131 | 132 | // InputDisplay provides the UI part for the text Input from each user. 133 | func (t *Terminal) InputDisplay() { 134 | par := ui.NewPar("") 135 | par.BorderLabel = "Input" 136 | par.Height = 10 137 | par.Width = ui.TermWidth() - 25 138 | par.WrapLength = ui.TermWidth() - 25 139 | par.Y = ui.TermHeight() - 10 140 | par.X = 0 141 | par.Text = ">" 142 | 143 | go func() { 144 | for { 145 | ui.Render(par) 146 | select { 147 | case <-t.stop: 148 | return 149 | case <-t.reRenders[inputRender]: 150 | par.Y = ui.TermHeight() - 10 151 | par.Height = 10 152 | par.Width = ui.TermWidth() - 25 153 | par.WrapLength = ui.TermWidth() - 25 154 | case l := <-t.display.Input: 155 | par.Text = ">" + l 156 | } 157 | } 158 | }() 159 | } 160 | 161 | // MessagesDisplay provides the UI part for the Messages from each user. 162 | func (t *Terminal) MessagesDisplay() { 163 | data := make([]string, 0, 300) 164 | 165 | par := ui.NewPar("Welcome to ChatterBox") 166 | par.BorderLabel = "Messages" 167 | par.Height = ui.TermHeight() - 10 168 | par.Width = ui.TermWidth() - 25 169 | par.WrapLength = ui.TermWidth() - 25 170 | par.Y = 0 171 | par.X = 0 172 | 173 | go func() { 174 | for { 175 | ui.Render(par) 176 | select { 177 | case <-t.stop: 178 | return 179 | case <-t.reRenders[msgsRender]: 180 | par.Height = ui.TermHeight() - 10 181 | par.Width = ui.TermWidth() - 25 182 | par.WrapLength = ui.TermWidth() - 25 183 | if len(data) > 0 { 184 | start := 0 185 | if par.Height-3 < len(data) { // I hate magic, but 3 seems to be a magic number for the term. 186 | start = len(data) - par.Height + 3 187 | } 188 | par.Text = strings.Join(data[start:], "\n") 189 | } 190 | case l := <-t.display.Msgs: 191 | data = append(data, l) 192 | // Trim down if we have more than 300 lines. 193 | if len(data) >= 300 { 194 | newData := make([]string, 150, 300) 195 | copy(newData, data[150:]) 196 | data = newData 197 | } 198 | if len(data) > 0 { 199 | start := 0 200 | if par.Height-3 < len(data) { // I hate magic, but 3 seems to be a magic number for the term. 201 | start = len(data) - par.Height + 3 202 | } 203 | par.Text = strings.Join(data[start:], "\n") 204 | } 205 | } 206 | } 207 | }() 208 | 209 | } 210 | 211 | var subscribeRE = regexp.MustCompile(`^subscribe\s+(.+)\s(.*)`) 212 | 213 | // Input handles all input from the mouse and keyboard. 214 | func (t *Terminal) Input() { 215 | content := make([]string, 0, 500) 216 | 217 | ////////////////////////////////// 218 | // Register input handlers 219 | ////////////////////////////////// 220 | 221 | // handle key backspace 222 | ui.Handle("/sys/kbd/C-8", func(e ui.Event) { 223 | if len(content) == 0 { 224 | return 225 | } 226 | content = content[0 : len(content)-1] 227 | t.display.Input <- strings.Join(content, "") 228 | }) 229 | 230 | ui.Handle("/sys/kbd/C-x", func(ui.Event) { 231 | // handle Ctrl + x combination 232 | ui.StopLoop() 233 | return 234 | }) 235 | 236 | ui.Handle("/sys/kbd/C-c", func(ui.Event) { 237 | ui.StopLoop() 238 | return 239 | }) 240 | 241 | ui.Handle("/sys/kbd/", func(e ui.Event) { 242 | switch { 243 | case len(content) == 0: 244 | // Do nothing 245 | case content[0] == "/": 246 | if len(content) == 1 { 247 | t.display.Msgs <- "'/' is not a valid command" 248 | return 249 | } 250 | 251 | v := strings.ToLower(strings.Join(content[1:], "")) 252 | switch { 253 | case v == "quit": 254 | cmd := NewCommand("quit", nil) 255 | t.input.Cmds <- cmd 256 | <-cmd.Resp 257 | ui.StopLoop() 258 | case strings.HasPrefix(v, `subscribe`): 259 | m := subscribeRE.FindStringSubmatch(v) 260 | if len(m) != 3 { 261 | t.display.Msgs <- fmt.Sprintf("/subscribe command incorrect syntax. Expected '/subscribe '") 262 | return 263 | } 264 | cmd := NewCommand("subscribe", []string{m[1], m[2]}) 265 | t.input.Cmds <- cmd 266 | t.display.Msgs <- <-cmd.Resp 267 | content = content[0:0] 268 | t.display.Input <- "" 269 | default: 270 | t.display.Msgs <- fmt.Sprintf("unsupported command: /%s", v) 271 | } 272 | default: 273 | t.input.Msgs <- strings.Join(content, "") 274 | content = content[0:0] 275 | t.display.Input <- "" 276 | } 277 | }) 278 | 279 | // handle a space 280 | ui.Handle("/sys/kbd/", func(e ui.Event) { 281 | if len(content)+1 > 500 { 282 | t.display.Msgs <- fmt.Sprintf("cannot send more than 500 characters") 283 | return 284 | } 285 | content = append(content, " ") 286 | t.display.Input <- strings.Join(content, "") 287 | }) 288 | 289 | // handle all other key pressing 290 | ui.Handle("/sys/kbd", func(e ui.Event) { 291 | s := e.Data.(ui.EvtKbd).KeyStr 292 | if len(content)+1 > 500 { 293 | t.display.Msgs <- fmt.Sprintf("cannot send more than 500 characters") 294 | return 295 | } 296 | content = append(content, s) 297 | t.display.Input <- strings.Join(content, "") 298 | }) 299 | 300 | // tell windows to redraw if the size has changed. 301 | ui.Handle("/sys/wnd/resize", func(ui.Event) { 302 | for _, ch := range t.reRenders { 303 | select { 304 | case ch <- struct{}{}: 305 | default: 306 | } 307 | } 308 | }) 309 | 310 | ////////////////////////////////// 311 | // Loop our ui until ui.StopLoop() to <-t.stop 312 | ////////////////////////////////// 313 | go func() { 314 | defer ui.Close() 315 | ui.Loop() 316 | }() 317 | } 318 | -------------------------------------------------------------------------------- /example/chatterbox/server/server.go: -------------------------------------------------------------------------------- 1 | // Package server implements a websocket server that sets up an irc like server. 2 | package server 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/golang/glog" 12 | "github.com/gorilla/websocket" 13 | "github.com/johnsiilver/boutique" 14 | "github.com/johnsiilver/boutique/example/chatterbox/messages" 15 | "github.com/johnsiilver/boutique/example/chatterbox/server/state" 16 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/actions" 17 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 18 | ) 19 | 20 | var upgrader = websocket.Upgrader{ 21 | ReadBufferSize: 1024, 22 | WriteBufferSize: 1024, 23 | } 24 | 25 | type channel struct { 26 | ctx context.Context 27 | cancel context.CancelFunc 28 | hub *state.Hub 29 | users map[string]bool 30 | } 31 | 32 | // ChatterBox implements a websocket server for sending messages in channels. 33 | // Similar to IRC. 34 | type ChatterBox struct { 35 | chMu sync.RWMutex 36 | channels map[string]*channel 37 | } 38 | 39 | // New is the constructor for ChatterBox. 40 | func New() *ChatterBox { 41 | return &ChatterBox{channels: map[string]*channel{}} 42 | } 43 | 44 | // Handler implements http.HandleFunc. 45 | func (c *ChatterBox) Handler(w http.ResponseWriter, r *http.Request) { 46 | conn, err := upgrader.Upgrade(w, r, nil) 47 | if err != nil { 48 | glog.Errorf("error connecting to server: %s", err) 49 | return 50 | } 51 | 52 | wg := &sync.WaitGroup{} 53 | wg.Add(2) 54 | 55 | go c.clientReceiver(r.Context(), wg, conn) 56 | 57 | wg.Wait() 58 | } 59 | 60 | // subscribe subscribes a user to the channel. 61 | func (c *ChatterBox) subscribe(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, m messages.Client) (*state.Hub, error) { 62 | c.chMu.Lock() 63 | defer c.chMu.Unlock() 64 | 65 | var ( 66 | hub *state.Hub 67 | err error 68 | ) 69 | 70 | mchan, ok := c.channels[m.Channel] 71 | if ok { 72 | hub = mchan.hub 73 | if mchan.users[m.User] { 74 | c.sendError( 75 | conn, 76 | fmt.Errorf("a user named %s is already in this channel: %s", m.User, m.Channel), 77 | ) 78 | return nil, fmt.Errorf("subscribe erorr") 79 | } 80 | } else { 81 | hub, err = state.New(m.Channel) 82 | if err != nil { 83 | return nil, err 84 | } 85 | mchan = &channel{ctx: ctx, cancel: cancel, hub: hub, users: map[string]bool{m.User: true}} 86 | c.channels[m.Channel] = mchan 87 | } 88 | 89 | mchan.users[m.User] = true 90 | if err = hub.Store.Perform(actions.AddUser(m.User)); err != nil { 91 | return nil, err 92 | } 93 | 94 | err = c.write( 95 | conn, 96 | messages.Server{ 97 | Type: messages.SMSubAck, 98 | Users: hub.Store.State().Data.(data.State).Users, 99 | }, 100 | ) 101 | if err != nil { 102 | glog.Errorf("problem writing subAck for user %s to chan %s: %s", m.User, m.Channel, err) 103 | delete(mchan.users, m.User) 104 | if len(mchan.users) == 0 { 105 | delete(c.channels, m.Channel) 106 | } 107 | return nil, fmt.Errorf("could not subscribe user %s: %s", m.User, err) 108 | } 109 | glog.Infof("client %v: subscribed to %s as %s", conn.RemoteAddr(), m.User, m.Channel) 110 | return hub, nil 111 | } 112 | 113 | // unsubscribe unsubscribes user "u" from channel "c". 114 | func (c *ChatterBox) unsubscribe(u string, channel string) { 115 | glog.Infof("called unsubsribe") 116 | c.chMu.Lock() 117 | defer c.chMu.Unlock() 118 | 119 | mchan, ok := c.channels[channel] 120 | if !ok { 121 | glog.Infof("could not find channel %s", channel) 122 | return 123 | } 124 | 125 | mchan.cancel() 126 | delete(mchan.users, u) 127 | if err := mchan.hub.Store.Perform(actions.RemoveUser(u)); err != nil { 128 | glog.Errorf("problem removing user from Store: %s", err) 129 | } 130 | glog.Infof("unsubscribed %s from %s", u, channel) 131 | } 132 | 133 | // clientReceiver is used to process messages that are received over the websocket from the client. 134 | // This is meant to be run in a goroutine as it blocks for the life of the conn and decrements 135 | // wg when it finally ends. 136 | func (c *ChatterBox) clientReceiver(ctx context.Context, wg *sync.WaitGroup, conn *websocket.Conn) { 137 | defer wg.Done() 138 | 139 | var ( 140 | cancel context.CancelFunc 141 | hub *state.Hub 142 | user string 143 | comm string 144 | ) 145 | 146 | for { 147 | m, err := c.read(conn) 148 | if err != nil { 149 | glog.Errorf("client %s terminated its connection", conn.RemoteAddr()) 150 | if cancel != nil { 151 | cancel() 152 | } 153 | return 154 | } 155 | 156 | err = m.Validate() 157 | if err != nil { 158 | glog.Errorf("error: client %v message did not validate: %v: %#+v: ignoring...", conn.RemoteAddr(), err, m) 159 | if err = c.sendError(conn, err); err != nil { 160 | return 161 | } 162 | continue 163 | } 164 | 165 | switch t := m.Type; t { 166 | case messages.CMSendText: 167 | if hub == nil { 168 | if err := c.sendError(conn, fmt.Errorf("cannot send a message, not subscribed to channel")); err != nil { 169 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 170 | return 171 | } 172 | } 173 | 174 | if err := hub.Store.Perform(actions.SendMessage(user, m.Text.Text)); err != nil { 175 | c.sendError(conn, fmt.Errorf("problem calling store.Perform(): %s", err)) 176 | continue 177 | } 178 | case messages.CMSubscribe: 179 | ctx, cancel = context.WithCancel(context.Background()) 180 | defer cancel() 181 | 182 | // If we already are subscribed, unsubscribe. 183 | if hub != nil { 184 | c.unsubscribe(user, comm) 185 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 186 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 187 | return 188 | } 189 | if cancel != nil { 190 | cancel() 191 | } 192 | } 193 | 194 | // Now subscribe to the new channel. 195 | var err error 196 | hub, err = c.subscribe(ctx, cancel, conn, m) 197 | if err != nil { 198 | if err = c.sendError(conn, err); err != nil { 199 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 200 | } 201 | return 202 | } 203 | user = m.User 204 | comm = m.Channel 205 | defer c.unsubscribe(user, comm) 206 | 207 | go c.clientSender(ctx, user, comm, conn, hub.Store) 208 | case messages.CMDrop: 209 | if hub == nil { 210 | if err := c.sendError(conn, fmt.Errorf("error: cannot drop a channel, your not subscribed to any")); err != nil { 211 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 212 | return 213 | } 214 | } 215 | if cancel != nil { 216 | cancel() 217 | } 218 | c.unsubscribe(user, comm) 219 | 220 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 221 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 222 | return 223 | } 224 | hub = nil 225 | user = "" 226 | comm = "" 227 | default: 228 | glog.Errorf("error: client %v had unknown message %v, ignoring", conn.RemoteAddr(), t) 229 | if err := c.sendError(conn, fmt.Errorf("received message type from client %v that the server doesn't understand", t)); err != nil { 230 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 231 | return 232 | } 233 | } 234 | } 235 | } 236 | 237 | // clientSender receives changes to the store's Messages field and pushes them out to 238 | // our websocket clients. 239 | func (c *ChatterBox) clientSender(ctx context.Context, usr string, chName string, conn *websocket.Conn, store *boutique.Store) { 240 | const ( 241 | msgField = "Messages" 242 | usersField = "Users" 243 | ) 244 | 245 | state := store.State() 246 | startData := state.Data.(data.State) 247 | 248 | var lastMsgID = -1 249 | if len(startData.Messages) > 0 { 250 | lastMsgID = startData.Messages[len(startData.Messages)-1].ID 251 | } 252 | 253 | msgCh, msgCancel, err := store.Subscribe(msgField) 254 | if err != nil { 255 | c.sendError(conn, err) 256 | return 257 | } 258 | defer msgCancel() 259 | 260 | usersCh, usersCancel, err := store.Subscribe(usersField) 261 | if err != nil { 262 | c.sendError(conn, err) 263 | return 264 | } 265 | defer usersCancel() 266 | 267 | for { 268 | select { 269 | case msgSig := <-msgCh: 270 | msgs := msgSig.State.Data.(data.State).Messages 271 | if len(msgs) == 0 { // This happens we delete the message queue at the end of this loop. 272 | continue 273 | } 274 | 275 | var toSend []data.Message 276 | toSend, lastMsgID = c.latestMsgs(msgs, lastMsgID) 277 | if len(toSend) > 0 { 278 | if err := c.sendMessages(conn, toSend); err != nil { 279 | glog.Errorf("error sending message to client on channel %s: %s", chName, err) 280 | return 281 | } 282 | } 283 | case userSig := <-usersCh: 284 | if err := c.write(conn, messages.Server{Type: messages.SMUserUpdate, Users: userSig.State.Data.(data.State).Users}); err != nil { 285 | c.sendError(conn, err) 286 | return 287 | } 288 | case <-ctx.Done(): 289 | return 290 | } 291 | } 292 | } 293 | 294 | func (c *ChatterBox) sendError(conn *websocket.Conn, err error) error { 295 | glog.ErrorDepth(1, err) 296 | wErr := c.write( 297 | conn, 298 | messages.Server{ 299 | Type: messages.SMError, 300 | Text: messages.Text{ 301 | Text: err.Error(), 302 | }, 303 | }, 304 | ) 305 | return wErr 306 | } 307 | 308 | // latestMsgs takes the Messages in the store, locates all Messages after 309 | // lastMsgID and then returns a slice containing those Messages and the 310 | // new lastMsgID. 311 | // TODO(johnsiilver): Because these messages have ascending IDs, should probably 312 | // look at the first ID and determine where the lastMsgID is instead of looping. 313 | func (*ChatterBox) latestMsgs(msgs []data.Message, lastMsgID int) ([]data.Message, int) { 314 | if len(msgs) == 0 { 315 | return nil, -1 316 | } 317 | 318 | var ( 319 | toSend []data.Message 320 | i int 321 | msg data.Message 322 | found bool 323 | ) 324 | 325 | for i, msg = range msgs { 326 | if msg.ID == lastMsgID { 327 | if i == len(msgs)-1 { // If its is the last message, then there is nothing new. 328 | return nil, lastMsgID 329 | } 330 | found = true 331 | break 332 | } 333 | } 334 | 335 | switch found { 336 | case true: 337 | if len(msgs) == 1 { 338 | toSend = msgs 339 | lastMsgID = msgs[0].ID 340 | } else { 341 | toSend = msgs[i+1:] 342 | lastMsgID = toSend[len(toSend)-1].ID 343 | } 344 | default: // All the messages are new, so send them all. 345 | toSend = msgs 346 | lastMsgID = toSend[len(toSend)-1].ID 347 | } 348 | return toSend, lastMsgID 349 | } 350 | 351 | // sendMessages sends a list of data.Message to the client via a Websocket. 352 | func (*ChatterBox) sendMessages(conn *websocket.Conn, msgs []data.Message) error { 353 | for _, ts := range msgs { 354 | msg := messages.Server{ 355 | Type: messages.SMSendText, 356 | User: ts.User, 357 | Text: messages.Text{ 358 | Text: ts.Text, 359 | }, 360 | } 361 | if err := websocket.WriteJSON(conn, msg); err != nil { 362 | return err 363 | } 364 | } 365 | return nil 366 | } 367 | 368 | // read reads a Message off the websocket. 369 | func (*ChatterBox) read(conn *websocket.Conn) (messages.Client, error) { 370 | m := messages.Client{} 371 | if err := conn.ReadJSON(&m); err != nil { 372 | return messages.Client{}, err 373 | } 374 | 375 | return m, nil 376 | } 377 | 378 | // write writes a Message to the weboscket. 379 | func (*ChatterBox) write(conn *websocket.Conn, msg messages.Server) error { 380 | defer conn.SetWriteDeadline(time.Time{}) 381 | conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) 382 | if err := conn.WriteJSON(msg); err != nil { 383 | return fmt.Errorf("problem writing msg: %s", err) 384 | } 385 | return nil 386 | } 387 | -------------------------------------------------------------------------------- /example/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/0xAX/notificator" 16 | "github.com/doneland/yquotes" 17 | "github.com/golang/glog" 18 | "github.com/johnsiilver/boutique" 19 | "github.com/johnsiilver/boutique/example/notifier/state" 20 | "github.com/johnsiilver/boutique/example/notifier/state/actions" 21 | "github.com/johnsiilver/boutique/example/notifier/state/data" 22 | "github.com/olekukonko/tablewriter" 23 | ) 24 | 25 | var ( 26 | reader = bufio.NewReader(os.Stdin) 27 | store *boutique.Store 28 | notify *notificator.Notificator 29 | ) 30 | 31 | func init() { 32 | switch runtime.GOOS { 33 | case "linux", "darwin", "windows": 34 | default: 35 | fmt.Printf("Error: your OS %s is not supported\n", runtime.GOOS) 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | // clear clears the terminal screen. 41 | func clear() { 42 | switch runtime.GOOS { 43 | case "linux", "darwin": 44 | cmd := exec.Command("clear") //Linux example, its tested 45 | cmd.Stdout = os.Stdout 46 | cmd.Run() 47 | case "windows": 48 | cmd := exec.Command("cls") //Windows example it is untested, but I think its working 49 | cmd.Stdout = os.Stdout 50 | cmd.Run() 51 | 52 | default: 53 | panic("clear is trying to clear on an unsupported OS, bad developer, bad!!!!") 54 | } 55 | } 56 | 57 | // readInt reads a single character from the command line, looking for an int. 58 | func readInt() (int, error) { 59 | text, err := reader.ReadString('\n') 60 | if err != nil { 61 | return 0, err 62 | } 63 | text = strings.Replace(text, "\n", "", -1) 64 | 65 | i, err := strconv.Atoi(text) 66 | if err != nil { 67 | return 0, fmt.Errorf("%s is not a valid integer: %s", text, err) 68 | } 69 | 70 | return i, nil 71 | } 72 | 73 | // readLine reads the next line of input and returns it. 74 | func readLine() (string, error) { 75 | text, err := reader.ReadString('\n') 76 | if err != nil { 77 | return "", err 78 | } 79 | text = strings.Replace(text, "\n", "", -1) 80 | return text, nil 81 | } 82 | 83 | // topMenu displays the top level menu. 84 | func topMenu() error { 85 | clear() 86 | 87 | fmt.Println("Options") 88 | fmt.Println("----------------------") 89 | fmt.Println("(1)add stock symbol") 90 | fmt.Println("(2)remove stock symbol") 91 | fmt.Println("(3)change buy point") 92 | fmt.Println("(4)change sell point") 93 | fmt.Println("(5)list positions") 94 | fmt.Println("(6)quit") 95 | fmt.Println("----------------------") 96 | fmt.Print("> ") 97 | 98 | i, err := readInt() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | switch i { 104 | case 1: 105 | return addStockMenu() 106 | case 2: 107 | return removeStockMenu() 108 | case 3: 109 | return changeBuyMenu() 110 | case 4: 111 | return changeSellMenu() 112 | case 5: 113 | return listPositions() 114 | case 6: 115 | os.Exit(0) 116 | default: 117 | return fmt.Errorf("%d was not a valid option", i) 118 | } 119 | return nil 120 | } 121 | 122 | // addStockMenu shows the add stock menu. 123 | func addStockMenu() error { 124 | clear() 125 | fmt.Println("Provide the symbol, buy point, and sell point. Comma separated.") 126 | fmt.Println("Exmple: googl;940.25;1050.00") 127 | fmt.Println("----------------------") 128 | fmt.Print("> ") 129 | input, err := readLine() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | sl, err := parse(input, 3) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | buy, err := strconv.ParseFloat(sl[1], 64) 140 | if err != nil { 141 | return fmt.Errorf("%s is incorrect format, 2nd value should be a float64 representing the buy price: %s", input, err) 142 | } 143 | 144 | sell, err := strconv.ParseFloat(sl[2], 64) 145 | if err != nil { 146 | return fmt.Errorf("%s is incorrect format, 3rd value should be a float64 representing the sell price: %s", input, err) 147 | } 148 | sl[0] = strings.ToUpper(sl[0]) 149 | d := data.Stock{Symbol: sl[0], Buy: buy, Sell: sell} 150 | 151 | glog.Infof("adding stock %q", d.Symbol) 152 | s, err := yquotes.NewStock(d.Symbol, false) 153 | if err != nil || s.Price.Last == 0 { 154 | return fmt.Errorf("problem retreiving stock information for %s: %s", d.Symbol, err) 155 | } 156 | 157 | go checkBuySell(d.Symbol) 158 | 159 | if err := store.Perform(actions.Track(d.Symbol, d.Buy, d.Sell)); err != nil { 160 | return fmt.Errorf("problem adding stock information for %s: %s", d.Symbol, err) 161 | } 162 | fmt.Println("Stock added successfully, Hit return to continue") 163 | readLine() 164 | 165 | return nil 166 | } 167 | 168 | // removeStockMenu shows the remove stock menu. 169 | func removeStockMenu() error { 170 | clear() 171 | fmt.Println("Provide the symbol to remove") 172 | fmt.Println("----------------------") 173 | fmt.Print("> ") 174 | input, err := readLine() 175 | if err != nil { 176 | return err 177 | } 178 | input = strings.ToUpper(input) 179 | 180 | if err := store.Perform(actions.Untrack(input)); err != nil { 181 | return fmt.Errorf("problem removing stock %s: %s", input, err) 182 | } 183 | 184 | return nil 185 | } 186 | 187 | // changeBuyMenu shows the change buy point menu. 188 | func changeBuyMenu() error { 189 | clear() 190 | fmt.Println("Provide the symbol;new buy point") 191 | fmt.Println("Example: googl;850") 192 | fmt.Println("----------------------") 193 | fmt.Print("> ") 194 | input, err := readLine() 195 | if err != nil { 196 | return err 197 | } 198 | 199 | sl, err := parse(input, 2) 200 | if err != nil { 201 | return err 202 | } 203 | sl[0] = strings.ToUpper(sl[0]) 204 | 205 | buy, err := strconv.ParseFloat(sl[1], 64) 206 | if err != nil || buy <= 0 { 207 | return fmt.Errorf("the second value you entered was not a valid price") 208 | } 209 | 210 | if _, ok := store.State().Data.(data.State).Tracking[sl[0]]; !ok { 211 | return fmt.Errorf("error: you are not currently tracking stock %s", sl[0]) 212 | } 213 | 214 | glog.Infof("changing stock %q buy to: %v", sl[0], buy) 215 | 216 | if err := store.Perform(actions.ChangeBuy(sl[0], buy), boutique.NoUpdate()); err != nil { 217 | return fmt.Errorf("problem changing stock information for %s: %s", sl[0], err) 218 | } 219 | fmt.Println("Buy changed successfully, Hit return to continue") 220 | readLine() 221 | 222 | return nil 223 | } 224 | 225 | // changeSellMenu shows the change sell point menu. 226 | func changeSellMenu() error { 227 | clear() 228 | fmt.Println("Provide the symbol;new sell point") 229 | fmt.Println("Example: googl;850") 230 | fmt.Println("----------------------") 231 | fmt.Print("> ") 232 | input, err := readLine() 233 | if err != nil { 234 | return err 235 | } 236 | 237 | sl, err := parse(input, 2) 238 | if err != nil { 239 | return err 240 | } 241 | sl[0] = strings.ToUpper(sl[0]) 242 | 243 | sell, err := strconv.ParseFloat(sl[1], 64) 244 | if err != nil || sell <= 0 { 245 | return fmt.Errorf("the second value you entered was not a valid price") 246 | } 247 | 248 | if _, ok := store.State().Data.(data.State).Tracking[sl[0]]; !ok { 249 | return fmt.Errorf("error: you are not currently tracking stock %s", sl[0]) 250 | } 251 | 252 | glog.Infof("changing stock %q sell to: %v", sl[0], sell) 253 | 254 | if err := store.Perform(actions.ChangeSell(sl[0], sell), boutique.NoUpdate()); err != nil { 255 | return fmt.Errorf("problem changing stock information for %s: %s", sl[0], err) 256 | } 257 | fmt.Println("Sell changed successfully, Hit return to continue") 258 | readLine() 259 | 260 | return nil 261 | } 262 | 263 | // bySymbol is a sorter to sort printed output by stock symbol. 264 | type bySymbol [][]string 265 | 266 | func (a bySymbol) Len() int { return len(a) } 267 | func (a bySymbol) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 268 | func (a bySymbol) Less(i, j int) bool { return a[i][0] < a[j][0] } 269 | 270 | func listPositions() error { 271 | clear() 272 | fmt.Println("Current positions being watched:") 273 | fmt.Println("----------------------") 274 | out := [][]string{} 275 | for _, v := range store.State().Data.(data.State).Tracking { 276 | out = append(out, []string{v.Symbol, fmt.Sprintf("%v", v.Buy), fmt.Sprintf("%v", v.Sell), fmt.Sprintf("%v", v.Current)}) 277 | } 278 | 279 | sort.Sort(bySymbol(out)) 280 | 281 | table := tablewriter.NewWriter(os.Stdout) 282 | table.SetHeader([]string{"Symbol", "Buy", "Sell", "Current"}) 283 | 284 | for _, v := range out { 285 | table.Append(v) 286 | } 287 | table.Render() // Send output 288 | 289 | fmt.Println("Hit return to continue") 290 | readLine() 291 | return nil 292 | } 293 | 294 | // parse parses the input strings. 295 | func parse(input string, expecting int) ([]string, error) { 296 | sl := strings.Split(input, ";") 297 | switch len(sl) { 298 | case expecting + 1: 299 | if sl[len(sl)-1] != "" { 300 | return nil, fmt.Errorf("%s is incorrect format, expecting %d values got %d", input, expecting, len(sl)) 301 | } 302 | sl = sl[:len(sl)-1] 303 | case expecting: 304 | default: 305 | return nil, fmt.Errorf("%s is incorrect format, expecting %d values got %d", input, expecting, len(sl)) 306 | } 307 | return sl, nil 308 | } 309 | 310 | // watcher loops forever and updates all prices of stocks we are tracking. 311 | func watcher() { 312 | wg := sync.WaitGroup{} 313 | for { 314 | for k, v := range store.State().Data.(data.State).Tracking { 315 | k := k 316 | v := v 317 | 318 | go func() { 319 | stock, err := yquotes.NewStock(k, false) 320 | if err != nil { 321 | glog.Errorf("problem retreiving stock information for %s: %s", k, err) 322 | return 323 | } 324 | 325 | if v.Current != stock.Price.Last { 326 | if err := store.Perform(actions.ChangeCurrent(k, stock.Price.Last)); err != nil { 327 | glog.Errorf("problem updating store for current price of stock %s: %s", k, err) 328 | } 329 | } 330 | }() 331 | } 332 | 333 | wg.Wait() 334 | time.Sleep(10 * time.Second) 335 | } 336 | } 337 | 338 | // checkBuySell watches stock updates and notifies us if the stock is in a buy 339 | // or sell position. 340 | func checkBuySell(symbol string) { 341 | // Use these to tell if we have already sent a notification for buying or selling. 342 | shouldBuyLast := false 343 | shouldSellLast := false 344 | 345 | ch, cancel, err := store.Subscribe("Tracking") 346 | if err != nil { 347 | panic(fmt.Sprintf("software is broken, bad developer: %s", err)) 348 | } 349 | 350 | defer cancel() 351 | defer glog.Infof("unsubscribing for %s", symbol) 352 | 353 | for up := range ch { 354 | m := up.State.Data.(data.State).Tracking 355 | 356 | // If the map doesn't contain our stock symbol, it has been untracked. 357 | stock, ok := m[symbol] 358 | if !ok { 359 | return 360 | } 361 | 362 | // We haven't updated the stock data yet. 363 | if stock.Current == 0 { 364 | continue 365 | } 366 | 367 | if stock.Current <= stock.Buy { 368 | if !shouldBuyLast { 369 | notify.Push( 370 | fmt.Sprintf("Buy %s", symbol), 371 | fmt.Sprintf("Stock %s reached Buy limit of %v, current: %v", symbol, stock.Buy, stock.Current), 372 | "/home/user/icon.png", 373 | notificator.UR_CRITICAL, 374 | ) 375 | glog.Infof("stock %s is at Buy. Buy was %v, current is %v", symbol, stock.Buy, stock.Current) 376 | shouldBuyLast = true 377 | } 378 | } else { 379 | shouldBuyLast = false 380 | } 381 | 382 | if stock.Current >= stock.Sell { 383 | if !shouldSellLast { 384 | notify.Push( 385 | fmt.Sprintf("Sell %s", symbol), 386 | fmt.Sprintf("Stock %s reached Sell limit of %v, current: %v", symbol, stock.Sell, stock.Current), 387 | "/home/user/icon.png", 388 | notificator.UR_CRITICAL, 389 | ) 390 | shouldSellLast = true 391 | } 392 | } else { 393 | shouldSellLast = false 394 | } 395 | } 396 | } 397 | 398 | func main() { 399 | var err error 400 | store, err = state.New() 401 | if err != nil { 402 | fmt.Printf("Error: %s\n", err) 403 | os.Exit(1) 404 | } 405 | 406 | notify = notificator.New(notificator.Options{ 407 | DefaultIcon: "icon/default.png", 408 | AppName: "Stock Notifier", 409 | }) 410 | 411 | go watcher() 412 | 413 | for { 414 | if err := topMenu(); err != nil { 415 | fmt.Println(err) 416 | fmt.Println("Hit return to continue") 417 | readLine() 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /boutique_test.go: -------------------------------------------------------------------------------- 1 | package boutique 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/golang/glog" 12 | "github.com/kylelemons/godebug/pretty" 13 | "github.com/lukechampine/freeze" 14 | ) 15 | 16 | const ( 17 | unknown = iota 18 | incrCounter 19 | decrCounter 20 | appendList 21 | statusChange 22 | ) 23 | 24 | type MyState struct { 25 | Counter int 26 | Status string 27 | List []string 28 | Dict map[string]bool 29 | private bool 30 | } 31 | 32 | // Actions 33 | 34 | func IncrCounter() Action { 35 | return Action{ 36 | Type: incrCounter, 37 | } 38 | } 39 | 40 | func DecrCounter() Action { 41 | return Action{ 42 | Type: decrCounter, 43 | } 44 | } 45 | 46 | func Status(s string) Action { 47 | return Action{ 48 | Type: statusChange, 49 | Update: s, 50 | } 51 | } 52 | 53 | // ChangeList replaces the items in the list with l. 54 | func AppendList(item string) Action { 55 | return Action{ 56 | Type: appendList, 57 | Update: item, 58 | } 59 | } 60 | 61 | // Updaters 62 | 63 | func UpCounter(state interface{}, action Action) interface{} { 64 | s := state.(MyState) 65 | switch action.Type { 66 | case incrCounter: 67 | s.Counter++ 68 | case decrCounter: 69 | s.Counter-- 70 | } 71 | return s 72 | } 73 | 74 | func UpStatus(state interface{}, action Action) interface{} { 75 | s := state.(MyState) 76 | switch action.Type { 77 | case statusChange: 78 | s.Status = action.Update.(string) 79 | } 80 | return s 81 | } 82 | 83 | func UpList(state interface{}, action Action) interface{} { 84 | s := state.(MyState) 85 | 86 | switch action.Type { 87 | case appendList: 88 | nl := CopyAppendSlice(s.List, action.Update.(string)).([]string) 89 | s.List = nl 90 | } 91 | return s 92 | } 93 | 94 | func TestModifier(t *testing.T) { 95 | t.Parallel() 96 | m := NewModifiers(UpCounter, UpStatus) 97 | s := MyState{} 98 | 99 | v := m.run(s, IncrCounter()) 100 | if v.(MyState).Counter != 1 { 101 | t.Errorf("Test TestModifier: Counter: got %d, want %d", v.(MyState).Counter, 1) 102 | } 103 | v = m.run(s, IncrCounter()) 104 | if v.(MyState).Counter != 1 { 105 | t.Errorf("Test TestModifier: Counter: got %d, want %d", v.(MyState).Counter, 1) 106 | } 107 | v = m.run(s, DecrCounter()) 108 | if v.(MyState).Counter != -1 { 109 | t.Errorf("Test TestModifier: Counter: got %d, want %d", v.(MyState).Counter, -1) 110 | } 111 | v = m.run(s, Status("Hello World")) 112 | if v.(MyState).Status != "Hello World" { 113 | t.Errorf("Test TestModifier: Status: got %s, want %s", v.(MyState).Status, "Hello World") 114 | } 115 | } 116 | 117 | func TestCopyAndAppendSlice(t *testing.T) { 118 | t.Parallel() 119 | glog.Infof("TestCopyAndAppendSlice") 120 | defer glog.Infof("End TestCopyAndAppendSlice") 121 | 122 | a := "apples" 123 | b := "oranges" 124 | 125 | tests := []struct { 126 | desc string 127 | slice interface{} 128 | item interface{} 129 | want interface{} 130 | err bool 131 | }{ 132 | { 133 | desc: "Error: 'slice' arg is not a slice", 134 | slice: 3, 135 | item: 2, 136 | err: true, 137 | }, 138 | { 139 | desc: "Error: 'item' arg is not same type as slice", 140 | slice: []string{"apples"}, 141 | item: 2, 142 | err: true, 143 | }, 144 | { 145 | desc: "Success with []string", 146 | slice: []string{"apples"}, 147 | item: "oranges", 148 | want: []string{"apples", "oranges"}, 149 | }, 150 | { 151 | desc: "Success with []*string", 152 | slice: []*string{&a}, 153 | item: &b, 154 | want: []*string{&a, &b}, 155 | }, 156 | } 157 | 158 | conf := pretty.Config{ 159 | IncludeUnexported: false, 160 | Diffable: true, 161 | } 162 | 163 | for _, test := range tests { 164 | // Make sure we don't modify the starting slice. 165 | if reflect.TypeOf(test.slice).Kind() == reflect.Slice { 166 | freeze.Slice(test.slice) 167 | } 168 | 169 | got, err := copyAppendSlice(test.slice, test.item) 170 | 171 | switch { 172 | case err == nil && test.err: 173 | t.Errorf("Test %s: got err == nil, want err != nil", test.desc) 174 | continue 175 | case err != nil && !test.err: 176 | t.Errorf("Test %s: got err != %s, want err == nil", test.desc, err) 177 | continue 178 | case err != nil: 179 | continue 180 | } 181 | 182 | if diff := conf.Compare(test.want, got); diff != "" { 183 | t.Errorf("Test %s: -want/+got:\n%s", test.desc, diff) 184 | } 185 | } 186 | } 187 | 188 | func TestFieldsChanged(t *testing.T) { 189 | t.Parallel() 190 | glog.Infof("TestFieldsChanged") 191 | defer glog.Infof("End TestFieldsChanged") 192 | 193 | os := MyState{Dict: map[string]bool{}} 194 | ns := MyState{Counter: 1, Dict: map[string]bool{}} 195 | 196 | changed := fieldsChanged(os, ns) 197 | if diff := pretty.Compare([]string{"Counter"}, changed); diff != "" { 198 | t.Errorf("TestFieldsChanged: -want/+got:\n%s", diff) 199 | } 200 | 201 | ns.List = append(ns.List, "hello") 202 | changed = fieldsChanged(os, ns) 203 | if diff := pretty.Compare([]string{"Counter", "List"}, changed); diff != "" { 204 | t.Errorf("TestFieldsChanged: -want/+got:\n%s", diff) 205 | } 206 | ns.Dict["key"] = true 207 | changed = fieldsChanged(os, ns) 208 | if diff := pretty.Compare([]string{"Counter", "List", "Dict"}, changed); diff != "" { 209 | t.Errorf("TestFieldsChanged: -want/+got:\n%s", diff) 210 | } 211 | 212 | // changed should not alter because ns.private is a a private variable. 213 | ns.private = true 214 | changed = fieldsChanged(os, ns) 215 | if diff := pretty.Compare([]string{"Counter", "List", "Dict"}, changed); diff != "" { 216 | t.Errorf("TestFieldsChanged: -want/+got:\n%s", diff) 217 | } 218 | } 219 | 220 | type counters struct { 221 | counter, status, list, dict, any int 222 | } 223 | 224 | func TestSubscribe(t *testing.T) { 225 | t.Parallel() 226 | glog.Infof("TestSubscribe") 227 | defer glog.Infof("End TestSubscribe") 228 | 229 | wg := sync.WaitGroup{} 230 | glog.Infof("--subscribeSignalsCorrectly") 231 | wg.Add(1) 232 | go func() { 233 | defer wg.Done() 234 | defer glog.Infof("--end subscribeSignalsCorrectly") 235 | subscribeSignalsCorrectly(t) 236 | }() 237 | glog.Infof("--signalsDontBlock") 238 | wg.Add(1) 239 | go func() { 240 | defer wg.Done() 241 | defer glog.Infof("--end signalsDontBlock") 242 | signalsDontBlock(t) 243 | }() 244 | 245 | glog.Infof("--signalsAlwaysLatest") 246 | wg.Add(1) 247 | go func() { 248 | defer wg.Done() 249 | defer glog.Infof("--end signalsAlwaysLatest") 250 | signalsAlwaysLatest(t) 251 | }() 252 | 253 | glog.Infof("--cancelWorks") 254 | wg.Add(1) 255 | go func() { 256 | defer wg.Done() 257 | defer glog.Infof("--end cancelWorks") 258 | cancelWorks(t) 259 | }() 260 | 261 | wg.Wait() 262 | } 263 | 264 | func subscribeSignalsCorrectly(t *testing.T) { 265 | initial := MyState{ 266 | Counter: 0, 267 | Status: "", 268 | List: []string{}, 269 | Dict: map[string]bool{}, 270 | } 271 | switch runtime.GOOS { 272 | case "darwin", "linux": 273 | freeze.Object(&initial) 274 | } 275 | 276 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 277 | if err != nil { 278 | t.Fatalf("TestPerform: %s", err) 279 | } 280 | 281 | sch, _, err := s.Subscribe("Status") 282 | if err != nil { 283 | t.Fatalf("TestPerform: %s", err) 284 | } 285 | lch, _, err := s.Subscribe("List") 286 | if err != nil { 287 | t.Fatalf("TestPerform: %s", err) 288 | } 289 | ach, _, err := s.Subscribe(Any) 290 | if err != nil { 291 | t.Fatalf("TestPerform: %s", err) 292 | } 293 | 294 | _, _, err = s.Subscribe("private") 295 | if err == nil { 296 | t.Errorf("TestPerform: s.Subcribe(\"private\"): err == nil, want error != nil") 297 | } 298 | 299 | mu := &sync.Mutex{} 300 | count := counters{} 301 | countDone := make(chan Signal, 1) 302 | go func() { 303 | for { 304 | select { 305 | case s := <-sch: 306 | if s.Fields[0] != "Status" { 307 | t.Fatalf("Test subscribeSignalsCorrectly: Signal for field Status had FieldsChanged set to: %s", s.Fields[0]) 308 | } 309 | mu.Lock() 310 | count.status++ 311 | mu.Unlock() 312 | case s := <-lch: 313 | if s.Fields[0] != "List" { 314 | t.Fatalf("Test subscribeSignalsCorrectly: Signal for field List had FieldsChanged set to: %s", s.Fields[0]) 315 | } 316 | mu.Lock() 317 | count.list++ 318 | mu.Unlock() 319 | case <-ach: 320 | mu.Lock() 321 | count.any++ 322 | mu.Unlock() 323 | } 324 | countDone <- Signal{} 325 | } 326 | }() 327 | 328 | status := "status" 329 | loops := 1000 330 | for i := 0; i < loops; i++ { 331 | status = status + ":changed:" 332 | s.Perform(Status(status)) 333 | <-countDone 334 | <-countDone // For Any 335 | 336 | if i%2 == 0 { 337 | s.Perform(AppendList("a")) 338 | <-countDone 339 | <-countDone // For Any 340 | } 341 | } 342 | 343 | want := counters{status: loops, list: loops / 2, any: loops + (loops / 2)} 344 | if diff := pretty.Compare(want, count); diff != "" { 345 | t.Errorf("TestSubscribe: -want/+got:\n%s", diff) 346 | } 347 | } 348 | 349 | func signalsDontBlock(t *testing.T) { 350 | // NOTE: initial must fully be filled out or freeze will nil pointer dereference. 351 | initial := MyState{ 352 | Counter: 0, 353 | Status: "", 354 | List: []string{}, 355 | Dict: map[string]bool{}, 356 | } 357 | switch runtime.GOOS { 358 | case "darwin", "linux": 359 | freeze.Object(&initial) 360 | } 361 | 362 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 363 | if err != nil { 364 | t.Fatalf("signalsDontBlock: %s", err) 365 | } 366 | 367 | ch, _, err := s.Subscribe("Counter") 368 | if err != nil { 369 | t.Fatalf("signalsDontBlock: %s", err) 370 | } 371 | 372 | for i := 0; i < 100; i++ { 373 | s.Perform(IncrCounter()) 374 | } 375 | // time.Sleep() things are prone to break, but I don't want to have to put a 376 | // signaling mechanism to know when the go cast() of finished. 377 | time.Sleep(1 * time.Second) 378 | 379 | // Remove two items, as there is a buffer of 1. 380 | <-ch 381 | 382 | select { 383 | case <-ch: 384 | t.Errorf("signalsDontBlock: got <-blocked had something on it, want <-block to block") 385 | default: 386 | } 387 | } 388 | 389 | func signalsAlwaysLatest(t *testing.T) { 390 | initial := MyState{ 391 | Counter: 0, 392 | Status: "", 393 | List: []string{}, 394 | Dict: map[string]bool{}, 395 | } 396 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 397 | if err != nil { 398 | t.Fatalf("signalsAlwaysLatest: %s", err) 399 | } 400 | 401 | ch, cancel, err := s.Subscribe("Counter") 402 | if err != nil { 403 | t.Fatalf("signalsDontBlock: %s", err) 404 | } 405 | 406 | wg := sync.WaitGroup{} 407 | var got int 408 | wg.Add(1) 409 | go func() { 410 | defer wg.Done() 411 | for sig := range ch { 412 | got = sig.State.Data.(MyState).Counter 413 | } 414 | }() 415 | 416 | for i := 0; i < 100000; i++ { 417 | s.Perform(IncrCounter()) 418 | } 419 | 420 | cancel() 421 | wg.Wait() 422 | if got != s.State().Data.(MyState).Counter { 423 | t.Errorf("signalsAlwaysLatest: got %v, want %v", got, s.State().Data.(MyState).Counter) 424 | } 425 | } 426 | 427 | func cancelWorks(t *testing.T) { 428 | initial := MyState{Counter: 0} 429 | 430 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 431 | if err != nil { 432 | t.Fatalf("cancelWorks: %s", err) 433 | } 434 | 435 | tests := []struct { 436 | desc string 437 | subscriptions int 438 | keyExist bool 439 | }{ 440 | { 441 | desc: "1 subscriber", 442 | subscriptions: 1, 443 | keyExist: false, 444 | }, 445 | { 446 | desc: "2 subscribers", 447 | subscriptions: 2, 448 | keyExist: true, 449 | }, 450 | } 451 | 452 | for _, test := range tests { 453 | var ( 454 | ch chan Signal 455 | cancel CancelFunc 456 | err error 457 | ) 458 | 459 | for i := 0; i < test.subscriptions; i++ { 460 | // We only want to keep the last set of these. 461 | ch, cancel, err = s.Subscribe("Counter") 462 | if err != nil { 463 | t.Fatalf("cancelWorks: test %s: %s", test.desc, err) 464 | } 465 | } 466 | 467 | s.Perform(IncrCounter()) 468 | 469 | sawBroadcast := make(chan Signal, 10) 470 | chClosed := make(chan Signal) 471 | go func() { 472 | defer close(chClosed) 473 | for _ = range ch { 474 | sawBroadcast <- Signal{} 475 | } 476 | }() 477 | 478 | select { 479 | case <-sawBroadcast: 480 | case <-time.After(5 * time.Second): 481 | t.Fatalf("cancelWorks: test %s: did not see anything on the subscription channel", test.desc) 482 | } 483 | 484 | cancel() 485 | 486 | s.Perform(IncrCounter()) 487 | 488 | select { 489 | case <-chClosed: 490 | case <-time.After(5 * time.Second): 491 | t.Fatalf("cancelWorks: test %s: did not see the channel close", test.desc) 492 | } 493 | 494 | _, ok := s.subscribers["Counter"] 495 | if test.keyExist && !ok { 496 | t.Fatalf("cancelWorks, test %s: expected to see the key in subscribers, but didn't", test.desc) 497 | } 498 | if !test.keyExist && ok { 499 | t.Fatalf("cancelWorks, test %s: expected to not find the key in subscribers, but did", test.desc) 500 | } 501 | } 502 | } 503 | 504 | func TestPerform(t *testing.T) { 505 | t.Parallel() 506 | initial := MyState{ 507 | Counter: 0, 508 | Status: "", 509 | List: []string{}, 510 | Dict: map[string]bool{}, 511 | } 512 | switch runtime.GOOS { 513 | case "darwin", "linux": 514 | freeze.Object(&initial) 515 | } 516 | 517 | s, err := New(initial, NewModifiers(UpCounter, UpStatus, UpList), nil) 518 | if err != nil { 519 | t.Fatalf("TestPerform: %s", err) 520 | } 521 | 522 | wg := &sync.WaitGroup{} 523 | status := "" 524 | list := []string{} 525 | loop := 1000 526 | for i := 0; i < loop; i++ { 527 | wg.Add(1) 528 | status = status + "a" 529 | s.Perform(Status(status), WaitForCommit(wg)) // Cannot be done in a goroutine. 530 | 531 | wg.Add(1) 532 | go s.Perform(IncrCounter(), WaitForCommit(wg)) 533 | 534 | wg.Add(1) 535 | list = append(list, "a") 536 | go s.Perform(AppendList("a"), WaitForCommit(wg)) 537 | } 538 | 539 | wg.Wait() 540 | 541 | diff := pretty.Compare( 542 | MyState{ 543 | Counter: 1000, 544 | Status: status, 545 | List: list, 546 | }, 547 | s.State().Data.(MyState), 548 | ) 549 | 550 | if s.State().Version != uint64(loop*3) { 551 | t.Errorf("Test TestPerform: got Version == %d, want %d", s.State().Version, loop*3) 552 | } 553 | 554 | switch { 555 | case s.State().FieldVersions["Status"] != uint64(loop): 556 | t.Errorf("Test TestPerform: got FieldVersion['Status'] == %d, want %d", s.State().FieldVersions["Status"], loop) 557 | case s.State().FieldVersions["Counter"] != uint64(loop): 558 | t.Errorf("Test TestPerform: got FieldVersion['Counter'] == %d, want %d", s.State().FieldVersions["Counter"], loop) 559 | case s.State().FieldVersions["List"] != uint64(loop): 560 | t.Errorf("Test TestPerform: got FieldVersion['List'] == %d, want %d", s.State().FieldVersions["List"], loop) 561 | } 562 | 563 | if diff != "" { 564 | t.Errorf("Test TestPerform: -want/+got:\n%s", diff) 565 | } 566 | } 567 | 568 | func TestMiddleware(t *testing.T) { 569 | t.Parallel() 570 | initial := MyState{ 571 | Counter: -1, 572 | } 573 | 574 | logs := []int{} 575 | logger := func(args *MWArgs) (changedData interface{}, stop bool, err error) { 576 | go func() { 577 | defer args.WG.Done() 578 | state := <-args.Committed 579 | if state.IsZero() { 580 | return 581 | } 582 | logs = append(logs, state.Data.(MyState).Counter) 583 | }() 584 | return nil, false, nil 585 | } 586 | 587 | fiveHundredToOneThousand := func(args *MWArgs) (changedData interface{}, stop bool, err error) { 588 | defer args.WG.Done() 589 | data := args.NewData.(MyState) 590 | if data.Counter == 500 { 591 | data.Counter = 1000 592 | } 593 | return data, false, nil 594 | } 595 | 596 | skipSevenHundred := func(args *MWArgs) (changedData interface{}, stop bool, err error) { 597 | defer args.WG.Done() 598 | data := args.NewData.(MyState) 599 | if data.Counter == 700 { 600 | return nil, true, nil 601 | } 602 | return nil, false, nil 603 | } 604 | 605 | sawSevenHundred := false 606 | sevenHundred := func(args *MWArgs) (changedData interface{}, stop bool, err error) { 607 | defer args.WG.Done() 608 | data := args.NewData.(MyState) 609 | if data.Counter == 700 { 610 | sawSevenHundred = true 611 | } 612 | return nil, false, nil 613 | } 614 | 615 | errorEightHundred := func(args *MWArgs) (changedData interface{}, stop bool, err error) { 616 | defer args.WG.Done() 617 | data := args.NewData.(MyState) 618 | if data.Counter == 800 { 619 | return nil, false, fmt.Errorf("error") 620 | } 621 | return nil, false, nil 622 | } 623 | 624 | middle := []Middleware{logger, fiveHundredToOneThousand, skipSevenHundred, sevenHundred, errorEightHundred} 625 | 626 | s, err := New(initial, NewModifiers(UpCounter), middle) 627 | if err != nil { 628 | t.Fatalf("TestPerform: %s", err) 629 | } 630 | 631 | for i := 0; i < 1000; i++ { 632 | s.Perform(IncrCounter()) 633 | } 634 | 635 | wantLogs := []int{} 636 | counter := 0 637 | for i := 0; i < 1000; i++ { 638 | switch counter { 639 | case 500: 640 | wantLogs = append(wantLogs, 1000) 641 | counter = 1001 642 | case 800: 643 | continue 644 | default: 645 | wantLogs = append(wantLogs, counter) 646 | counter++ 647 | } 648 | } 649 | 650 | if sawSevenHundred == true { 651 | t.Errorf("Test TestMiddleware: middleware sevenHundred was called on update of 700, which should have been prevented by skipSevenHundred") 652 | } 653 | 654 | if diff := pretty.Compare(logs, wantLogs); diff != "" { 655 | t.Errorf("Test TestMiddleware: -want/+got:\n%s", diff) 656 | } 657 | } 658 | 659 | func TestSignaler(t *testing.T) { 660 | t.Parallel() 661 | s := newSignaler() 662 | 663 | for i := 0; i <= 100000; i++ { 664 | s.insert(Signal{Version: uint64(i)}) 665 | if i%10000 == 0 { 666 | time.Sleep(10 * time.Millisecond) 667 | } 668 | } 669 | s.close() 670 | 671 | var v uint64 672 | for sig := range s.ch { 673 | v = sig.Version 674 | } 675 | 676 | if v != 100000 { 677 | t.Errorf("TestSignaler: got %v, want %v", v, 100000) 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /boutique.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package boutique provides an immutable state storage with subscriptions to 3 | changes in the store. It is intended to be a single storage for individual client 4 | data or a single state store for an application not requiring high QPS. 5 | 6 | Features and Drawbacks 7 | 8 | Features: 9 | 10 | * Immutable data does not require locking outside the store. 11 | * Subscribing to individual field changes are simple. 12 | * Data locking is handled by the Store. 13 | 14 | Drawbacks: 15 | 16 | * You are giving up static type checks on compile. 17 | * Internal reflection adds about 1.8x overhead. 18 | * Must be careful to not mutate data. 19 | 20 | Immutability 21 | 22 | When we say immutable, we mean that everything gets copied, as Go 23 | does not have immutable objects or types other than strings. This means every 24 | update to a pointer or reference type (map, dict, slice) must make a copy of the 25 | data before changing it, not a mutation. Because of modern processors, this 26 | copying is quite fast. 27 | 28 | Usage structure 29 | 30 | Boutique provides storage that is best designed in a modular method: 31 | 32 | └── state 33 | ├── state.go 34 | ├── actions 35 | │   └── actions.go 36 | ├── data 37 | | └── data.go 38 | ├── middleware 39 | | └── middleware.go 40 | └── modifiers 41 | └── modifiers.go 42 | 43 | The files are best organized by using them as follows: 44 | 45 | state.go - Holds the constructor for a boutique.Store for your application 46 | actions.go - Holds the actions that will be used by the updaters to update the store 47 | data.go - Holds the definition of your state object 48 | middleware.go = Holds middleware for acting on proposed changes to your data. This is not required 49 | modifiers.go - Holds all the Modifier(s) that are used by the boutique.Store to modify the store's data 50 | 51 | 52 | Note: These are all simply suggestions, you can combine this in a single file or name the files whatever you wish. 53 | 54 | Example 55 | 56 | Please see github.com/johnsiilver/boutique for a complete guide to using this 57 | package. Its complicated enough to warrant some documentation to guide you 58 | through. 59 | 60 | If your very impatient, there is an example directory with examples of verying complexity. 61 | */ 62 | package boutique 63 | 64 | import ( 65 | "fmt" 66 | "reflect" 67 | "regexp" 68 | "sort" 69 | "sync" 70 | "sync/atomic" 71 | "time" 72 | 73 | "github.com/golang/glog" 74 | "github.com/mohae/deepcopy" 75 | ) 76 | 77 | // Any is used to indicated to Store.Subscribe() that you want updates for 78 | // any update to the store, not just a field. 79 | const Any = "any" 80 | 81 | var ( 82 | publicRE = regexp.MustCompile(`^[A-Z].*`) 83 | ) 84 | 85 | // Signal is used to signal upstream subscribers that a field in the Store.Store 86 | // has changed. 87 | type Signal struct { 88 | // Version is the version of the field that was changed. If Any was passed, it will 89 | // be the store's version, not a specific field. 90 | Version uint64 91 | 92 | // Fields are the field names that were updated. This is only a single name unless 93 | // Any is used. 94 | Fields []string 95 | 96 | // State is the new State object. 97 | State State 98 | } 99 | 100 | // FieldChanged loops over Fields to deterimine if "f" exists. 101 | // len(s.Fields) is always small, so a linear search is optimal. 102 | // Only useful if you are subscribed to "Any", as otherwise its a single entry. 103 | func (s Signal) FieldChanged(f string) bool { 104 | for _, field := range s.Fields { 105 | if field == f { 106 | return true 107 | } 108 | } 109 | return false 110 | } 111 | 112 | // signaler provides a way to extract the latest Signal from a Store.Subscription. 113 | // Simply listen on ch to get updates. 114 | type signaler struct { 115 | ch chan Signal 116 | // mu protects everything below. 117 | mu sync.Mutex 118 | latest *Signal 119 | done chan struct{} 120 | } 121 | 122 | func newSignaler() *signaler { 123 | s := &signaler{ 124 | ch: make(chan Signal), 125 | done: make(chan struct{}), 126 | } 127 | s.handler() 128 | return s 129 | } 130 | 131 | func (s *signaler) handler() { 132 | go func() { 133 | defer close(s.ch) 134 | 135 | for { 136 | switch s.sendSleepReturn() { 137 | case sleep: 138 | time.Sleep(1 * time.Millisecond) 139 | case loop: 140 | case closed: 141 | return 142 | } 143 | } 144 | }() 145 | } 146 | 147 | const ( 148 | loop = 0 149 | closed = 1 150 | sleep = 2 151 | ) 152 | 153 | // sendSleepReturn sends on ch if we have a new Signal, returns sleep if we 154 | // can't send the signal or there is no signal to send, and returns closed 155 | // if the signaler is has had close() called. 156 | func (s *signaler) sendSleepReturn() int { 157 | s.mu.Lock() 158 | defer s.mu.Unlock() 159 | 160 | sig := s.latest 161 | if sig != nil { 162 | select { 163 | case s.ch <- *sig: 164 | s.latest = nil 165 | return loop 166 | default: 167 | return sleep 168 | } 169 | } 170 | 171 | if s.isClosed() { 172 | return closed 173 | } 174 | return sleep 175 | } 176 | 177 | func (s *signaler) isClosed() bool { 178 | select { 179 | case <-s.done: 180 | return true 181 | default: 182 | return false 183 | } 184 | } 185 | 186 | // close closes the signaler. 187 | func (s *signaler) close() { 188 | s.mu.Lock() 189 | defer s.mu.Unlock() 190 | 191 | close(s.done) 192 | } 193 | 194 | // insert puts a new Signal out. This is not thread safe. 195 | func (s *signaler) insert(sig Signal) { 196 | s.mu.Lock() 197 | defer s.mu.Unlock() 198 | 199 | select { 200 | case <-s.done: 201 | panic("inserting on closed signaler") 202 | default: 203 | } 204 | s.latest = &sig 205 | } 206 | 207 | // ActionType indicates what type of Action this is. 208 | type ActionType int 209 | 210 | func (a ActionType) isActionType() bool { 211 | return true 212 | } 213 | 214 | // Action represents an action to take on the Store. 215 | type Action struct { 216 | // Type should be an enumerated constant representing the type of Action. 217 | // It is valuable to use http://golang.org/x/tools/cmd/stringer to allow 218 | // for string representation. 219 | Type ActionType 220 | 221 | // Update holds the values to alter in the Update. 222 | Update interface{} 223 | } 224 | 225 | // Modifier takes in the existing state and an action to perform on the state. 226 | // The result will be the new state. 227 | // Implementation of an Modifier must be careful to not mutate "state", it must 228 | // work on a copy only. If you are changing a reference type contained in 229 | // state, you must make a copy of that reference first and then manipulate 230 | // the copy, storing it in the new state object. 231 | type Modifier func(state interface{}, action Action) interface{} 232 | 233 | // Modifiers provides the internals the ability to use the Modifier. 234 | type Modifiers struct { 235 | updater Modifier 236 | } 237 | 238 | // NewModifiers creates a new Modifiers with the Modifiers provided. 239 | func NewModifiers(modifiers ...Modifier) Modifiers { 240 | return Modifiers{updater: combineModifier(modifiers...)} 241 | } 242 | 243 | // run calls the updater on state/action. 244 | func (m Modifiers) run(state interface{}, action Action) interface{} { 245 | return m.updater(state, action) 246 | } 247 | 248 | // State holds the state data. 249 | type State struct { 250 | // Version is the version of the state this represents. Each change updates 251 | // this version number. 252 | Version uint64 253 | 254 | // FieldVersions holds the version each field is at. This allows us to track 255 | // individual field updates. 256 | FieldVersions map[string]uint64 257 | 258 | // Data is the state data. They type is some type of struct. 259 | Data interface{} 260 | } 261 | 262 | // IsZero indicates that the State isn't set. 263 | func (s State) IsZero() bool { 264 | if s.Data == nil { 265 | return true 266 | } 267 | return false 268 | } 269 | 270 | // GetState returns the state of the Store. 271 | type GetState func() State 272 | 273 | // MWArgs are the arguments to a Middleware implmentor. 274 | type MWArgs struct { 275 | // Action is the Action that is being performed. 276 | Action Action 277 | // NewData is the proposed new State.Data field in the Store. This can be modified by the 278 | // Middleware and returned as the changedData return value. 279 | NewData interface{} 280 | // GetState if a function that will return the current State of the Store. 281 | GetState GetState 282 | // Committed is only used if the Middleware will spin off a goroutine. In that case, 283 | // the committed state will be sent via this channel. This allows Middleware that wants 284 | // to do something based on the final state (like logging) to work. If the data was not 285 | // committed due to another Middleware cancelling the commit, State.IsZero() will be true. 286 | Committed chan State 287 | 288 | // WG must have .Done() called by all Middleware once it has finished. If using Committed, you must 289 | // not call WG.Done() until your goroutine is completed. 290 | WG *sync.WaitGroup 291 | } 292 | 293 | // Middleware provides a function that is called before the state is written. The Action that 294 | // is being applied is passed, with the newData that is going to be commited, a method to get the current state, 295 | // and committed which will close when newData is committed. It returns either a changed version of newData or 296 | // nil if newData is unchanged. It returns an indicator if we should stop processing middleware but continue 297 | // with our commit of the newData. And it returns an error if we should not commit. 298 | // Finally the "wg" WaitGroup that is passed must have .Done() called when the Middleware finishes. 299 | // "committed" can be ignored unless the middleware wants to spin off a goroutine that does something after 300 | // the data is committed. If the data is not committed because another Middleware returns an error, the channel will 301 | // be closed with an empty state. This ability allow Middleware that performs things such as logging the final result. 302 | // If using this ability, do not call wg.Done() until all processing is done. 303 | type Middleware func(args *MWArgs) (changedData interface{}, stop bool, err error) 304 | 305 | // combineModifier takes multiple Modifiers and combines them into a 306 | // single instance. 307 | // Note: We do not provide any safety here. If you 308 | func combineModifier(updaters ...Modifier) Modifier { 309 | return func(state interface{}, action Action) interface{} { 310 | if err := validateState(state); err != nil { 311 | panic(err) 312 | } 313 | 314 | for _, u := range updaters { 315 | state = u(state, action) 316 | } 317 | return state 318 | } 319 | } 320 | 321 | // validateState validates that state is actually a Struct. 322 | func validateState(state interface{}) error { 323 | if reflect.TypeOf(state).Kind() != reflect.Struct { 324 | return fmt.Errorf("a state may only be of type struct, which does not include *struct, was: %s", reflect.TypeOf(state).Kind()) 325 | } 326 | return nil 327 | } 328 | 329 | // subscribers holds a mapping of field names to channels that will receive 330 | // an update when the field name changes. A special field "any" will be updated 331 | // for any change. 332 | type subscribers map[string][]subscriber 333 | 334 | type subscriber struct { 335 | id int 336 | sig *signaler 337 | } 338 | 339 | type stateChange struct { 340 | old, new interface{} 341 | newVersion uint64 342 | newFieldVersions map[string]uint64 343 | changed []string 344 | } 345 | 346 | // CancelFunc is used to cancel a subscription 347 | type CancelFunc func() 348 | 349 | func cancelFunc(c *Store, field string, id int) CancelFunc { 350 | return func() { 351 | c.smu.Lock() 352 | defer c.smu.Unlock() 353 | 354 | v := c.subscribers[field] 355 | if len(v) == 1 { 356 | v[0].sig.close() 357 | delete(c.subscribers, field) 358 | return 359 | } 360 | 361 | l := make([]subscriber, 0, len(v)-1) 362 | for _, s := range v { 363 | if s.id == id { 364 | s.sig.close() 365 | continue 366 | } 367 | l = append(l, s) 368 | } 369 | c.subscribers[field] = l 370 | } 371 | } 372 | 373 | type performOptions struct { 374 | committed *sync.WaitGroup 375 | subscribe chan State 376 | noUpdate bool 377 | } 378 | 379 | // PerformOption is an optional arguement to Store.Peform() calls. 380 | type PerformOption func(p *performOptions) 381 | 382 | // WaitForCommit passed a WaitGroup that will be decremented by 1 when a 383 | // Perform is completed. This option allows you to use goroutines to call 384 | // Perform, continue on and then wait for the commit to be completed. 385 | // Because you cannot increment the WaitGroup before the Perform, you must wait 386 | // until Peform() is completed. If doing Perform in a 387 | func WaitForCommit(wg *sync.WaitGroup) PerformOption { 388 | return func(p *performOptions) { 389 | p.committed = wg 390 | } 391 | } 392 | 393 | // WaitForSubscribers passes a channel that will receive the State from a change 394 | // once all subscribers have been updated with this state. This channel should 395 | // generally have a buffer of 1. If not, the Perform() will return an error. 396 | // If the channel is full when it tries to update the channel, no update will 397 | // be sent. 398 | func WaitForSubscribers(ch chan State) PerformOption { 399 | return func(p *performOptions) { 400 | p.subscribe = ch 401 | } 402 | } 403 | 404 | // NoUpdate indicates that when this Perform() is run, no subscribers affected 405 | // should receive an update for this change. 406 | func NoUpdate() PerformOption { 407 | return func(p *performOptions) { 408 | p.noUpdate = true 409 | } 410 | } 411 | 412 | // Store provides access to the single data store for the application. 413 | // The Store is thread-safe. 414 | type Store struct { 415 | // mod holds all the state modifiers. 416 | mod Modifiers 417 | 418 | // middle holds all the Middleware we must apply. 419 | middle []Middleware 420 | 421 | // pmu prevents concurrent Perform() calls. 422 | pmu sync.Mutex 423 | 424 | // state is current state of the Store. Its value is a interface{}, so we 425 | // don't know the type, but it is guarenteed to be a struct. 426 | state atomic.Value 427 | 428 | // version holds the version of the store. This will be the same as .state.Version. 429 | version atomic.Value // uint64 430 | 431 | // fieldVersions is the versions of each field. This will be teh same as .state.FieldVersions. 432 | fieldVersions atomic.Value // map[string]uint64 433 | 434 | // smu protects subscribers and sid. 435 | smu sync.RWMutex 436 | 437 | // subscribers holds the map of subscribers for different fields. 438 | subscribers subscribers 439 | 440 | // sid is an id for a subscriber. 441 | sid int 442 | } 443 | 444 | // New is the constructor for Store. initialState should be a struct that is 445 | // used for application's state. All Modifiers in mod must return the same struct 446 | // that initialState contains or you will receive a panic. 447 | func New(initialState interface{}, mod Modifiers, middle []Middleware) (*Store, error) { 448 | if err := validateState(initialState); err != nil { 449 | return nil, err 450 | } 451 | 452 | if mod.updater == nil { 453 | return nil, fmt.Errorf("mod must contain at least one Modifier") 454 | } 455 | 456 | fieldVersions := map[string]uint64{} 457 | for _, f := range fieldList(initialState) { 458 | fieldVersions[f] = 0 459 | } 460 | 461 | s := &Store{mod: mod, subscribers: subscribers{}, middle: middle} 462 | s.state.Store(State{Version: 0, FieldVersions: fieldVersions, Data: initialState}) 463 | s.version.Store(uint64(0)) 464 | s.fieldVersions.Store(fieldVersions) 465 | 466 | return s, nil 467 | } 468 | 469 | // Perform performs an Action on the Store's state. 470 | func (s *Store) Perform(a Action, options ...PerformOption) error { 471 | opts := &performOptions{} 472 | for _, opt := range options { 473 | opt(opts) 474 | } 475 | 476 | defer func() { 477 | if opts.committed != nil { 478 | opts.committed.Done() 479 | } 480 | }() 481 | 482 | s.pmu.Lock() 483 | defer s.pmu.Unlock() 484 | 485 | state := s.state.Load().(State) 486 | n := s.mod.run(state.Data, a) 487 | 488 | var ( 489 | commitChans []chan State 490 | err error 491 | ) 492 | 493 | middleWg := &sync.WaitGroup{} 494 | middleWg.Add(len(s.middle)) 495 | n, commitChans, err = s.processMiddleware(a, n, middleWg) 496 | if err != nil { 497 | for _, ch := range commitChans { 498 | close(ch) 499 | } 500 | return err 501 | } 502 | 503 | s.perform(state, n, commitChans, opts) 504 | 505 | done := make(chan struct{}) 506 | timer := time.NewTimer(5 * time.Second) 507 | go func() { 508 | middleWg.Wait() 509 | close(done) 510 | }() 511 | 512 | // This helps users diagnose misbehaving middleware. 513 | for { 514 | select { 515 | case <-done: 516 | timer.Stop() 517 | case <-timer.C: 518 | glog.Infof("middleware is taking longer that 5 seconds, did you call wg.Done()?") 519 | continue 520 | } 521 | break 522 | } 523 | 524 | return nil 525 | } 526 | 527 | func (s *Store) processMiddleware(a Action, newData interface{}, wg *sync.WaitGroup) (data interface{}, commitChans []chan State, err error) { 528 | commitChans = make([]chan State, len(s.middle)) 529 | for i := 0; i < len(commitChans); i++ { 530 | commitChans[i] = make(chan State, 1) 531 | } 532 | 533 | for i, m := range s.middle { 534 | cd, stop, err := m(&MWArgs{Action: a, NewData: newData, GetState: s.State, Committed: commitChans[i], WG: wg}) 535 | if err != nil { 536 | return nil, nil, err 537 | } 538 | 539 | if cd != nil { 540 | newData = cd 541 | } 542 | 543 | if stop { 544 | break 545 | } 546 | } 547 | return newData, commitChans, nil 548 | } 549 | 550 | func (s *Store) perform(state State, n interface{}, commitChans []chan State, opts *performOptions) { 551 | changed := fieldsChanged(state.Data, n) 552 | 553 | // This can happen if middleware interferes. 554 | if len(changed) == 0 { 555 | return 556 | } 557 | 558 | // Copy the field versions so that its safe between loaded states. 559 | fieldVersions := make(map[string]uint64, len(state.FieldVersions)) 560 | for k, v := range state.FieldVersions { 561 | fieldVersions[k] = v 562 | } 563 | 564 | // Update the field versions that had changed. 565 | for _, k := range changed { 566 | fieldVersions[k] = fieldVersions[k] + 1 567 | } 568 | sort.Strings(changed) 569 | 570 | sc := stateChange{ 571 | old: state.Data, 572 | new: n, 573 | newVersion: state.Version + 1, 574 | newFieldVersions: fieldVersions, 575 | changed: changed, 576 | } 577 | 578 | writtenState := s.write(sc, opts) 579 | 580 | for _, ch := range commitChans { 581 | ch <- writtenState 582 | } 583 | } 584 | 585 | // write processes the change in state. 586 | func (s *Store) write(sc stateChange, opts *performOptions) State { 587 | state := State{Data: sc.new, Version: sc.newVersion, FieldVersions: sc.newFieldVersions} 588 | s.state.Store(state) 589 | s.version.Store(sc.newVersion) 590 | s.fieldVersions.Store(sc.newFieldVersions) 591 | 592 | if opts.noUpdate { 593 | return state 594 | } 595 | 596 | s.smu.RLock() 597 | defer s.smu.RUnlock() 598 | 599 | if len(s.subscribers) > 0 { 600 | s.cast(sc, state, opts) 601 | } 602 | return state 603 | } 604 | 605 | // Subscribe creates a subscriber to be notified when a field is updated. 606 | // The notification comes over the returned channel. If the field is set to 607 | // the Any enumerator, any field change in the state data sends an update. 608 | // CancelFunc() can be called to cancel the subscription. On cancel, chan Signal 609 | // will be closed after the last entry is pulled from the channel. 610 | // The returned channel is guarenteed to have the latest data at the time the 611 | // Signal is returned. If you pull off the channel slower than the sender, 612 | // you will still receive the latest data when you pull off the channel. 613 | // It is not guarenteed that you will see every update, only the latest. 614 | // If you need every update, you need to write middleware. 615 | func (s *Store) Subscribe(field string) (chan Signal, CancelFunc, error) { 616 | if field != Any && !publicRE.MatchString(field) { 617 | return nil, nil, fmt.Errorf("cannot subscribe to a field that is not public: %s", field) 618 | } 619 | 620 | if field != Any && !fieldExist(field, s.State().Data) { 621 | return nil, nil, fmt.Errorf("cannot subscribe to non-existing field: %s", field) 622 | } 623 | 624 | sig := newSignaler() 625 | 626 | s.smu.Lock() 627 | defer s.smu.Unlock() 628 | defer func() { s.sid++ }() 629 | 630 | if v, ok := s.subscribers[field]; ok { 631 | s.subscribers[field] = append(v, subscriber{id: s.sid, sig: sig}) 632 | } else { 633 | s.subscribers[field] = []subscriber{ 634 | {id: s.sid, sig: sig}, 635 | } 636 | } 637 | return sig.ch, cancelFunc(s, field, s.sid), nil 638 | } 639 | 640 | // State returns the current stored state. 641 | func (s *Store) State() State { 642 | return s.state.Load().(State) 643 | } 644 | 645 | // Version returns the current version of the Store. 646 | func (s *Store) Version() uint64 { 647 | return s.version.Load().(uint64) 648 | } 649 | 650 | // FieldVersion returns the current version of field "f". If "f" doesn't exist, 651 | // the default value 0 will be returned. 652 | func (s *Store) FieldVersion(f string) uint64 { 653 | return s.fieldVersions.Load().(map[string]uint64)[f] 654 | } 655 | 656 | // cast updates subscribers for data changes. 657 | func (s *Store) cast(sc stateChange, state State, opts *performOptions) { 658 | s.smu.RLock() 659 | defer s.smu.RUnlock() 660 | 661 | for _, field := range sc.changed { 662 | if v, ok := s.subscribers[field]; ok { 663 | for _, sub := range v { 664 | sig := Signal{Version: sc.newFieldVersions[field], State: state, Fields: []string{field}} 665 | sub.sig.insert(sig) 666 | } 667 | } 668 | } 669 | 670 | for _, sub := range s.subscribers["any"] { 671 | sub.sig.insert(Signal{Version: sc.newVersion, State: state, Fields: sc.changed}) 672 | } 673 | 674 | if opts.subscribe != nil { 675 | select { 676 | case opts.subscribe <- state: 677 | // Do nothing 678 | default: 679 | glog.Errorf("someone passed a WaitForSubscribers with a full channel") 680 | } 681 | } 682 | } 683 | 684 | // signal sends a Signal on a Signler. 685 | func signal(sig Signal, signaler *signaler, wg *sync.WaitGroup, opts *performOptions) { 686 | defer wg.Done() 687 | 688 | signaler.insert(sig) 689 | } 690 | 691 | // fieldExists returns true if the field exists in "i". This will panic if 692 | // "i" is not a struct. 693 | func fieldExist(f string, i interface{}) bool { 694 | return reflect.ValueOf(i).FieldByName(f).IsValid() 695 | } 696 | 697 | // fieldsChanged detects if a field changed between a and z. It reports that 698 | // field name in the return. It is assumed a and z are the same type, if not 699 | // this will not work correctly. 700 | func fieldsChanged(a, z interface{}) []string { 701 | r := []string{} 702 | 703 | av := reflect.ValueOf(a) 704 | zv := reflect.ValueOf(z) 705 | 706 | for i := 0; i < av.NumField(); i++ { 707 | if av.Field(i).CanInterface() { 708 | if !reflect.DeepEqual(av.Field(i).Interface(), zv.Field(i).Interface()) { 709 | r = append(r, av.Type().Field(i).Name) 710 | } 711 | } 712 | } 713 | return r 714 | } 715 | 716 | // FieldList takes in a struct and returns a list of all its field names. 717 | // This will panic if "st" is not a struct. 718 | func fieldList(st interface{}) []string { 719 | v := reflect.TypeOf(st) 720 | sl := make([]string, v.NumField()) 721 | for i := 0; i < v.NumField(); i++ { 722 | sl[i] = v.Field(i).Name 723 | } 724 | return sl 725 | } 726 | 727 | // ShallowCopy makes a copy of a value. On pointers or references, you will 728 | // get a copy of the pointer, not of the underlying value. 729 | func ShallowCopy(i interface{}) interface{} { 730 | return i 731 | } 732 | 733 | // DeepCopy makes a DeepCopy of all elements in "from" into "to". 734 | // "to" must be a pointer to the type of "from". 735 | // "from" and "to" must be the same type. 736 | // Private fields will not be copied. It this is needed, you should handle 737 | // the copy method yourself. 738 | func DeepCopy(from, to interface{}) error { 739 | if reflect.TypeOf(to).Kind() != reflect.Ptr { 740 | return fmt.Errorf("DeepCopy: to must be a pointer") 741 | } 742 | 743 | cpy := deepcopy.Copy(from) 744 | glog.Infof("%v", reflect.ValueOf(to).CanSet()) 745 | reflect.ValueOf(to).Elem().Set(reflect.ValueOf(cpy)) 746 | 747 | return nil 748 | } 749 | 750 | // CopyAppendSlice takes a slice, copies the slice into a new slice and appends 751 | // item to the new slice. If slice is not actually a slice or item is not the 752 | // same type as []Type, then this will panic. 753 | // This is simply a convenience function for copying then appending to a slice. 754 | // It is faster to do this by hand without the reflection. 755 | // This is also not a deep copy, its simply copies the underlying array. 756 | func CopyAppendSlice(slice interface{}, item interface{}) interface{} { 757 | i, err := copyAppendSlice(slice, item) 758 | if err != nil { 759 | panic(err) 760 | } 761 | return i 762 | } 763 | 764 | // copyAppendSlice implements CopyAppendSlice, but with an error if there is 765 | // a type mismatch. This makes it easier to test. 766 | func copyAppendSlice(slice interface{}, item interface{}) (interface{}, error) { 767 | t := reflect.TypeOf(slice) 768 | if t.Kind() != reflect.Slice { 769 | return nil, fmt.Errorf("CopyAppendSlice 'slice' argument was a %s", reflect.TypeOf(slice).Kind()) 770 | } 771 | if t.Elem().Kind() != reflect.TypeOf(item).Kind() { 772 | return nil, fmt.Errorf("CopyAppendSlice item is of type %s, but slice is of type %s", t.Elem(), reflect.TypeOf(item).Kind()) 773 | } 774 | 775 | slicev := reflect.ValueOf(slice) 776 | var newcap, newlen int 777 | if slicev.Len() == slicev.Cap() { 778 | newcap = slicev.Len() + 1 779 | newlen = newcap 780 | } else { 781 | newlen = slicev.Len() + 1 782 | newcap = slicev.Cap() 783 | } 784 | 785 | ns := reflect.MakeSlice(slicev.Type(), newlen, newcap) 786 | 787 | reflect.Copy(ns, slicev) 788 | 789 | ns.Index(newlen - 1).Set(reflect.ValueOf(item)) 790 | return ns.Interface(), nil 791 | } 792 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Boutique 2 | 3 | ## One line summary 4 | 5 | Boutique is an immutable state store with subscriptions to field changes. 6 | 7 | ## The long summary 8 | 9 | Boutique is an experiment in versioned, generic state storage for Go. 10 | 11 | It provides a state store for storing immutable data. This allows data 12 | retrieved from the store to be used without synchronization. 13 | 14 | In addition, Boutique allows subscriptions to be registered for changes to a 15 | data field or any field changes. Data is versioned, so you can compare the 16 | version number between the data retrieved and the last data pulled. 17 | 18 | Finally, Boutique supports middleware for any change that is being committed 19 | to the store. This allows for features like debugging, long term storage, 20 | authorization checks, ... to be created. 21 | 22 | ## Best use cases? 23 | 24 | Boutique is useful for: 25 | 26 | * A web based application that stores state on the server and not in 27 | Javascript clients. I like to use it instead of Redux. 28 | * An application that has lots of clients, each which need to store state and 29 | receive updates. 30 | * An application that has listeners sharing a single state with updates pushed 31 | to all listeners. 32 | 33 | ## Before we get started 34 | 35 | ### Go doesn't have immutable objects, does it? 36 | 37 | Correct, Go doesn't have immutable objects. It does contain immutable types, 38 | such as strings and constants. However, immutability in this case is simply a 39 | contract to only change the data through the Boutique store. All changes in 40 | the store must copy the data before committing the changes. 41 | 42 | On Unix based systems, it is possible to test your code to ensure no mutations. 43 | See: http://godoc.org/github.com/lukechampine/freeze 44 | 45 | I have seen no way to do this for Windows. 46 | 47 | ## What is the cost of using a generic immutable store? 48 | 49 | There are three main drawbacks for using Boutique: 50 | 51 | * Boutique writes are slower than a non generic implementation due to type 52 | assertion, reflection and data copies 53 | * In very specific circumstances, Boutique can have runtime errors due to using 54 | interface{} 55 | * Storage updates are done via **Actions**, which adds some complexity 56 | 57 | The first, running slower is because we must not only type assert at different 58 | points, but reflection is used to detect changes in the data fields of the 59 | stored data. We also need to copy data out of maps, slices, etc... into new 60 | maps, slices, etc... This cost is lessened by reads of data without 61 | synchronization and reduced complexity in the subscription model. 62 | 63 | The second, runtime errors, happen when one of two events occur. The type of 64 | data to be stored in Boutique is changed on a write. The first data type 65 | passed to the store is the only type that can be stored. Any attempt to store 66 | a different type of data will result in an error. The second way is if the 67 | data being stored in Boutique is not a struct type. The top level data must be 68 | a struct. In a non-generic store, these would be caught by the compiler. But 69 | these are generally non-issues. 70 | 71 | The third is more difficult. Changes are routed through **Actions**. 72 | **Actions** trigger **Modifers**, which also must be written. The concepts 73 | take a bit to understand and you have to be careful not to mutate the data when 74 | writing **Modifier(s)**. This adds a certain amount of complexity. But once 75 | you get used to the method, the code is easy to follow. 76 | 77 | ## Where are some example applications? 78 | 79 | You can find several example applications of varying sophistication here: 80 | 81 | IRC like chat server/client using websockets with a sample terminal UI. 82 | Welcome back to the 70's: 83 | 84 | [http://github.com/johnsiilver/boutique/example/chatterbox](http://github.com/johnsiilver/boutique/blob/master/example/chatterbox/chatterbox.go) 85 | 86 | Stock buy/sell point notifier using desktop notifications: 87 | 88 | [http://github.com/johnsiilver/boutique/example/notifier](http://github.com/johnsiilver/boutique/blob/master/example/notifier/notifier.go) 89 | 90 | ## What does using Boutique look like? 91 | 92 | Forgetting all the setup, usage looks like this: 93 | ```go 94 | // Create a boutique.Store which holds the State object (not defined here) 95 | // with a Modifier function called AddUser (for changing field State.User). 96 | store, err := boutique.New(State{}, boutique.NewModifiers(AddUser), nil) 97 | if err != nil { 98 | // Handle the error. 99 | } 100 | 101 | // Create a subscription to changes in the "Users" field. 102 | userNotify, cancel, err := store.Subscribe("Users") 103 | if err != nil { 104 | // Handle the error. 105 | } 106 | defer cancel() // Cancel our subscription when the function closes. 107 | 108 | // Print out the latest user list whenever State.Users changes. 109 | go func(){ 110 | for signal := range userNotify { 111 | fmt.Println("current users:") 112 | for _, user := range signal.State.Data(State).Users { 113 | fmt.Printf("\t%s\n", user) 114 | } 115 | } 116 | }() 117 | 118 | // Change field .Users to contain "Mary". 119 | if err := store.Perform(AddUser("Mary")); err != nil { 120 | // Handle the error. 121 | } 122 | 123 | // Change field .Users to contain "Joe". 124 | if err := store.Perform(AddUser("Joe")); err != nil { 125 | // Handle the error. 126 | } 127 | 128 | // We can also just grab the state at any time. 129 | s := store.State() 130 | fmt.Println(s.Version) // The current version of the Store. 131 | fmt.Println(s.FieldVersions["Users"]) // The version of the .Users field. 132 | fmt.Println(s.Data.(State).Users) // The .Users field. 133 | 134 | ``` 135 | 136 | Key things to note here: 137 | 138 | * The State object retrieved from the signal requires no locks. 139 | * Perform() calls do not require locks. 140 | * Everything is versioned. 141 | * Subscribers only receive the **latest** update, not every update. 142 | This cuts down on unnecessary processing (it is possible, with Middleware 143 | to get every update). 144 | * This is just scratching the surface with what you can do, especially with 145 | Middleware. 146 | 147 | ## Start simply: the basics 148 | 149 | [http://github.com/johnsiilver/boutique/example/basic](http://github.com/johnsiilver/boutique/blob/master/example/basic/basic.go) 150 | 151 | This application simply spins up a bunch of goroutines and we use a 152 | boutique.Store to track the number of goroutines running. 153 | 154 | In itself, not practical, but it will help define our concepts. 155 | 156 | ### First, define what data you want to store 157 | 158 | To start with, the data to be stored must be of type struct. Now to be clear, 159 | this cannot be a pointer to struct (\*struct), it must be a plain struct. It 160 | is also important to note that only public fields can received notification of 161 | subscriber changes. 162 | 163 | Here's the state we want to store: 164 | ```go 165 | // State is our state data for Boutique. 166 | type State struct { 167 | // Goroutines is how many goroutines we are running. 168 | Goroutines int 169 | } 170 | ``` 171 | 172 | ### Now, we need to define Actions for making changes to the State 173 | 174 | ```go 175 | // These are our ActionTypes. This informs us of what kind of change we want 176 | // to do with an Action. 177 | const ( 178 | // ActIncr indicates we are incrementing the Goroutines field. 179 | ActIncr boutique.ActionType = iota 180 | 181 | // ActDecr indicates we are decrementing the Gorroutines field. 182 | ActDecr 183 | ) 184 | 185 | // IncrGoroutines creates an ActIncr boutique.Action. 186 | func IncrGoroutines(n int) boutique.Action { 187 | return boutique.Action{Type: ActIncr, Update: n} 188 | } 189 | 190 | // DecrGoroutines creates and ActDecr boutique.Action. 191 | func DecrGoroutines() boutique.Action { 192 | return boutique.Action{Type: ActDecr} 193 | } 194 | ``` 195 | Here we have two Action creator functions: 196 | * IncrGoroutines which is used to increment the Goroutines count by n 197 | * DecrGoroutines which is used to decrement the Goroutines count by 1 198 | 199 | **boutique.Action** contains two fields: 200 | * Type - Indicates the type of change that is to be made 201 | * Update - a blank interface{} where you can store whatever information 202 | is needed for the change. In the case of an ActIncr change, it is the 203 | number of Goroutines we are adding. It can also be nil, as sometimes you only 204 | need the Type to make the change. 205 | 206 | ### Define our Modifiers, which are what implement a change to the State 207 | 208 | ```go 209 | // HandleIncrDecr is a boutique.Modifier for handling ActIncr and ActDecr boutique.Actions. 210 | func HandleIncrDecr(state interface{}, action boutique.Action) interface{} { 211 | s := state.(State) 212 | 213 | switch action.Type { 214 | case ActIncr: 215 | s.Goroutines = s.Goroutines + action.Update.(int) 216 | case ActDecr: 217 | s.Goroutines = s.Goroutines - 1 218 | } 219 | 220 | return s 221 | } 222 | ``` 223 | We only have a single Modifier which handles Actions of type **ActIncr** and 224 | **ActDecr**. We could have made two Modifier(s), but opted for a single one. 225 | 226 | A modifier has to implement the following signature: 227 | ```go 228 | type Modifier func(state interface{}, action Action) interface{} 229 | ``` 230 | 231 | So let's talk about what is going on. A **Modifier** is called when a change is 232 | being made to the boutique.Store. It is passed a copy of the data that is 233 | stored. We need to modify that data if the action that is passed is one that 234 | our Modifier is designed for. 235 | 236 | First, we transform the **copy** of our State object into its concrete 237 | state (instead of interface{}). 238 | 239 | Now we check to see if this is an **action.Type** we handle. If not, we simply 240 | skip doing anything (which will return the state data as it was before the 241 | **Modifier** was called). 242 | 243 | If it was an **ActIncr Action**, we increment **.Goroutines** by 244 | **action.Update**, which will be of type **int**. 245 | 246 | If it was an **ActDecr Action**, we decrement **.Goroutines** by 1. 247 | 248 | ### Let's create a subscriber to print out the current .Gouroutines number 249 | ```go 250 | func printer(killMe, done chan struct{}, store *boutique.Store) { 251 | defer close(done) 252 | defer store.Perform(DecrGoroutines()) 253 | 254 | // Subscribe to the .Goroutines field changes. 255 | ch, cancel, err := store.Subscribe("Goroutines") 256 | if err != nil { 257 | panic(err) 258 | } 259 | defer cancel() // Cancel our subscription when this goroutine ends. 260 | 261 | for { 262 | select { 263 | case sig := <-ch: // This is the latest change to the .Goroutines field. 264 | fmt.Println(sig.State.Data.(State).Goroutines) 265 | // Put a 1 second pause in. Remember, we won't receive 1000 increment 266 | // signals and 1000 decrement signals. We will always receive the 267 | // latest data, which may be far less than 2000. 268 | time.Sleep(1 * time.Second) 269 | case <-killMe: // We were told to die. 270 | return 271 | } 272 | } 273 | } 274 | ``` 275 | The **close()** lets others know when this printer dies. 276 | 277 | The **store.Perform(DecrGoroutines())** reduces our goroutine count by 1 when 278 | the printer ends. 279 | 280 | ```go 281 | // Subscribe to the .Goroutines field changes. 282 | ch, cancel, err := store.Subscribe("Goroutines") 283 | if err != nil { 284 | panic(err) 285 | } 286 | defer cancel() // Cancel our subscription when this goroutine ends. 287 | ``` 288 | Here we subscribe to the **.Goroutines** field. Whenever an update happens 289 | to this field, we will get notified on channel **ch**. 290 | 291 | However, we will only get the **latest** update, not every update. 292 | This is important to remember. 293 | 294 | **cancel()** cancels the subscription when the printer ends. 295 | 296 | ```go 297 | for { 298 | select { 299 | case sig := <-ch: // This is the latest change to the .Goroutines field. 300 | fmt.Println(sig.State.Data.(State).Goroutines) 301 | // Put a 1 second pause in. Remember, we won't receive 1000 increment 302 | // signals and 1000 decrement signals. We will always receive the 303 | // latest data, which may be far less than 2000. 304 | time.Sleep(1 * time.Second) 305 | case <-killPrinter: // We were told to die. 306 | return 307 | } 308 | } 309 | ``` 310 | Finally we loop and listen for one of two things to happen: 311 | 312 | * We get a **boutique.Signal** that **.Goroutines** has changed and 313 | print the value. 314 | * We have been signaled to die, so we kill the printer goroutine by returning. 315 | 316 | ### Let's create our main() 317 | 318 | ```go 319 | func main() { 320 | // Create our new Store with our default State{} object and our only 321 | // Modifier. We are not going to define Middleware, so we pass nil. 322 | store, err := boutique.New(State{}, boutique.NewModifiers(HandleIncrDecr), nil) 323 | if err != nil { 324 | panic(err) 325 | } 326 | ``` 327 | 328 | Now we start using what we've created. Here you can see we create the 329 | new boutique.Store instance. We pass it the initial state, **State{}** and 330 | we give it a collection of **Modifier(s)**. In our case we only have one 331 | **Modifier**, so we pass **HandleIncrDecr**. The final **nil** simply indicates 332 | that we are not passing any Middleware (we talk about it later). 333 | 334 | ```go 335 | // killPrinter lets us signal our printer goroutine that we no longer need 336 | // its services. 337 | killPrinter := make(chan struct{}) 338 | // printerKilled informs us that the printer goroutine has exited. 339 | printerKilled := make(chan struct{}) 340 | 341 | go printer() 342 | ``` 343 | Now we create a couple of channels to signal that we want **printer()** to die 344 | and another let us know **printer()** has died. We then kick off our 345 | **printer()**. 346 | 347 | ```go 348 | wg := sync.WaitGroup{} 349 | 350 | // Spin up a 1000 goroutines that sleep between 0 and 5 seconds. 351 | // Pause after generating every 100 for 0 - 8 seconds. 352 | for i := 0; i < 1000; i++ { 353 | if i%100 == 0 { 354 | time.Sleep(time.Duration(rand.Intn(8)) * time.Second) 355 | } 356 | store.Perform(IncrGoroutines(1)) 357 | wg.Add(1) 358 | go func() { 359 | defer wg.Done() 360 | defer store.Perform(DecrGoroutines()) 361 | time.Sleep(time.Duration(rand.Intn(5)) * time.Second) 362 | }() 363 | } 364 | 365 | wg.Wait() // Wait for the goroutines to finish. 366 | close(killPrinter) // kill the printer. 367 | <-printerKilled // wait for the printer to die. 368 | 369 | fmt.Printf("Final goroutine count: %d\n", store.State().Data.(State).Goroutines) 370 | fmt.Printf("Final Boutique.Store version: %d\n", store.Version()) 371 | ``` 372 | 373 | Now into the final stretch. We kick off 1000 goroutines, pausing for 8 seconds 374 | after spinning off every 100. Every time we spin off a goroutine, we 375 | increment our goroutine count with a **Perform()** call and increment a 376 | **sync.WaitGroup** so we know when all goroutines are done. 377 | 378 | We then wait for all goroutines to finish, kill the **printer()** and print out 379 | some values from the Store. 380 | 381 | 382 | ## Let's try a more complex example! 383 | 384 | ### A little about file layout 385 | 386 | Boutique provides storage that is best designed in a modular method: 387 | ``` 388 | └── state 389 | ├── state.go 390 | ├── actions 391 | │ └── actions.go 392 | ├── data 393 | | └── data.go 394 | ├── middleware 395 | | └── middleware.go 396 | └── modifiers 397 | └── modifiers.go 398 | ``` 399 | 400 | The files are best organized by using them as follows: 401 | 402 | * state.go - Holds the constructor for a boutique.Store for your application 403 | * actions.go - Holds the actions that will be used by the **Modifiers** to 404 | update the store 405 | * data.go - Holds the definition of your state object 406 | * middleware.go = Holds middleware for acting on proposed changes to your data. 407 | This is not required 408 | * modifiers.go - Holds all the Modifier(s) that are used by the boutique.Store 409 | to modify the store's data 410 | 411 | **Note**: These are all simply suggestions, you can combine this in a 412 | single file or name the files whatever you wish. 413 | 414 | ### First, define what data you want to store 415 | 416 | For this example we are going to use the example application ChatterBox included 417 | with Boutique. This provides an IRC like service using websockets. Users 418 | can access the chat server and subscribe to a channel where they can: 419 | 420 | * Send and receive messages on a comm channel to other users on the comm 421 | channel 422 | * View who is on a comm channel 423 | * Change comm channels 424 | 425 | We are going to include middleware that: 426 | 427 | * Prevents messages being sent over 500 characters. 428 | * Allows debug logging of the boutique.Store as it is updated. 429 | * Deletes older messages in the boutique.Store that are no longer needed. 430 | 431 | This example is not going to include all of the application's functions, just 432 | enough to cover the subjects we are discussing. For example, we don't want to 433 | get into how websockets work as that isn't important for understanding Boutique. 434 | 435 | Let's start by defining the data we need, which is going to be stored 436 | in state/data/data.go 437 | 438 | ```go 439 | // Package data holds the Store object that is used by our boutique instances. 440 | package data 441 | 442 | import ( 443 | "os" 444 | "time" 445 | ) 446 | 447 | // Message represents a message sent. 448 | type Message struct { 449 | // ID is the ID of the message in the order it was sent. 450 | ID int 451 | // Timestamp is the time in which the message was written. 452 | Timestamp time.Time 453 | // User is the user who sent the message. 454 | User string 455 | // Text is the text of the message. 456 | Text string 457 | } 458 | 459 | // State holds our state data for each communication channel that is open. 460 | type State struct { 461 | // ServerID is a UUID that uniquely represents this server instance. 462 | ServerID string 463 | // Channel is the channel this represents. 464 | Channel string 465 | // Users are the users in the Channel. 466 | Users []string 467 | // Messages waiting to be sent out to our users. 468 | Messages []Message 469 | ``` 470 | 471 | First there is the State object. Each comm channel that is opened for users to 472 | communicate on has its own State object. 473 | 474 | Inside here, we have different attributes related to the state of the channel. 475 | **ServerID** lets us identify the particular instance's log files, 476 | **Channel** holds the name of our channel. Users is the list of current users 477 | in the **Channel**, while **Messages** is the current buffer of user messages 478 | waiting to be sent out to the users. 479 | 480 | 481 | ### Create our actions 482 | 483 | Here I'm going to include a smaller version of the actions.go from our example 484 | application, to keep it simple. 485 | 486 | ```go 487 | // Package actions details boutique.Actions that are used by Modifiers to modify the store. 488 | package actions 489 | 490 | import ( 491 | "fmt" 492 | 493 | "github.com/johnsiilver/boutique" 494 | ) 495 | 496 | const ( 497 | // ActSendMessage indicates we want to send a message via the store. 498 | ActSendMessage boutique.ActionType = iota 499 | // ActAddUser indicates the Action wants to add a user to the store. 500 | ActAddUser 501 | ) 502 | 503 | // SendMessage sends a message via the store. 504 | func SendMessage(user string, s string) boutique.Action { 505 | m := data.Message{Timestamp: time.Now(), User: user, Text: s} 506 | return boutique.Action{Type: ActSendMessage, Update: m}, nil 507 | } 508 | 509 | // AddUser adds a user to the store, indicating a new user is in the comm channel. 510 | func AddUser(u string) boutique.Action { 511 | return boutique.Action{Type: ActAddUser, Update: u} 512 | } 513 | ``` 514 | 515 | Let's talk about the constants we defined: 516 | 517 | ```go 518 | const ( 519 | // ActSendMessage indicates we want to send a message via the store. 520 | ActSendMessage boutique.ActionType = iota 521 | // ActAddUser indicates the Action wants to add a user to the store. 522 | ActAddUser 523 | ) 524 | ``` 525 | These are our Action Types that will be used for signaling. By convention, 526 | these should be prefixed with "Act" to indicate its an Action type. 527 | 528 | We have defined two types here, one that indicates the Action is trying to 529 | send a message via the Store and one that is trying to add a user to the Store. 530 | 531 | Now we have our Action creators: 532 | 533 | ```go 534 | // SendMessage sends a message via the store. 535 | func SendMessage(id int, user string, s string) boutique.Action { 536 | m := data.Message{Timestamp: time.Now(), User: user, Text: s} 537 | return boutique.Action{Type: ActSendMessage, Update: m}, nil 538 | } 539 | 540 | // AddUser adds a user to the store, indicating a new user is in the room. 541 | func AddUser(u string) boutique.Action { 542 | return boutique.Action{Type: ActAddUser, Update: u} 543 | } 544 | ``` 545 | 546 | SendMessage takes in the user sending the message, and the text 547 | message itself. It creates a boutique.Action setting the Type to ActSendMessage 548 | and the Update to a data.Message, which we will use to update our store. 549 | 550 | ### Writing Modifiers 551 | 552 | So we need to touch on something we did not talk about in our first example. 553 | There is a fundamental rule that MUST be obeyed by all **Modifier(s)**: 554 | 555 | **THOU SHALL NOT MUTATE DATA!** 556 | 557 | Non-references can be changed directly. But reference types 558 | or pointer values must be copied and replaced, never modified. 559 | This allows downstream readers to ignore locking. 560 | 561 | So if you want to add a value to a slice, you must copy the slice, add the 562 | new value, then change the reference in the Store. You must never directly 563 | append. This is relatively fast on modern processors when data fits in the 564 | cache. 565 | 566 | The only exception to this is synchronization Types that can be copied, such 567 | as a channel or \*sync.WaitGroup. Do this sparingly! 568 | 569 | We provide a few utility functions such as **CopyAppendSlice()**, 570 | **ShallowCopy()**, and **DeepCopy()** to help ease this. 571 | 572 | Here are some **Modifier(s)** to handle our **Actions**. We could write one 573 | Modifier to handle all **Actions** or multiple **Modifier(s)** handling each 574 | individual **Actions**. I've chosen the latter, as I find it more readable. 575 | 576 | ```go 577 | // All is a boutique.Modifiers made up of all Modifier(s) in this file. 578 | var All = boutique.NewModifiers(SendMessage, AddUser) 579 | 580 | // SendMessage handles an Action of type ActSendMessage. 581 | func SendMessage(state interface{}, action boutique.Action) interface{} { 582 | s := state.(data.State) 583 | 584 | switch action.Type { 585 | case actions.ActSendMessage: 586 | msg := action.Update.(data.Message) 587 | msg.ID = s.NextMsgID 588 | s.Messages = boutique.CopyAppendSlice(s.Messages, msg).([]data.Message) 589 | s.NextMsgID = s.NextMsgID + 1 590 | } 591 | return s 592 | } 593 | 594 | // AddUser handles an Action of type ActAddUser. 595 | func AddUser(state interface{}, action boutique.Action) interface{} { 596 | s := state.(data.State) 597 | 598 | switch action.Type { 599 | case actions.ActAddUser: 600 | s.Users = boutique.CopyAppendSlice(s.Users, action.Update).([]string) 601 | } 602 | return s 603 | } 604 | ``` 605 | 606 | All **Modifier(s)** follow the boutique.Modifier signature. They receive a State 607 | and an Action. 608 | 609 | SendMessage receives immediately type asserts the state from an interface{} into 610 | our concrete state. This is always safe, because boutique.Store's are always 611 | initialized with a starting state object. 612 | 613 | ```go 614 | s := state.(data.State) 615 | ``` 616 | 617 | We then switch on the action.Type. We are only interested if the 618 | action.Type == actions.SendMessage. Otherwise, we do nothing and just return 619 | an unmodified state object. 620 | 621 | ```go 622 | switch action.Type { 623 | case actions.ActSendMessage: 624 | ... 625 | return s 626 | ``` 627 | 628 | If we received an action.Type == actions.SendMessage, we now need to type 629 | assert our action.Update so that we can retrieve the data we want to 630 | modify data.Messages with. 631 | 632 | ```go 633 | up := action.Update.(actions.Message) 634 | s.Messages = boutique.CopyAppendSlice(s.Messages, data.Message{ID: up.ID, Timestamp: time.Now(), User: up.User, Text: up.Text}).([]data.Message) 635 | ``` 636 | 637 | Now we need to update our Messages slice to contain our new Messages. 638 | Remember we cannot append to our existing Messages object, as that would be 639 | updating a reference. However, we can use a handy boutique.CopyAppendSlice() 640 | method to simply make a copy of our slice and update it with our new message. 641 | 642 | ```go 643 | boutique.CopyAppendSlice(s.Messages, data.Message{ID: up.ID, Timestamp: time.Now(), User: up.User, Text: up.Text}) 644 | ``` 645 | This handles the copy and append, but it returns an interface{}, so you must 646 | remember to type assert the result: 647 | 648 | ```go 649 | .([]data.Message) 650 | ``` 651 | 652 | AddUser works in the same way: 653 | 654 | ```go 655 | // AddUser handles an Action of type ActAddUser. 656 | func AddUser(state interface{}, action boutique.Action) interface{} { 657 | s := state.(data.State) 658 | 659 | switch action.Type { 660 | case actions.ActAddUser: 661 | s.Users = boutique.CopyAppendSlice(s.Users, action.Update).([]string) 662 | } 663 | return s 664 | } 665 | ``` 666 | 667 | Now lets talk about **Modifiers**. 668 | 669 | ```go 670 | var Modifiers = boutique.NewModifiers(SendMessage, AddUser) 671 | ``` 672 | 673 | Every update to boutique.Store is done through .Perform(). When .Perform() 674 | is called, it runs all of your **Modifier(s)** in the order you choose. These 675 | **Modifier(s)** are registered to boutique.Store via the New() call. 676 | A **Modifiers** is a collection of **Modifier(s)** in the order they will 677 | be applied. 678 | 679 | ### Creating your boutique.Store 680 | 681 | Generally speaking, it is a good idea to wrap your boutique.Store inside 682 | another object, though this isn't required. But either way, it is always a 683 | good idea to have a constructor to handle the initial setup. 684 | 685 | By convention, state.go is where you would want to do this. 686 | 687 | ```go 688 | // Package state contains our Hub, which is used to store data for a particular 689 | // channel users are communicating on. 690 | package state 691 | 692 | import ( 693 | "github.com/johnsiilver/boutique" 694 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/data" 695 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/middleware" 696 | "github.com/johnsiilver/boutique/example/chatterbox/server/state/modifiers" 697 | ) 698 | 699 | // Hub contains the central store and our middleware. 700 | type Hub struct { 701 | // Store is our boutique.Store. 702 | Store *boutique.Store 703 | } 704 | 705 | // New is the constructor for Hub. 706 | func New(channelName string, serverID string) (*Hub, error) { 707 | d := data.State{ 708 | ServerID: serverID, 709 | Channel: channelName, 710 | Users: []string{}, 711 | Messages: []data.Message{}, 712 | } 713 | 714 | s, err := boutique.New(d, modifiers.All, nil) 715 | if err != nil { 716 | return nil, err 717 | } 718 | 719 | return &Hub{Store: s}, nil 720 | } 721 | ``` 722 | 723 | Here I've create a constructor that sets up our boutique.Store. New() 724 | creates our initial data object, data.State, giving it a unique serverID 725 | (I like pborman's UUID library for generating unique IDs), the name of the 726 | channel we are storing state for, our initial Users and Messages. 727 | 728 | Then the store is initiated containing our starting data, our **Modifiers**, and 729 | no **Middleware** (we will come back to this). 730 | 731 | Alright, let's see how we can use this. 732 | 733 | ### Using the Store 734 | 735 | The example application has some complex logic, mostly around dealing with 736 | data coming in on a websocket and pushing the data out. We will skip around 737 | that and just talk about using the Stores. So you might not see a 1:1 738 | correlation with the code. 739 | 740 | Our application will receive websocket connections. The first thing we expect 741 | to happen is to receive a request to subscribe to a channel. If we do not, 742 | that connection is rejected. 743 | 744 | If their subscription request contains comm channel name that doesn't exist, 745 | we then create one: 746 | 747 | ```go 748 | // subscribe subscribes a user to the channel. 749 | func (c *ChatterBox) subscribe(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, m messages.Client) (*state.Hub, error) { 750 | 751 | c.chMu.Lock() 752 | defer c.chMu.Unlock() 753 | 754 | var ( 755 | hub *state.Hub 756 | err error 757 | ) 758 | 759 | // See if the channel exists, if so, get it. 760 | mchan, ok := c.channels[m.Channel] 761 | if ok { 762 | hub = mchan.hub 763 | if mchan.users[m.User] { // Don't allow two users with the same name to the same channel. 764 | // Send error on websocket and exit. 765 | ... 766 | return nil, err 767 | } 768 | } else { // Channel doesn't exist, create it. 769 | hub, err = state.New(m.Channel) 770 | if err != nil { 771 | return nil, err 772 | } 773 | mchan = &channel{ctx: ctx, cancel: cancel, hub: hub, users: map[string]bool{m.User: true}} 774 | c.channels[m.Channel] = mchan 775 | } 776 | 777 | // Add the user to the channel. 778 | mchan.users[m.User] = true 779 | if err = hub.Store.Perform(actions.AddUser(m.User)); err != nil { 780 | return nil, err 781 | } 782 | 783 | // Send a websocket acknowledgement. 784 | ... 785 | 786 | glog.Infof("client %v: subscribed to %s as %s", conn.RemoteAddr(), m.User, m.Channel) 787 | return hub, nil 788 | ``` 789 | At this point, nothing special has happened. You've spent a lot more time 790 | updating a struct, which is not all that useful. 791 | 792 | But here is some payoff. Every time someone subscribes to the channel, we can 793 | now update all clients to the new user lists. We also now have the ability to 794 | subscribe all listeners to message updates. Every time a user on this channel 795 | submits a message, So for the person who just created the comm channel, lets 796 | send him updates whenever anyone sends on the channel or joins/leaves the 797 | channel. 798 | 799 | #### Updating a client when new messages arrive 800 | 801 | ```go 802 | // clientSender receives changes to the store's Messages/Users fields and pushes 803 | // them out to our websocket clients. 804 | func (c *ChatterBox) clientSender(ctx context.Context, usr string, chName string, conn *websocket.Conn, store *boutique.Store) { 805 | const ( 806 | msgField = "Messages" 807 | usersField = "Users" 808 | ) 809 | 810 | state := store.State() 811 | startData := state.Data.(data.State) 812 | 813 | // We only want to send messages we haven't seen before. 814 | // Cleanup is done in Middleware. 815 | var lastMsgID = -1 816 | if len(startData.Messages) > 0 { 817 | lastMsgID = startData.Messages[len(startData.Messages)-1].ID 818 | } 819 | 820 | // Subscribe to our store's "Messages" field. 821 | msgCh, msgCancel, err := store.Subscribe(msgField) 822 | if err != nil { 823 | c.sendError(conn, err) 824 | return 825 | } 826 | defer msgCancel() // cancel the subscription when this function ends. 827 | 828 | // Subscribe to our store's "Users" field. 829 | usersCh, usersCancel, err := store.Subscribe(usersField) 830 | if err != nil { 831 | c.sendError(conn, err) 832 | return 833 | } 834 | defer usersCancel() 835 | 836 | // Loop looking for messages to come in, users to subscribe/leave or 837 | // the context object to be cancelled. 838 | for { 839 | select { 840 | // Our .Messages changed. 841 | case msgSig := <-msgCh: 842 | msgs := msgSig.State.Data.(data.State).Messages 843 | if len(msgs) == 0 { // Don't send blank messages. 844 | continue 845 | } 846 | 847 | // Send all messages we haven't seen. 848 | var toSend []data.Message 849 | toSend, lastMsgID = c.latestMsgs(msgs, lastMsgID) // helper method. 850 | if len(toSend) > 0 { 851 | // This simply sends on the websocket. 852 | if err := c.sendMessages(conn, toSend); err != nil { 853 | glog.Errorf("error sending message to client on channel %s: %s", chName, err) 854 | return 855 | } 856 | } 857 | // Our .Users changed. 858 | case userSig := <-usersCh: 859 | // Send the message out to this client on a websocket. 860 | if err := c.write(conn, messages.Server{Type: messages.SMUserUpdate, Users: userSig.State.Data.(data.State).Users}); err != nil { 861 | c.sendError(conn, err) 862 | return 863 | } 864 | case <-ctx.Done(): 865 | return 866 | } 867 | } 868 | } 869 | ``` 870 | The first thing we need to do is get the existing Store's data and calculate 871 | the last Message. We only want to send messages after this: 872 | 873 | ```go 874 | const ( 875 | msgField = "Messages" 876 | usersField = "Users" 877 | ) 878 | 879 | state := store.State() 880 | startData := state.Data.(data.State) 881 | 882 | // We only want to send messages we haven't seen before. 883 | // Cleanup is done in Middleware. 884 | var lastMsgID = -1 885 | if len(startData.Messages) > 0 { 886 | lastMsgID = startData.Messages[len(startData.Messages)-1].ID 887 | } 888 | ``` 889 | 890 | Now that we've done that, let's subscribe to the fields we care about: 891 | ```go 892 | // Subscribe to our store's "Messages" field. 893 | msgCh, msgCancel, err := store.Subscribe(msgField) 894 | if err != nil { 895 | c.sendError(conn, err) 896 | return 897 | } 898 | defer msgCancel() // cancel the subscription when this function ends. 899 | 900 | // Subscribe to our store's "Users" field. 901 | usersCh, usersCancel, err := store.Subscribe(usersField) 902 | if err != nil { 903 | c.sendError(conn, err) 904 | return 905 | } 906 | defer usersCancel() 907 | ``` 908 | 909 | Finally, the **for** loop, which reads off our various channels until 910 | our context is killed. Each time we receive from the subscription channels, 911 | we update our client. 912 | ```go 913 | for { 914 | select { 915 | // Our .Messages changed. 916 | case msgSig := <-msgCh: 917 | msgs := msgSig.State.Data.(data.State).Messages 918 | if len(msgs) == 0 { // Don't send blank messages. 919 | continue 920 | } 921 | 922 | // Send all messages we haven't seen. 923 | var toSend []data.Message 924 | toSend, lastMsgID = c.latestMsgs(msgs, lastMsgID) // helper method. 925 | if len(toSend) > 0 { 926 | // This simply sends on the websocket. 927 | if err := c.sendMessages(conn, toSend); err != nil { 928 | glog.Errorf("error sending message to client on channel %s: %s", chName, err) 929 | return 930 | } 931 | } 932 | // Our .Users changed. 933 | case userSig := <-usersCh: 934 | // Send the message out to this client on a websocket. 935 | if err := c.write(conn, messages.Server{Type: messages.SMUserUpdate, Users: userSig.State.Data.(data.State).Users}); err != nil { 936 | c.sendError(conn, err) 937 | return 938 | } 939 | case <-ctx.Done(): 940 | return 941 | } 942 | } 943 | ``` 944 | 945 | #### Update the Store.Messages when a client sends a new Message 946 | 947 | ```go 948 | // clientReceiver is used to process messages that are received over the websocket from the client. 949 | // This is meant to be run in a goroutine as it blocks for the life of the conn and decrements 950 | // wg when it finally ends. 951 | func (c *ChatterBox) clientReceiver(ctx context.Context, wg *sync.WaitGroup, conn *websocket.Conn) { 952 | defer wg.Done() // Let the caller know we are done. 953 | defer c.unsubscribe(user, comm) // When we no longer can talk to the client, unsubscribe. 954 | 955 | var ( 956 | cancel context.CancelFunc 957 | hub *state.Hub 958 | user string 959 | comm string 960 | ) 961 | 962 | for { 963 | m, err := c.read(conn) // Reads from the websocket. 964 | if err != nil { 965 | glog.Errorf("client %s terminated its connection", conn.RemoteAddr()) 966 | if cancel != nil { 967 | cancel() 968 | } 969 | return 970 | } 971 | 972 | err = m.Validate() // Validate the message. 973 | if err != nil { 974 | glog.Errorf("error: client %v message did not validate: %v: %#+v: ignoring...", conn.RemoteAddr(), err, m) 975 | if err = c.sendError(conn, err); err != nil { 976 | return 977 | } 978 | continue 979 | } 980 | 981 | switch t := m.Type; t { 982 | case messages.CMSendText: // User wants to send a message. 983 | if hub == nil { 984 | if err := c.sendError(conn, fmt.Errorf("cannot send a message, not subscribed to channel")); err != nil { 985 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 986 | return 987 | } 988 | } 989 | 990 | if err := hub.Store.Perform(actions.SendMessage(user, m.Text.Text)); err != nil { 991 | c.sendError(conn, fmt.Errorf("problem calling store.Perform(): %s", err)) 992 | continue 993 | } 994 | case messages.CMSubscribe: // User wants to subscribe to a channel. 995 | ctx, cancel = context.WithCancel(context.Background()) 996 | defer cancel() 997 | 998 | // If we already are subscribed, unsubscribe. 999 | if hub != nil { 1000 | c.unsubscribe(user, comm) 1001 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 1002 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1003 | return 1004 | } 1005 | if cancel != nil { 1006 | cancel() 1007 | } 1008 | } 1009 | 1010 | // Now subscribe to the new channel. 1011 | var err error 1012 | hub, err = c.subscribe(ctx, cancel, conn, m) 1013 | if err != nil { 1014 | if err = c.sendError(conn, err); err != nil { 1015 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1016 | } 1017 | return 1018 | } 1019 | user = m.User 1020 | comm = m.Channel 1021 | 1022 | go c.clientSender(ctx, user, comm, conn, hub.Store) // Start sending messages to the client. 1023 | case messages.CMDrop: // User wants to drop from a channel. 1024 | if hub == nil { 1025 | if err := c.sendError(conn, fmt.Errorf("error: cannot drop a channel, your not subscribed to any")); err != nil { 1026 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1027 | return 1028 | } 1029 | } 1030 | if cancel != nil { 1031 | cancel() 1032 | } 1033 | c.unsubscribe(user, comm) 1034 | 1035 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 1036 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1037 | return 1038 | } 1039 | hub = nil 1040 | user = "" 1041 | comm = "" 1042 | default: // We don't understand the message. 1043 | glog.Errorf("error: client %v had unknown message %v, ignoring", conn.RemoteAddr(), t) 1044 | if err := c.sendError(conn, fmt.Errorf("received message type from client %v that the server doesn't understand", t)); err != nil { 1045 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1046 | return 1047 | } 1048 | } 1049 | } 1050 | } 1051 | ``` 1052 | So we will take this one section at a time: 1053 | ```go 1054 | defer wg.Done() // Let the caller know we are done. 1055 | defer c.unsubscribe(user, comm) // When we no longer can talk to the client, unsubscribe. 1056 | ``` 1057 | Here, we let our caller know that we are done and unsubscribe from the channel 1058 | when this function ends. This function ends only when we cannot read/write on 1059 | the websocket. 1060 | 1061 | ```go 1062 | var ( 1063 | cancel context.CancelFunc 1064 | hub *state.Hub 1065 | user string 1066 | comm string 1067 | ) 1068 | ``` 1069 | Here we are keeping track of variables that only exist if we are subscribed to 1070 | a channel. The **cancel()** is for when we want to kill the **clientSender()**. 1071 | **Hub** contains the boutique.Store for this channel's data. **User** is the 1072 | current user the client is connected as. Finally, **comm** is the name of the 1073 | channel we are in. 1074 | 1075 | ```go 1076 | for { 1077 | m, err := c.read(conn) // Reads from the websocket. 1078 | if err != nil { 1079 | glog.Errorf("client %s terminated its connection", conn.RemoteAddr()) 1080 | if cancel != nil { 1081 | cancel() 1082 | } 1083 | return 1084 | } 1085 | 1086 | err = m.Validate() // Validate the message. 1087 | if err != nil { 1088 | glog.Errorf("error: client %v message did not validate: %v: %#+v: ignoring...", conn.RemoteAddr(), err, m) 1089 | if err = c.sendError(conn, err); err != nil { 1090 | return 1091 | } 1092 | continue 1093 | } 1094 | ``` 1095 | Now we enter our loop. We read off the websocket for a messsage. We then 1096 | **Validate()** that message. 1097 | 1098 | ```go 1099 | for { 1100 | ... 1101 | switch t := m.Type; t { 1102 | case messages.CMSendText: // User wants to send a message. 1103 | if hub == nil { 1104 | if err := c.sendError(conn, fmt.Errorf("cannot send a message, not subscribed to channel")); err != nil { 1105 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1106 | return 1107 | } 1108 | } 1109 | 1110 | if err := hub.Store.Perform(actions.SendMessage(user, m.Text.Text)); err != nil { 1111 | c.sendError(conn, fmt.Errorf("problem calling store.Perform(): %s", err)) 1112 | continue 1113 | } 1114 | } 1115 | ``` 1116 | Next we switch on the message type. If the message is for sending text on 1117 | the channel, we check if we are in a channel with **if hub == nil**. If we are, 1118 | we simply call **.Perform(actions.SendMessage(...))**, which will cause all 1119 | other clients to be updated via their subscriptions in **clientSender()**. 1120 | 1121 | ```go 1122 | ... 1123 | switch t := m.Type; t { 1124 | ... 1125 | case messages.CMSubscribe: // User wants to subscribe to a channel. 1126 | ctx, cancel = context.WithCancel(context.Background()) 1127 | defer cancel() 1128 | 1129 | // If we already are subscribed, unsubscribe. 1130 | if hub != nil { 1131 | c.unsubscribe(user, comm) 1132 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 1133 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1134 | return 1135 | } 1136 | if cancel != nil { 1137 | cancel() 1138 | } 1139 | } 1140 | 1141 | // Now subscribe to the new channel. 1142 | var err error 1143 | hub, err = c.subscribe(ctx, cancel, conn, m) 1144 | if err != nil { 1145 | if err = c.sendError(conn, err); err != nil { 1146 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1147 | } 1148 | return 1149 | } 1150 | user = m.User 1151 | comm = m.Channel 1152 | 1153 | go c.clientSender(ctx, user, comm, conn, hub.Store) // Start sending messages to the client. 1154 | ``` 1155 | Now our next type of message is when the client wants to subscribe to a channel. 1156 | We use a **context** with a **cancelFunc** to let us kill the **clientSender()** 1157 | we create. 1158 | 1159 | If we are already subscribed to a channel, **unsubscribe()**. 1160 | 1161 | Then we **subscribe()** to the channel, and finally call **clientSender()**. 1162 | 1163 | ```go 1164 | ... 1165 | switch t := m.Type; t { 1166 | ... 1167 | case messages.CMDrop: // User wants to drop from a channel. 1168 | if hub == nil { 1169 | if err := c.sendError(conn, fmt.Errorf("error: cannot drop a channel, your not subscribed to any")); err != nil { 1170 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1171 | return 1172 | } 1173 | } 1174 | if cancel != nil { 1175 | cancel() 1176 | } 1177 | c.unsubscribe(user, comm) 1178 | 1179 | if err := c.write(conn, messages.Server{Type: messages.SMChannelDrop}); err != nil { 1180 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1181 | return 1182 | } 1183 | hub = nil 1184 | user = "" 1185 | comm = "" 1186 | ``` 1187 | This message type signals the user wants to drop from a channel. 1188 | We simply reset our hub/user/comm variables and let the remote side know it 1189 | was successful. 1190 | 1191 | ```go 1192 | ... 1193 | switch t := m.Type; t { 1194 | ... 1195 | default: // We don't understand the message. 1196 | glog.Errorf("error: client %v had unknown message %v, ignoring", conn.RemoteAddr(), t) 1197 | if err := c.sendError(conn, fmt.Errorf("received message type from client %v that the server doesn't understand", t)); err != nil { 1198 | glog.Errorf("error: client %v: %s", conn.RemoteAddr(), err) 1199 | return 1200 | } 1201 | } 1202 | } // End function 1203 | ``` 1204 | Finally, we have our default statement, which is where we inform the client we 1205 | don't know what it is asking us to do. 1206 | 1207 | #### Wrapping up the non-Middleware 1208 | 1209 | There are of course a bunch of helper functions we didn't discuss, but those 1210 | aren't relevant to the discussion. 1211 | 1212 | At this point, we have made a program that: 1213 | * Creates a boutique.Store for each comm channel that exists. 1214 | * Clients in a comm channel are updated of changes via various subscriptions 1215 | that trigger updates to be sent on their connected websockets. 1216 | 1217 | There are other ways to do this of course. You could create arrays of channels 1218 | that you lock/unlock as you add/remove users. You could send messages down 1219 | those channels that are then processed and sent via the websocket. 1220 | 1221 | And in some cases, that might be simpler. But as your program expands, this 1222 | paradigm gets more complicated. 1223 | 1224 | Maybe you want to: 1225 | * Prevent messages longer than 500 characters. 1226 | * Log all messages to persistent storage. 1227 | * Allow debugging of all changes with users/messages. 1228 | * Add authentication checks on message sends. 1229 | 1230 | Don't get me wrong, you can absolutely do this without Boutique. You can do 1231 | this more efficiently. But it takes a lot more thought and planning to get 1232 | right (and probably a lot of refactoring). Boutique adds structure that can 1233 | make this simplistic. 1234 | 1235 | Those "Maybe you want to's" above are easily accomplished by adding Middleware. 1236 | 1237 | ### Middleware 1238 | 1239 | #### Introduction 1240 | **Middleware** allows you to extend the Store by inserting data handlers into 1241 | the Perform() calls either before the commit to the Store or after the data 1242 | has been committed. 1243 | 1244 | **Middleware** can: 1245 | 1246 | * Change store data as an update passed through 1247 | * Deny an update 1248 | * Signal or spin off other async calls 1249 | * See the end result of the change 1250 | 1251 | A few example middleware applications: 1252 | 1253 | * Log all State changes for debug purposes 1254 | * Write certain data changes to long term storage 1255 | * Authorize/Deny changes 1256 | * Update certain fields in conjunction with the update type 1257 | * Provide cleanup mechanisms for certain fields 1258 | * ... 1259 | 1260 | #### Defining Middleware 1261 | **Middleware** is simply a function/method that implements the 1262 | following signature: 1263 | 1264 | ```go 1265 | type Middleware func(args *MWArgs) (changedData interface{}, stop bool, err error) 1266 | ``` 1267 | 1268 | Let's talk first about the args that are provided: 1269 | 1270 | ```go 1271 | type MWArgs struct { 1272 | // Action is the Action that is being performed. 1273 | Action Action 1274 | // NewData is the proposed new State.Data field in the Store. This can be modified by the 1275 | // Middleware and returned as the changedData return value. 1276 | NewData interface{} 1277 | // GetState if a function that will return the current State of the Store. 1278 | GetState GetState 1279 | // Committed is only used if the Middleware will spin off a goroutine. In that case, 1280 | // the committed state will be sent via this channel. This allows Middleware that wants 1281 | // to do something based on the final state (like logging) to work. If the data was not 1282 | // committed due to another Middleware cancelling the commit, State.IsZero() will be true. 1283 | Committed chan State 1284 | 1285 | // WG must have .Done() called by all Middleware once it has finished. If using Committed, you must 1286 | // not call WG.Done() until your goroutine is completed. 1287 | WG *sync.WaitGroup 1288 | } 1289 | ``` 1290 | 1291 | So first we have **Action**. By observing the **Action.Type**, you can see 1292 | what the **Action** was that **Perform()** was called with. 1293 | Altering this has no effect. 1294 | 1295 | **NewData** is the **State.Data** that will result from the Action. It has not 1296 | been committed. Altering this by itself will have no effect, but I will show 1297 | how to alter it in a moment and affect a change. 1298 | 1299 | **GetState** is a function that you can call to get the current **State** 1300 | object. 1301 | 1302 | Let's skip **Committed** for the moment, we'll get back to it later. 1303 | 1304 | **WG** is very important. Your **Middleware** must call **WG.Done()** before 1305 | exiting or your **Perform()** call will block forever. There is a handy log 1306 | message that catches these if your forget during development. 1307 | 1308 | Now that we have args out of the way, let's talk about the return values: 1309 | 1310 | ```go 1311 | (changedData interface{}, stop bool, err error) 1312 | ``` 1313 | 1314 | **changedData** represents the **State.Data** with your changes, if any. 1315 | If you are not going to edit the data, then you can simply return **nil** here. 1316 | Otherwise you may modify **args.NewData** and then return it here to affect 1317 | your change. 1318 | 1319 | **stop** is an indicator that you want to prevent other **Middleware** from 1320 | executing and immediately commit the change. 1321 | 1322 | **err** indicates you wish to prevent the change and send an error to the 1323 | **Perform()** caller. 1324 | 1325 | #### A synchronous Middleware 1326 | 1327 | So let's design a synchronous **Middleware** that cleans up older Messages in 1328 | our example application. This will be synchronous because we do this before our 1329 | Perform is completed. 1330 | 1331 | ```go 1332 | // CleanMessages deletes data.State.Messages older than 1 Minute. 1333 | func CleanMessages(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 1334 | // Remember to do this, otherwise the Middleware will block a Perform() call. 1335 | defer args.WG.Done() 1336 | 1337 | d := args.NewData.(data.State) // Assert the data to the correct type. 1338 | 1339 | var ( 1340 | i int 1341 | m data.Message 1342 | deleteAll = true 1343 | now = time.Now() 1344 | ) 1345 | 1346 | // Find the first message that is within our expiring time. 1347 | // Every message after that is still good. 1348 | for i, m = range d.Messages { 1349 | if now.Sub(m.Timestamp) < CleanTimer { 1350 | deleteAll = false 1351 | break 1352 | } 1353 | } 1354 | 1355 | switch { 1356 | // Nothing should be changed, so return nil for the changedData. 1357 | case i == 0: 1358 | return nil, false, nil 1359 | // Looks like all the Messages are expired, so kill them. 1360 | case deleteAll: 1361 | d.Messages = []data.Message{} 1362 | // Copy the non-expired Messages into a new slice and then assign it to our 1363 | // new data.State object. 1364 | case len(d.Messages[i:]) > 0: 1365 | newMsg := make([]data.Message, len(d.Messages[i:])) 1366 | copy(newMsg, d.Messages[i:]) 1367 | d.Messages = newMsg 1368 | } 1369 | // Return our altered data.State object. 1370 | return d, false, nil 1371 | } 1372 | ``` 1373 | 1374 | First thing: defer our **args.WG.Done()** call! Not doing this will cause 1375 | problems. 1376 | 1377 | Next we need to go through our **[]data.Message** until we locate the first 1378 | index that is within our time limit. Anything from there till the end of our 1379 | slice does not need to be deleted. 1380 | 1381 | ```go 1382 | 1383 | for i, m = range d.Messages { 1384 | if now.Sub(m.Timestamp) < CleanTimer { 1385 | deleteAll = false 1386 | break 1387 | } 1388 | } 1389 | ``` 1390 | **deleteAll** simply lets us know if we find any **Message** not expired. 1391 | If we don't, we delete all messages. 1392 | 1393 | Finally our switch statement handles all our cases. Most are self explanatory, 1394 | but there is one that we should look at: 1395 | 1396 | ```go 1397 | case len(d.Messages[i:]) > 0: 1398 | newMsg := make([]data.Message, len(d.Messages[i:])) 1399 | copy(newMsg, d.Messages[i:]) 1400 | d.Messages = newMsg 1401 | ``` 1402 | 1403 | Here we copy the data from the slice into a new slice, though that isn't 1404 | strictly necessary. **Middleware** is run after **Modifiers**, so all this data 1405 | is a copy already. However, not doing so will make the slice smaller, but the 1406 | underlying array will continue to grow. Your len() may be 0, but your 1407 | capacity might be 50,000. Not what you want in a cleanup **Middleware**! 1408 | 1409 | #### Asynchronous Middleware 1410 | 1411 | Asynchronous **Middleware** is useful when you want to trigger something to 1412 | happen or view the final committed data. However, it comes with the limitation 1413 | that you cannot alter the data. 1414 | 1415 | Use asynchronous Middleware when: 1416 | * Triggering other code and your not required to modify data here 1417 | * You want to trigger something to happen after the commit to the store occurs 1418 | 1419 | The key here is that no matter what, Asynchronous **Middleware** cannot alter 1420 | the data. 1421 | 1422 | Let's create some **Middleware** that can be turned on or off at anytime and 1423 | lets us record a diff of our Store on each commit. We can use this to debug 1424 | our application by observing changes to the Store's data. 1425 | 1426 | ```go 1427 | var pConfig = &pretty.Config{ 1428 | Diffable: true, 1429 | 1430 | // Field and value options 1431 | IncludeUnexported: false, 1432 | PrintStringers: true, 1433 | PrintTextMarshalers: true, 1434 | } 1435 | 1436 | type Logging struct { 1437 | lastData boutique.State 1438 | file *os.File 1439 | } 1440 | 1441 | func NewLogging(fName string) (*Logging, error) { 1442 | f, err := os.OpenFile(fName, os.O_WRONLY+ os.O_CREATEflag, 0664) 1443 | if err != nil { 1444 | return nil, err 1445 | } 1446 | return &Logging{file: f}, nil 1447 | } 1448 | 1449 | func (l *Logging) DebugLog(args *boutique.MWArgs) (changedData interface{}, stop bool, err error) { 1450 | go func() { // Set off our Asynchronous method. 1451 | defer args.WG.Done() // Signal when we are done. Not doing this will cause the program to stall. 1452 | 1453 | state := <-args.Committed // Wait for our data to get committed. 1454 | 1455 | if state.IsZero() { // This indicates that another middleware killed the commit. No need to log. 1456 | return 1457 | } 1458 | 1459 | d := state.Data.(data.State) // Typical type assertion. 1460 | 1461 | _, err := l.file.WriteString(fmt.Sprintf("%s\n\n", pConfig.Compare(l.lastData, state))) 1462 | if err != nil { 1463 | glog.Errorf("problem writing to debug file: %s", err) 1464 | return 1465 | } 1466 | l.lastData = state 1467 | }() 1468 | 1469 | return nil, false, nil // Don't change any data and let other Middleware execute. 1470 | } 1471 | ``` 1472 | 1473 | So let's break this down, starting with **pConfig**. 1474 | 1475 | I need something to diff the Store, and in this case I've decided to use the 1476 | pretty library by Kyle Lemons. I love this library for diffs and it gives 1477 | a lot of control on how things are diffed. You can find it here: 1478 | 1479 | http://github.com/kylelemons/godebug/pretty 1480 | 1481 | Next we need to setup our **Logger**. If the user starts the server with debug 1482 | logging turned on, we include this in our **Middleware**. If not we don't. 1483 | 1484 | ```go 1485 | type Logging struct { 1486 | lastData boutique.State 1487 | file *os.File 1488 | } 1489 | ``` 1490 | 1491 | Here we are storing the last state we saw the boutique Store in and the file 1492 | that we are going to write our logs to. 1493 | 1494 | Let's skip on down to the nitty gritty, shall we? 1495 | 1496 | ```go 1497 | defer args.WG.Done() 1498 | go func() { // Set off our Asynchronous method. 1499 | state := <-args.Committed // Wait for our data to get committed. 1500 | 1501 | if state.IsZero() { // This indicates that another middleware killed the commit. No need to log. 1502 | return 1503 | } 1504 | 1505 | d := state.Data.(data.State) // Typical type assertion. 1506 | 1507 | _, err := l.file.WriteString(fmt.Sprintf("%s\n\n", pConfig.Compare(l.lastData, state))) 1508 | if err != nil { 1509 | glog.Errorf("problem writing to debug file: %s", err) 1510 | return 1511 | } 1512 | l.lastData = state 1513 | }() 1514 | ``` 1515 | 1516 | First thing we do is kick off our **Middleware** into async mode with a 1517 | goroutine. If we didn't need to wait for the data to be committed, we would 1518 | simply do the: 1519 | ```go 1520 | args.WG.Done() 1521 | ``` 1522 | immediately before the goroutine. But we need to keep ordering intact for 1523 | proper logging, so we don't want multiple **Process()** calls to occur. 1524 | 1525 | Next, we finally use that **args.Committed** channel. 1526 | ```go 1527 | state := <-args.Committed 1528 | ``` 1529 | This channel will return the committed state right after it is committed to the 1530 | store. Now we have the data we need to write a diff to a file. 1531 | 1532 | Finally we simply write out the diff of the Store to disk and update our 1533 | **.lastData** attribute. 1534 | ```go 1535 | _, err := l.file.WriteString(fmt.Sprintf("%s\n\n", pConfig.Compare(l.lastData, state))) 1536 | if err != nil { 1537 | glog.Errorf("problem writing to debug file: %s", err) 1538 | return 1539 | } 1540 | l.lastData = state 1541 | ``` 1542 | 1543 | ## Final thoughts 1544 | 1545 | I hope this has given a decent explanation on what you can do with Boutique. 1546 | In the future, I hope to restructure this into sections with some video 1547 | introductions and guide to testing. 1548 | 1549 | Hopefully you will find this useful. Happy coding! 1550 | 1551 | ## Previous works 1552 | 1553 | Boutique has its origins from the Redux library: [http://redux.js.org](http://redux.js.org) 1554 | 1555 | Redux is very useful for Javascript clients that need to store state. Its ideas 1556 | were the basis of this model. 1557 | --------------------------------------------------------------------------------