158 lines
3.2 KiB
Go
158 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Syncable interface {
|
|
Sync()
|
|
}
|
|
type UserAndSync[TR any, TU comparable] struct {
|
|
user *TU
|
|
sync Syncable
|
|
}
|
|
type Rooms[TR any, TU comparable] struct {
|
|
byName map[string]*Room[TR, TU]
|
|
names []string
|
|
}
|
|
|
|
func (rs *Rooms[TR, TU]) Visit(fn func(n string)) {
|
|
for _, n := range rs.names {
|
|
fn(n)
|
|
}
|
|
}
|
|
|
|
func (rs *Rooms[TR, TU]) Get(n string) (*Room[TR, TU], bool) {
|
|
rm, ok := rs.byName[n]
|
|
return rm, ok
|
|
}
|
|
|
|
func (rs *Rooms[TR, TU]) Start() {
|
|
for _, rm := range rs.byName {
|
|
go rm.run()
|
|
}
|
|
}
|
|
|
|
func (rs *Rooms[TR, TU]) Stop() {
|
|
for _, rm := range rs.byName {
|
|
rm.stop()
|
|
}
|
|
}
|
|
|
|
// NewRooms seeds the rooms once at startup.
|
|
// Assumptions: rooms don't change. Should be sorted by name.
|
|
func NewRooms[TR any, TU comparable](names ...string) Rooms[TR, TU] {
|
|
byName := make(map[string]*Room[TR, TU])
|
|
for _, n := range names {
|
|
byName[n] = NewRoom[TR, TU](n)
|
|
}
|
|
|
|
return Rooms[TR, TU]{byName, names}
|
|
}
|
|
|
|
type Room[TR any, TU comparable] struct {
|
|
data TR
|
|
dataMu sync.RWMutex
|
|
members map[TU]Syncable
|
|
membersMu sync.RWMutex
|
|
Name string
|
|
join chan *UserAndSync[TR, TU]
|
|
leave chan *TU
|
|
done chan struct{}
|
|
stopChannel chan struct{}
|
|
dirty bool
|
|
}
|
|
|
|
// UpdateData lets the calling function update the room data.
|
|
// Is called with a write lock - so should be *fast*
|
|
func (r *Room[TR, TU]) UpdateData(fn func(data *TR)) {
|
|
r.dataMu.Lock()
|
|
defer r.dataMu.Unlock()
|
|
fn(&r.data)
|
|
r.dirty = true
|
|
}
|
|
|
|
func (r *Room[TR, TU]) Publish() {
|
|
r.dataMu.Lock()
|
|
if !r.dirty {
|
|
r.dataMu.Unlock()
|
|
return
|
|
}
|
|
|
|
publishers := make([]Syncable, 0, len(r.members))
|
|
for _, sync := range r.members {
|
|
publishers = append(publishers, sync)
|
|
}
|
|
r.dirty = false
|
|
r.dataMu.Unlock()
|
|
|
|
// Now call Sync without holding the lock
|
|
for _, sync := range publishers {
|
|
sync.Sync()
|
|
}
|
|
}
|
|
|
|
// GetData returns a copy of room data.
|
|
// Accepts an optional subset function to transform data before copying.
|
|
func (r *Room[TR, TU]) GetData(subsetFn ...func(*TR) TR) TR {
|
|
r.dataMu.RLock()
|
|
defer r.dataMu.RUnlock()
|
|
|
|
if len(subsetFn) == 0 || subsetFn[0] == nil {
|
|
return r.data
|
|
}
|
|
|
|
tmp := r.data
|
|
return subsetFn[0](&tmp)
|
|
}
|
|
|
|
func (r *Room[TR, TU]) Join(us *UserAndSync[TR, TU]) {
|
|
r.join <- us
|
|
}
|
|
|
|
func (r *Room[TR, TU]) Leave(u *TU) {
|
|
r.leave <- u
|
|
}
|
|
|
|
func NewRoom[TR any, TU comparable](n string) *Room[TR, TU] {
|
|
return &Room[TR, TU]{
|
|
Name: n,
|
|
join: make(chan *UserAndSync[TR, TU], 5),
|
|
leave: make(chan *TU, 5),
|
|
stopChannel: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
members: make(map[TU]Syncable),
|
|
}
|
|
}
|
|
|
|
func (r *Room[TR, TU]) run() {
|
|
defer close(r.done)
|
|
publishTicker := time.NewTicker(100 * time.Millisecond)
|
|
defer publishTicker.Stop()
|
|
for {
|
|
select {
|
|
case usrAndSync := <-r.join:
|
|
fmt.Println("Joining: ", *usrAndSync.user)
|
|
r.membersMu.Lock()
|
|
r.members[*usrAndSync.user] = usrAndSync.sync
|
|
r.membersMu.Unlock()
|
|
case usr := <-r.leave:
|
|
fmt.Println("Leaving: ", *usr)
|
|
r.membersMu.Lock()
|
|
delete(r.members, *usr)
|
|
r.membersMu.Unlock()
|
|
case <-publishTicker.C:
|
|
r.Publish()
|
|
case <-r.stopChannel:
|
|
return // exit goroutine
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Room[TR, TU]) stop() {
|
|
close(r.stopChannel)
|
|
<-r.done // wait for run() to finish
|
|
}
|