diff --git a/app/cli/liars/board/keyboard.go b/app/cli/liars/board/keyboard.go index b68fc75c..79aea12a 100644 --- a/app/cli/liars/board/keyboard.go +++ b/app/cli/liars/board/keyboard.go @@ -113,6 +113,8 @@ func (b *Board) newGame() error { b.lastStatus = status + b.drawInit(true) + return nil } diff --git a/app/cli/liars/engine/engine.go b/app/cli/liars/engine/engine.go index 9d202e19..498938dd 100644 --- a/app/cli/liars/engine/engine.go +++ b/app/cli/liars/engine/engine.go @@ -94,7 +94,10 @@ func (e *Engine) Events(f func(event string, address common.Address)) (func(), e url := strings.Replace(e.url, "http", "ws", 1) url = fmt.Sprintf("%s/v1/game/events", url) - socket, _, err := websocket.DefaultDialer.Dial(url, nil) + req := make(http.Header) + req.Add("authorization", fmt.Sprintf("Bearer %s", e.token)) + + socket, _, err := websocket.DefaultDialer.Dial(url, req) if err != nil { return nil, fmt.Errorf("dial: %w", err) } diff --git a/app/services/engine/main.go b/app/services/engine/main.go index 4ddb43d6..d72f1a77 100644 --- a/app/services/engine/main.go +++ b/app/services/engine/main.go @@ -22,7 +22,6 @@ import ( "github.com/ardanlabs/liarsdice/business/web/v1/auth" "github.com/ardanlabs/liarsdice/business/web/v1/debug" "github.com/ardanlabs/liarsdice/business/web/v1/mux" - "github.com/ardanlabs/liarsdice/foundation/events" "github.com/ardanlabs/liarsdice/foundation/keystore" "github.com/ardanlabs/liarsdice/foundation/logger" "github.com/ardanlabs/liarsdice/foundation/web" @@ -190,8 +189,6 @@ func run(ctx context.Context, log *logger.Logger) error { oneETHToUSD, oneUSDToETH := converter.Values() log.Info(ctx, "currency values", "oneETHToUSD", oneETHToUSD, "oneUSDToETH", oneUSDToETH) - evts := events.New() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -242,7 +239,6 @@ func run(ctx context.Context, log *logger.Logger) error { Auth: authClient, Converter: converter, Bank: bankClient, - Evts: evts, AnteUSD: cfg.Game.AnteUSD, ActiveKID: cfg.Auth.ActiveKID, BankTimeout: cfg.Bank.Timeout, @@ -276,10 +272,6 @@ func run(ctx context.Context, log *logger.Logger) error { log.Info(ctx, "shutdown", "status", "shutdown started", "signal", sig) defer log.Info(ctx, "shutdown", "status", "shutdown complete", "signal", sig) - // Release any web sockets that are currently active. - log.Info(ctx, "shutdown", "status", "shutdown web socket channels") - evts.Shutdown() - ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout) defer cancel() diff --git a/app/services/engine/v1/build/all/all.go b/app/services/engine/v1/build/all/all.go index 1ee3adba..660893cf 100644 --- a/app/services/engine/v1/build/all/all.go +++ b/app/services/engine/v1/build/all/all.go @@ -28,7 +28,6 @@ func (add) Add(app *web.App, cfg mux.Config) { Auth: cfg.Auth, Converter: cfg.Converter, Bank: cfg.Bank, - Evts: cfg.Evts, AnteUSD: cfg.AnteUSD, ActiveKID: cfg.ActiveKID, BankTimeout: cfg.BankTimeout, diff --git a/app/services/engine/v1/handlers/gamegrp/events.go b/app/services/engine/v1/handlers/gamegrp/events.go new file mode 100644 index 00000000..993ecc4f --- /dev/null +++ b/app/services/engine/v1/handlers/gamegrp/events.go @@ -0,0 +1,136 @@ +package gamegrp + +import ( + "fmt" + "sync" +) + +// These types exist for documentation purposes. The API will +// will accept a string. +type ( + gameID string + playerID string +) + +// evts maintains the set player channels for sending messages over +// the players web socket. +var evts = newEvents() + +// events maintains a mapping of unique id and channels so goroutines +// can register and receive events. +type events struct { + players map[playerID]chan string + games map[gameID]map[playerID]struct{} + mu sync.RWMutex +} + +func newEvents() *events { + return &events{ + players: make(map[playerID]chan string), + games: make(map[gameID]map[playerID]struct{}), + } +} + +func (evt *events) acquire(pID string) chan string { + evt.mu.Lock() + defer evt.mu.Unlock() + + // Since a message will be dropped if the websocket receiver is + // not ready to receive, this arbitrary buffer should give the receiver + // enough time to not lose a message. Websocket send could take long. + const messageBuffer = 100 + + playID := playerID(pID) + + ch, exists := evt.players[playID] + if !exists { + ch = make(chan string, messageBuffer) + evt.players[playID] = ch + } + + return ch +} + +func (evt *events) release(pID string) error { + evt.mu.Lock() + defer evt.mu.Unlock() + + playID := playerID(pID) + + ch, exists := evt.players[playID] + if !exists { + return fmt.Errorf("player id %q does not exist", pID) + } + + delete(evt.players, playID) + close(ch) + + return nil +} + +func (evt *events) addPlayerToGame(gID string, pID string) error { + evt.mu.Lock() + defer evt.mu.Unlock() + + gameID := gameID(gID) + playID := playerID(pID) + + if _, exists := evt.players[playID]; !exists { + return fmt.Errorf("player id %q does not exist", pID) + } + + playerMap, exists := evt.games[gameID] + if !exists { + playerMap = make(map[playerID]struct{}) + evt.games[gameID] = playerMap + } + + playerMap[playID] = struct{}{} + + return nil +} + +func (evt *events) removePlayersFromGame(gID string) error { + evt.mu.Lock() + defer evt.mu.Unlock() + + gameID := gameID(gID) + + playerMap, exists := evt.games[gameID] + if !exists { + return nil + } + + for playID := range playerMap { + delete(playerMap, playID) + } + delete(evt.games, gameID) + + return nil +} + +// send signals a message to every registered channel for the specified +// game. Send will not block waiting for a receiver on any given channel. +func (evt *events) send(gID string, s string) { + evt.mu.RLock() + defer evt.mu.RUnlock() + + gameID := gameID(gID) + + playerMap, exists := evt.games[gameID] + if !exists { + return + } + + for playID := range playerMap { + ch, exists := evt.players[playID] + if !exists { + continue + } + + select { + case ch <- s: + default: + } + } +} diff --git a/app/services/engine/v1/handlers/gamegrp/gamegrp.go b/app/services/engine/v1/handlers/gamegrp/gamegrp.go index 6ef8d88e..ab485d48 100644 --- a/app/services/engine/v1/handlers/gamegrp/gamegrp.go +++ b/app/services/engine/v1/handlers/gamegrp/gamegrp.go @@ -19,7 +19,6 @@ import ( v1 "github.com/ardanlabs/liarsdice/business/web/v1" "github.com/ardanlabs/liarsdice/business/web/v1/auth" "github.com/ardanlabs/liarsdice/business/web/v1/mid" - "github.com/ardanlabs/liarsdice/foundation/events" "github.com/ardanlabs/liarsdice/foundation/logger" "github.com/ardanlabs/liarsdice/foundation/web" "github.com/ethereum/go-ethereum/common" @@ -32,13 +31,11 @@ type handlers struct { bank *bank.Bank log *logger.Logger ws websocket.Upgrader - evts *events.Events activeKID string auth *auth.Auth anteUSD float64 bankTimeout time.Duration connectTimeout time.Duration - games *games } // connect is used to return a game token for API usage. @@ -66,7 +63,6 @@ func (h *handlers) connect(ctx context.Context, w http.ResponseWriter, r *http.R // events handles a web socket to provide events to a client. func (h *handlers) events(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - v := web.GetValues(ctx) // Need this to handle CORS on the websocket. h.ws.CheckOrigin = func(r *http.Request) bool { return true } @@ -77,7 +73,7 @@ func (h *handlers) events(ctx context.Context, w http.ResponseWriter, r *http.Re return err } - h.log.Info(ctx, "websocket open", "path", "/v1/game/events", "traceid", v.TraceID) + h.log.Info(ctx, "websocket open", "path", "/v1/game/events") // Set the timeouts for the ping to identify if a web socket // connection is broken. @@ -94,8 +90,14 @@ func (h *handlers) events(ctx context.Context, w http.ResponseWriter, r *http.Re c.SetPongHandler(f) // This provides a channel for receiving events from the blockchain. - ch := h.evts.Acquire(v.TraceID) - defer h.evts.Release(v.TraceID) + subjectID := mid.GetSubject(ctx).String() + ch := evts.acquire(subjectID) + defer func() { + evts.release(subjectID) + h.log.Info(ctx, "evts.release", "account", subjectID) + }() + + h.log.Info(ctx, "evts.acquire", "account", subjectID) // Starting a ticker to send a ping message over the websocket. pingSend := time.NewTicker(pingPeriod) @@ -120,7 +122,7 @@ func (h *handlers) events(ctx context.Context, w http.ResponseWriter, r *http.Re defer func() { wg.Wait() - h.log.Info(ctx, "websocket closed", "path", "/v1/game/events", "traceid", v.TraceID) + h.log.Info(ctx, "websocket closed", "path", "/v1/game/events") }() defer c.Close() @@ -135,13 +137,15 @@ func (h *handlers) events(ctx context.Context, w http.ResponseWriter, r *http.Re } if err := c.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil { - h.log.Info(ctx, "websocket write", "path", "/v1/game/events", "traceid", v.TraceID, "ERROR", err) + h.log.Info(ctx, "websocket write", "path", "/v1/game/events", "ERROR", err) return nil } + h.log.Info(ctx, "evts.send", "msg", msg) + case <-pingSend.C: if err := c.WriteMessage(websocket.PingMessage, []byte("ping")); err != nil { - h.log.Info(ctx, "websocket ping", "path", "/v1/game/events", "traceid", v.TraceID, "ERROR", err) + h.log.Info(ctx, "websocket ping", "path", "/v1/game/events", "ERROR", err) return nil } } @@ -189,7 +193,7 @@ func (h *handlers) tables(ctx context.Context, w http.ResponseWriter, r *http.Re info := struct { GameIDs []string `json:"gameIDs"` }{ - GameIDs: h.games.active(), + GameIDs: game.Tables.Active(), } return web.Respond(ctx, w, info, http.StatusOK) @@ -201,7 +205,6 @@ func (h *handlers) status(ctx context.Context, w http.ResponseWriter, r *http.Re address := common.HexToAddress(claims.Subject) gameID := web.Param(ctx, "id") - h.log.Info(ctx, "******************* GETTING GAME ID", "ID", gameID) g, err := h.getGame(gameID) if err != nil { @@ -251,17 +254,19 @@ func (h *handlers) status(ctx context.Context, w http.ResponseWriter, r *http.Re // newGame creates a new game if there is no game or the status of the current game // is GameOver. func (h *handlers) newGame(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - claims := mid.GetClaims(ctx) - address := claims.Subject - - game, err := h.createGame(ctx, address) + g, err := game.New(ctx, h.log, h.converter, h.bank, mid.GetSubject(ctx), h.anteUSD) if err != nil { - return v1.NewTrustedError(err, http.StatusBadRequest) + return v1.NewTrustedError(fmt.Errorf("unable to create game: %w", err), http.StatusBadRequest) + } + + subjectID := mid.GetSubject(ctx).String() + + h.log.Info(ctx, "evts.addPlayerToGame", "gameID", g.ID(), "account", subjectID) + if err := evts.addPlayerToGame(g.ID(), subjectID); err != nil { + h.log.Info(ctx, "evts.addPlayerToGame", "ERROR", err, "account", subjectID) } - gameID := game.ID() - h.games.add(gameID, game) - ctx = web.SetParam(ctx, "id", gameID) + ctx = web.SetParam(ctx, "id", g.ID()) return h.status(ctx, w, r) } @@ -273,14 +278,18 @@ func (h *handlers) join(ctx context.Context, w http.ResponseWriter, r *http.Requ return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) + subjectID := mid.GetSubject(ctx) - if err := g.AddAccount(ctx, address); err != nil { + if err := g.AddAccount(ctx, subjectID); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"join","address":%q}`, address)) + h.log.Info(ctx, "evts.addPlayerToGame", "gameID", g.ID(), "account", subjectID) + if err := evts.addPlayerToGame(g.ID(), subjectID.String()); err != nil { + h.log.Info(ctx, "evts.addPlayerToGame", "ERROR", err, "account", subjectID) + } + + evts.send(g.ID(), fmt.Sprintf(`{"type":"join","address":%q}`, subjectID)) return h.status(ctx, w, r) } @@ -292,14 +301,11 @@ func (h *handlers) startGame(ctx context.Context, w http.ResponseWriter, r *http return err } - claims := mid.GetClaims(ctx) - address := claims.Subject - if err := g.StartGame(ctx); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"start","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"start","address":%q}`, mid.GetSubject(ctx))) return h.status(ctx, w, r) } @@ -311,14 +317,11 @@ func (h *handlers) rollDice(ctx context.Context, w http.ResponseWriter, r *http. return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - - if err := g.RollDice(ctx, address); err != nil { + if err := g.RollDice(ctx, mid.GetSubject(ctx)); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"rolldice","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"rolldice","address":%q}`, mid.GetSubject(ctx))) return h.status(ctx, w, r) } @@ -330,9 +333,6 @@ func (h *handlers) bet(ctx context.Context, w http.ResponseWriter, r *http.Reque return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - number, err := strconv.Atoi(web.Param(ctx, "number")) if err != nil { return v1.NewTrustedError(fmt.Errorf("converting number: %s", err), http.StatusBadRequest) @@ -343,11 +343,13 @@ func (h *handlers) bet(ctx context.Context, w http.ResponseWriter, r *http.Reque return v1.NewTrustedError(fmt.Errorf("converting suit: %s", err), http.StatusBadRequest) } + address := mid.GetSubject(ctx) + if err := g.Bet(ctx, address, number, suit); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"bet","address":%q,"index":%d}`, address, g.Info(ctx).Cups[address].OrderIdx)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"bet","address":%q,"index":%d}`, address, g.Info(ctx).Cups[address].OrderIdx)) return h.status(ctx, w, r) } @@ -359,10 +361,7 @@ func (h *handlers) callLiar(ctx context.Context, w http.ResponseWriter, r *http. return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - - if _, _, err := g.CallLiar(ctx, address); err != nil { + if _, _, err := g.CallLiar(ctx, mid.GetSubject(ctx)); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } @@ -370,7 +369,7 @@ func (h *handlers) callLiar(ctx context.Context, w http.ResponseWriter, r *http. return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"callliar","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"callliar","address":%q}`, mid.GetSubject(ctx))) return h.status(ctx, w, r) } @@ -382,9 +381,6 @@ func (h *handlers) reconcile(ctx context.Context, w http.ResponseWriter, r *http return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - ctx, cancel := context.WithTimeout(ctx, h.bankTimeout) defer cancel() @@ -392,20 +388,19 @@ func (h *handlers) reconcile(ctx context.Context, w http.ResponseWriter, r *http return v1.NewTrustedError(err, http.StatusInternalServerError) } - h.evts.Send(fmt.Sprintf(`{"type":"reconcile","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"reconcile","address":%q}`, mid.GetSubject(ctx))) + + evts.removePlayersFromGame(g.ID()) return h.status(ctx, w, r) } // balance returns the player balance from the smart contract. func (h *handlers) balance(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - claims := mid.GetClaims(ctx) - address := claims.Subject - ctx, cancel := context.WithTimeout(ctx, h.bankTimeout) defer cancel() - balanceGWei, err := h.bank.AccountBalance(ctx, common.HexToAddress(address)) + balanceGWei, err := h.bank.AccountBalance(ctx, mid.GetSubject(ctx)) if err != nil { return v1.NewTrustedError(err, http.StatusInternalServerError) } @@ -426,14 +421,11 @@ func (h *handlers) nextTurn(ctx context.Context, w http.ResponseWriter, r *http. return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - if err := g.NextTurn(ctx); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"nextturn","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"nextturn","address":%q}`, mid.GetSubject(ctx))) return h.status(ctx, w, r) } @@ -447,37 +439,25 @@ func (h *handlers) updateOut(ctx context.Context, w http.ResponseWriter, r *http return err } - claims := mid.GetClaims(ctx) - address := common.HexToAddress(claims.Subject) - outs, err := strconv.Atoi(web.Param(ctx, "outs")) if err != nil { return v1.NewTrustedError(fmt.Errorf("converting outs: %s", err), http.StatusBadRequest) } + address := mid.GetSubject(ctx) + if err := g.ApplyOut(ctx, address, outs); err != nil { return v1.NewTrustedError(err, http.StatusBadRequest) } - h.evts.Send(fmt.Sprintf(`{"type":"outs","address":%q}`, address)) + evts.send(g.ID(), fmt.Sprintf(`{"type":"outs","address":%q}`, address)) return h.status(ctx, w, r) } -// createGame resets the existing game. At this time we let this happen at any -// time regardless of game state. -func (h *handlers) createGame(ctx context.Context, address string) (*game.Game, error) { - g, err := game.New(ctx, h.log, h.converter, h.bank, common.HexToAddress(address), h.anteUSD) - if err != nil { - return nil, fmt.Errorf("unable to create game: %w", err) - } - - return g, nil -} - // getGame safely returns a copy of the game pointer. func (h *handlers) getGame(gameID string) (*game.Game, error) { - g, err := h.games.retrieve(gameID) + g, err := game.Tables.Retrieve(gameID) if err != nil { return nil, v1.NewTrustedError(errors.New("no game exists"), http.StatusBadRequest) } diff --git a/app/services/engine/v1/handlers/gamegrp/games.go b/app/services/engine/v1/handlers/gamegrp/games.go deleted file mode 100644 index 3a46d25b..00000000 --- a/app/services/engine/v1/handlers/gamegrp/games.go +++ /dev/null @@ -1,71 +0,0 @@ -package gamegrp - -import ( - "fmt" - "sync" - "time" - - "github.com/ardanlabs/liarsdice/business/core/game" -) - -type games struct { - mp map[string]*game.Game - mu sync.RWMutex -} - -func initGames() *games { - return &games{ - mp: make(map[string]*game.Game), - } -} - -func (g *games) add(key string, gm *game.Game) { - g.mu.Lock() - defer g.mu.Unlock() - - g.mp[key] = gm - - // Let's find games that are older than an hour and - // remove them from the cache. - hour := time.Now().Add(time.Hour) - for k, v := range g.mp { - if v.CreatedDate().After(hour) { - delete(g.mp, k) - } - } -} - -func (g *games) delete(key string) { - g.mu.Lock() - defer g.mu.Unlock() - - delete(g.mp, key) -} - -func (g *games) retrieve(key string) (*game.Game, error) { - g.mu.RLock() - defer g.mu.RUnlock() - - game, ok := g.mp[key] - if !ok { - return nil, fmt.Errorf("key %q not found", key) - } - - return game, nil -} - -func (g *games) active() []string { - g.mu.RLock() - defer g.mu.RUnlock() - - var ids []string - - for k, v := range g.mp { - switch v.Status() { - case game.StatusPlaying, game.StatusNewGame, game.StatusRoundOver: - ids = append(ids, k) - } - } - - return ids -} diff --git a/app/services/engine/v1/handlers/gamegrp/route.go b/app/services/engine/v1/handlers/gamegrp/route.go index 795efe4a..e8ba12e2 100644 --- a/app/services/engine/v1/handlers/gamegrp/route.go +++ b/app/services/engine/v1/handlers/gamegrp/route.go @@ -8,7 +8,6 @@ import ( "github.com/ardanlabs/liarsdice/business/core/bank" "github.com/ardanlabs/liarsdice/business/web/v1/auth" "github.com/ardanlabs/liarsdice/business/web/v1/mid" - "github.com/ardanlabs/liarsdice/foundation/events" "github.com/ardanlabs/liarsdice/foundation/logger" "github.com/ardanlabs/liarsdice/foundation/web" "github.com/gorilla/websocket" @@ -20,7 +19,7 @@ type Config struct { Auth *auth.Auth Converter *currency.Converter Bank *bank.Bank - Evts *events.Events + Evts *events AnteUSD float64 ActiveKID string BankTimeout time.Duration @@ -35,19 +34,17 @@ func Routes(app *web.App, cfg Config) { converter: cfg.Converter, bank: cfg.Bank, log: cfg.Log, - evts: cfg.Evts, ws: websocket.Upgrader{}, auth: cfg.Auth, activeKID: cfg.ActiveKID, anteUSD: cfg.AnteUSD, bankTimeout: cfg.BankTimeout, connectTimeout: cfg.ConnectTimeout, - games: initGames(), } app.Handle(http.MethodPost, version, "/game/connect", hdl.connect) - app.Handle(http.MethodGet, version, "/game/events", hdl.events) + app.Handle(http.MethodGet, version, "/game/events", hdl.events, mid.Authenticate(cfg.Auth)) app.Handle(http.MethodGet, version, "/game/config", hdl.configuration) app.Handle(http.MethodGet, version, "/game/usd2wei/:usd", hdl.usd2Wei) app.Handle(http.MethodGet, version, "/game/new", hdl.newGame, mid.Authenticate(cfg.Auth)) diff --git a/business/core/game/game.go b/business/core/game/game.go index a7a17018..f880dd38 100644 --- a/business/core/game/game.go +++ b/business/core/game/game.go @@ -74,6 +74,8 @@ func New(ctx context.Context, log *logger.Logger, converter *currency.Converter, return nil, errors.New("unable to add owner to the game") } + Tables.add(&g) + return &g, nil } diff --git a/business/core/game/tables.go b/business/core/game/tables.go new file mode 100644 index 00000000..a476ffaf --- /dev/null +++ b/business/core/game/tables.go @@ -0,0 +1,71 @@ +package game + +import ( + "fmt" + "sync" + "time" +) + +// Tables maintains the set of games in the system. +var Tables = newTables() + +// tables represent the current set of tables that actively exist. The state +// of these tables can be of any state. The Add API will remove tables that are +// older than an hour. +type tables struct { + games map[string]*Game + mu sync.RWMutex +} + +func newTables() *tables { + return &tables{ + games: make(map[string]*Game), + } +} + +// Add inserts the specified game into the table management system. +func (t *tables) add(game *Game) { + t.mu.Lock() + defer t.mu.Unlock() + + t.games[game.id] = game + + // Let's find games that are older than an hour and + // remove them from the cache. + hour := time.Now().Add(time.Hour) + for k, v := range t.games { + if v.CreatedDate().After(hour) { + delete(t.games, k) + } + } +} + +// Retrieve returns the specified game from the table management system. +func (t *tables) Retrieve(key string) (*Game, error) { + t.mu.RLock() + defer t.mu.RUnlock() + + game, ok := t.games[key] + if !ok { + return nil, fmt.Errorf("key %q not found", key) + } + + return game, nil +} + +// Active returns the IDs for all the active games in the system. +func (t *tables) Active() []string { + t.mu.RLock() + defer t.mu.RUnlock() + + var ids []string + + for k, v := range t.games { + switch v.Status() { + case StatusPlaying, StatusNewGame, StatusRoundOver: + ids = append(ids, k) + } + } + + return ids +} diff --git a/business/web/v1/mid/mid.go b/business/web/v1/mid/mid.go index 6110ba49..8fb29a44 100644 --- a/business/web/v1/mid/mid.go +++ b/business/web/v1/mid/mid.go @@ -5,6 +5,7 @@ import ( "context" "github.com/ardanlabs/liarsdice/business/web/v1/auth" + "github.com/ethereum/go-ethereum/common" ) type ctxKey int @@ -23,3 +24,8 @@ func GetClaims(ctx context.Context) auth.Claims { } return v } + +// GetSubject provides access to the subject from the claims. +func GetSubject(ctx context.Context) common.Address { + return common.HexToAddress(GetClaims(ctx).Subject) +} diff --git a/business/web/v1/mux/mux.go b/business/web/v1/mux/mux.go index d6c71536..68e09174 100644 --- a/business/web/v1/mux/mux.go +++ b/business/web/v1/mux/mux.go @@ -11,7 +11,6 @@ import ( "github.com/ardanlabs/liarsdice/business/core/bank" "github.com/ardanlabs/liarsdice/business/web/v1/auth" "github.com/ardanlabs/liarsdice/business/web/v1/mid" - "github.com/ardanlabs/liarsdice/foundation/events" "github.com/ardanlabs/liarsdice/foundation/logger" "github.com/ardanlabs/liarsdice/foundation/web" ) @@ -36,7 +35,6 @@ type Config struct { Auth *auth.Auth Converter *currency.Converter Bank *bank.Bank - Evts *events.Events AnteUSD float64 ActiveKID string BankTimeout time.Duration diff --git a/foundation/events/events.go b/foundation/events/events.go deleted file mode 100644 index baa48b57..00000000 --- a/foundation/events/events.go +++ /dev/null @@ -1,84 +0,0 @@ -// Package events allows for the registering and receiving of events. -package events - -import ( - "fmt" - "sync" -) - -// Events maintains a mapping of unique id and channels so goroutines -// can register and receive events. -type Events struct { - m map[string]chan string - mu sync.RWMutex -} - -// New constructs an events for registering and receiving events. -func New() *Events { - return &Events{ - m: make(map[string]chan string), - } -} - -// Shutdown closes and removes all channels that were provided by -// the call to Acquire. -func (evt *Events) Shutdown() { - evt.mu.RLock() - defer evt.mu.RUnlock() - - for id, ch := range evt.m { - delete(evt.m, id) - close(ch) - } -} - -// Acquire takes a unique id and returns a channel that can be used -// to receive events. -func (evt *Events) Acquire(id string) chan string { - evt.mu.Lock() - defer evt.mu.Unlock() - - ch, exists := evt.m[id] - if exists { - return ch - } - - // Since a message will be dropped if the websocket receiver is - // not ready to receive, this arbitrary buffer should give the receiver - // enough time to not lose a message. Websocket send could take long. - const messageBuffer = 100 - - evt.m[id] = make(chan string, messageBuffer) - return evt.m[id] -} - -// Release closes and removes the channel that was provided by -// the call to Acquire. -func (evt *Events) Release(id string) error { - evt.mu.Lock() - defer evt.mu.Unlock() - - ch, exists := evt.m[id] - if !exists { - return fmt.Errorf("id %q does not exist", id) - } - - delete(evt.m, id) - close(ch) - - return nil -} - -// Send signals a message to ever registered channel. Send will not block -// waiting for a receiver on any given channel. -func (evt *Events) Send(s string) { - evt.mu.RLock() - defer evt.mu.RUnlock() - - for _, ch := range evt.m { - select { - case ch <- s: - default: - } - } -}