From 4052b40c2da25f62ca3605edb7d3cad0059de780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Auger?= Date: Wed, 19 Aug 2020 08:23:02 +0200 Subject: [PATCH] add support for in memory and file system store --- config.toml.sample | 11 ++ go.mod | 1 - go.sum | 24 +--- internal/hub/hub.go | 1 + main.go | 48 ++++++-- store/fs/fs.go | 288 ++++++++++++++++++++++++++++++++++++++++++++ store/mem/mem.go | 218 +++++++++++++++++++++++++++++++++ 7 files changed, 560 insertions(+), 31 deletions(-) create mode 100644 store/fs/fs.go create mode 100644 store/mem/mem.go diff --git a/config.toml.sample b/config.toml.sample index 4fd1e51..36000c0 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -37,6 +37,9 @@ websocket_timeout = "3s" # Session cookie name. session_cookie = "niltoken" +# Storage kind, one of redis|memory|fs. +storage = "redis" + # Redis cache server. # Rooms are cached until they expires. Messages are not cached. [store] @@ -49,3 +52,11 @@ timeout = "3s" prefix_room = "NIL:ROOM:%s" prefix_session = "NIL:SESS:ROOM:%s" + +# InMemory store config. +# [store] +# no options available. + +# FileSystem store config. +# [store] +# path = "db.json" diff --git a/go.mod b/go.mod index cde7f97..52a6e82 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( github.com/go-chi/chi v4.1.0+incompatible - github.com/go-yaml/yaml v2.1.0+incompatible // indirect github.com/gomodule/redigo v2.0.0+incompatible github.com/gorilla/websocket v1.4.2 github.com/knadh/koanf v0.9.1 diff --git a/go.sum b/go.sum index 6ded6a2..890d0a2 100644 --- a/go.sum +++ b/go.sum @@ -4,29 +4,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= -github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w= github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/knadh/koanf v0.8.1 h1:4VLACWqrkWRQIup3ooq6lOnaSbOJSNO+YVXnJn/NPZ8= -github.com/knadh/koanf v0.8.1/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w= github.com/knadh/koanf v0.9.1 h1:qfcwiF9/Z8buTJ0QXaZvOxJ6eKJmOiiWKP/PktiW5RE= github.com/knadh/koanf v0.9.1/go.mod h1:31bzRSM7vS5Vm9LNLo7B2Re1zhLOZT6EQKeodixBikE= -github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M= -github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4= github.com/knadh/stuffbin v1.1.0 h1:f5S5BHzZALjuJEgTIOMC9NidEnBJM7Ze6Lu1GHR/lwU= github.com/knadh/stuffbin v1.1.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -34,33 +23,24 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= -github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= -golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24 h1:R8bzl0244nw47n1xKs1MUMAaTNgjavKcN/aX2Ss3+Fo= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -69,7 +49,5 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/hub/hub.go b/internal/hub/hub.go index f10d073..72d35f1 100644 --- a/internal/hub/hub.go +++ b/internal/hub/hub.go @@ -44,6 +44,7 @@ type Config struct { RoomTimeout time.Duration `koanf:"room_timeout"` RoomAge time.Duration `koanf:"room_age"` SessionCookie string `koanf:"session_cookie"` + Storage string `koanf:"storage"` } // Hub acts as the controller and container for all chat rooms. diff --git a/main.go b/main.go index 3ffb6bd..42753b2 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,9 @@ import ( "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/posflag" "github.com/knadh/niltalk/internal/hub" + "github.com/knadh/niltalk/store" + "github.com/knadh/niltalk/store/fs" + "github.com/knadh/niltalk/store/mem" "github.com/knadh/niltalk/store/redis" "github.com/knadh/stuffbin" flag "github.com/spf13/pflag" @@ -195,14 +198,45 @@ func main() { } // Initialize store. - var storeCfg redis.Config - if err := ko.Unmarshal("store", &storeCfg); err != nil { - logger.Fatalf("error unmarshalling 'store' config: %v", err) - } + var store store.Store + if app.cfg.Storage == "redis" { + var storeCfg redis.Config + if err := ko.Unmarshal("store", &storeCfg); err != nil { + logger.Fatalf("error unmarshalling 'store' config: %v", err) + } - store, err := redis.New(storeCfg) - if err != nil { - log.Fatalf("error initializing store: %v", err) + s, err := redis.New(storeCfg) + if err != nil { + log.Fatalf("error initializing store: %v", err) + } + store = s + + } else if app.cfg.Storage == "memory" { + var storeCfg mem.Config + if err := ko.Unmarshal("store", &storeCfg); err != nil { + logger.Fatalf("error unmarshalling 'store' config: %v", err) + } + + s, err := mem.New(storeCfg) + if err != nil { + log.Fatalf("error initializing store: %v", err) + } + store = s + + } else if app.cfg.Storage == "fs" { + var storeCfg fs.Config + if err := ko.Unmarshal("store", &storeCfg); err != nil { + logger.Fatalf("error unmarshalling 'store' config: %v", err) + } + + s, err := fs.New(storeCfg, logger) + if err != nil { + log.Fatalf("error initializing store: %v", err) + } + store = s + + } else { + logger.Fatal("app.storage must be one of redis|memory|fs") } app.hub = hub.NewHub(app.cfg, store, logger) diff --git a/store/fs/fs.go b/store/fs/fs.go new file mode 100644 index 0000000..00fb073 --- /dev/null +++ b/store/fs/fs.go @@ -0,0 +1,288 @@ +package fs + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "sync" + "time" + + "github.com/knadh/niltalk/store" +) + +// Config represents the file store config structure. +type Config struct { + Path string `koanf:"path"` +} + +// File represents the file implementation of the Store interface. +type File struct { + cfg *Config + rooms map[string]*room + data map[string][]byte + mu sync.Mutex + dirty bool + log *log.Logger +} + +type room struct { + store.Room + Sessions map[string]string + Expire time.Time +} + +// New returns a new Redis store. +func New(cfg Config, log *log.Logger) (*File, error) { + store := &File{ + cfg: &cfg, + rooms: map[string]*room{}, + data: map[string][]byte{}, + log: log, + } + err := store.load() + go store.watch() + return store, err +} + +// watch the store to clean it up. +func (m *File) watch() { + t := time.NewTicker(time.Minute) + defer t.Stop() + for range t.C { + m.cleanup() + m.save() + } +} + +// cleanup the store to removes expired items. +func (m *File) cleanup() { + m.mu.Lock() + defer m.mu.Unlock() + + now := time.Now() + + for id, r := range m.rooms { + if r.Expire.Before(now) { + delete(m.rooms, id) + m.dirty = true + continue + } + } +} + +// load the data from the file system. +func (m *File) load() error { + if _, err := os.Stat(m.cfg.Path); os.IsExist(err) { + x := struct { + Rooms map[string]*room + Data map[string][]byte + }{} + var data []byte + data, err = ioutil.ReadFile(m.cfg.Path) + if err != nil { + return err + } + err = json.Unmarshal(data, &x) + if err != nil { + return err + } + m.rooms = x.Rooms + m.data = x.Data + } + return nil +} + +// save the data to the file system. +func (m *File) save() { + m.mu.Lock() + defer m.mu.Unlock() + if m.dirty { + data, err := json.Marshal(struct { + Rooms map[string]*room + Data map[string][]byte + }{ + Rooms: m.rooms, + Data: m.data, + }) + if err == nil { + m.dirty = false + go func() { + err := ioutil.WriteFile(m.cfg.Path, data, os.ModePerm) + if err != nil { + m.log.Printf("error writing file %q: %v", m.cfg.Path, err) + } + }() + } + } +} + +// AddRoom adds a room to the store. +func (m *File) AddRoom(r store.Room, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + key := r.ID + m.rooms[key] = &room{ + Room: r, + Expire: r.CreatedAt.Add(ttl), + Sessions: map[string]string{}, + } + m.dirty = true + + return nil +} + +// ExtendRoomTTL extends a room's TTL. +func (m *File) ExtendRoomTTL(id string, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[id] + if !ok { + return store.ErrRoomNotFound + } + + room.Expire = room.Expire.Add(ttl) + m.rooms[id] = room + m.dirty = true + return nil +} + +// GetRoom gets a room from the store. +func (m *File) GetRoom(id string) (store.Room, error) { + m.mu.Lock() + defer m.mu.Unlock() + + out, ok := m.rooms[id] + + if !ok { + return out.Room, store.ErrRoomNotFound + } + return out.Room, nil +} + +// RoomExists checks if a room exists in the store. +func (m *File) RoomExists(id string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + _, ok := m.rooms[id] + + return ok, nil +} + +// RemoveRoom deletes a room from the store. +func (m *File) RemoveRoom(id string) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, ok := m.rooms[id]; ok { + delete(m.rooms, id) + m.dirty = true + } + + return nil +} + +// AddSession adds a sessionID room to the store. +func (m *File) AddSession(sessID, handle, roomID string, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + room.Sessions[sessID] = handle + m.rooms[roomID] = room + m.dirty = true + + return nil +} + +// GetSession retrieves a peer session from the store. +func (m *File) GetSession(sessID, roomID string) (store.Sess, error) { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.Sess{}, store.ErrRoomNotFound + } + + handle, ok := room.Sessions[sessID] + + if !ok { + return store.Sess{}, nil + } + + return store.Sess{ + ID: sessID, + Handle: handle, + }, nil +} + +// RemoveSession deletes a session ID from a room. +func (m *File) RemoveSession(sessID, roomID string) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + if _, ok := room.Sessions[sessID]; ok { + delete(room.Sessions, sessID) + m.rooms[roomID] = room + m.dirty = true + } + + return nil +} + +// ClearSessions deletes all the sessions in a room. +func (m *File) ClearSessions(roomID string) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + room.Sessions = map[string]string{} + + m.rooms[roomID] = room + m.dirty = true + + return nil +} + +// Get value from a key. +func (m *File) Get(key string) ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() + d, ok := m.data[key] + if !ok { + return nil, fmt.Errorf("key %q not found", key) + } + return d, nil +} + +// Set a value. +func (m *File) Set(key string, data []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + m.data[key] = make([]byte, len(data), len(data)) + copy(m.data[key], data) + m.dirty = true + return nil +} diff --git a/store/mem/mem.go b/store/mem/mem.go new file mode 100644 index 0000000..fa1418c --- /dev/null +++ b/store/mem/mem.go @@ -0,0 +1,218 @@ +package mem + +import ( + "fmt" + "sync" + "time" + + "github.com/knadh/niltalk/store" +) + +// Config represents the InMemory store config structure. +type Config struct{} + +// InMemory represents the in-memory implementation of the Store interface. +type InMemory struct { + cfg *Config + rooms map[string]*room + data map[string][]byte + mu sync.Mutex +} + +type room struct { + store.Room + Sessions map[string]string + Expire time.Time +} + +// New returns a new Redis store. +func New(cfg Config) (*InMemory, error) { + store := &InMemory{ + cfg: &cfg, + rooms: map[string]*room{}, + data: map[string][]byte{}, + } + go store.watch() + return store, nil +} + +// watch the store to clean it up. +func (m *InMemory) watch() { + t := time.NewTicker(time.Minute) + defer t.Stop() + for range t.C { + m.cleanup() + } +} + +// cleanup the store to removes expired items. +func (m *InMemory) cleanup() { + m.mu.Lock() + defer m.mu.Unlock() + + now := time.Now() + + for id, r := range m.rooms { + if r.Expire.Before(now) { + delete(m.rooms, id) + continue + } + } +} + +// AddRoom adds a room to the store. +func (m *InMemory) AddRoom(r store.Room, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.rooms[r.ID] = &room{ + Room: r, + Expire: r.CreatedAt.Add(ttl), + Sessions: map[string]string{}, + } + + return nil +} + +// ExtendRoomTTL extends a room's TTL. +func (m *InMemory) ExtendRoomTTL(id string, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[id] + if !ok { + return store.ErrRoomNotFound + } + + room.Expire = room.Expire.Add(ttl) + m.rooms[id] = room + return nil +} + +// GetRoom gets a room from the store. +func (m *InMemory) GetRoom(id string) (store.Room, error) { + m.mu.Lock() + defer m.mu.Unlock() + + out, ok := m.rooms[id] + + if !ok { + return out.Room, store.ErrRoomNotFound + } + return out.Room, nil +} + +// RoomExists checks if a room exists in the store. +func (m *InMemory) RoomExists(id string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + _, ok := m.rooms[id] + + return ok, nil +} + +// RemoveRoom deletes a room from the store. +func (m *InMemory) RemoveRoom(id string) error { + m.mu.Lock() + defer m.mu.Unlock() + + delete(m.rooms, id) + + return nil +} + +// AddSession adds a sessionID room to the store. +func (m *InMemory) AddSession(sessID, handle, roomID string, ttl time.Duration) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + room.Sessions[sessID] = handle + m.rooms[roomID] = room + + return nil +} + +// GetSession retrieves a peer session from the store. +func (m *InMemory) GetSession(sessID, roomID string) (store.Sess, error) { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.Sess{}, store.ErrRoomNotFound + } + + handle, ok := room.Sessions[sessID] + + if !ok { + return store.Sess{}, nil + } + + return store.Sess{ + ID: sessID, + Handle: handle, + }, nil +} + +// RemoveSession deletes a session ID from a room. +func (m *InMemory) RemoveSession(sessID, roomID string) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + delete(room.Sessions, sessID) + m.rooms[roomID] = room + + return nil +} + +// ClearSessions deletes all the sessions in a room. +func (m *InMemory) ClearSessions(roomID string) error { + m.mu.Lock() + defer m.mu.Unlock() + + room, ok := m.rooms[roomID] + + if !ok { + return store.ErrRoomNotFound + } + + room.Sessions = map[string]string{} + + m.rooms[roomID] = room + + return nil +} + +// Get value from a key. +func (m *InMemory) Get(key string) ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() + d, ok := m.data[key] + if !ok { + return nil, fmt.Errorf("key %q not found", key) + } + return d, nil +} + +// Set a value. +func (m *InMemory) Set(key string, data []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + m.data[key] = make([]byte, len(data), len(data)) + copy(m.data[key], data) + return nil +}