Files
2025-11-15 10:40:07 -01:00

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
}