Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at main 138 lines 2.8 kB view raw
1package collaboration 2 3import ( 4 "encoding/json" 5 "log" 6 "sync" 7) 8 9type Hub struct { 10 rooms map[string]*Room 11 mu sync.RWMutex 12} 13 14type Room struct { 15 documentRKey string 16 clients map[*Client]bool 17 broadcast chan *broadcastMsg 18 register chan *Client 19 unregister chan *Client 20 mu sync.RWMutex 21} 22 23// broadcastMsg carries a message and an optional sender to exclude. 24type broadcastMsg struct { 25 data []byte 26 except *Client // nil = send to all 27} 28 29func NewHub() *Hub { 30 return &Hub{ 31 rooms: make(map[string]*Room), 32 } 33} 34 35func (h *Hub) GetOrCreateRoom(rkey string) *Room { 36 h.mu.Lock() 37 defer h.mu.Unlock() 38 if room, exists := h.rooms[rkey]; exists { 39 return room 40 } 41 room := &Room{ 42 documentRKey: rkey, 43 clients: make(map[*Client]bool), 44 broadcast: make(chan *broadcastMsg, 256), 45 register: make(chan *Client), 46 unregister: make(chan *Client), 47 } 48 h.rooms[rkey] = room 49 go room.run() 50 return room 51} 52 53func (h *Hub) GetRoom(rkey string) *Room { 54 h.mu.RLock() 55 defer h.mu.RUnlock() 56 return h.rooms[rkey] 57} 58 59func (r *Room) run() { 60 for { 61 select { 62 case client := <-r.register: 63 r.mu.Lock() 64 r.clients[client] = true 65 r.mu.Unlock() 66 r.broadcastPresence() 67 case client := <-r.unregister: 68 r.mu.Lock() 69 if _, ok := r.clients[client]; ok { 70 delete(r.clients, client) 71 close(client.send) 72 } 73 r.mu.Unlock() 74 r.broadcastPresence() 75 case msg := <-r.broadcast: 76 r.mu.RLock() 77 for client := range r.clients { 78 if client == msg.except { 79 continue 80 } 81 select { 82 case client.send <- msg.data: 83 default: 84 close(client.send) 85 delete(r.clients, client) 86 } 87 } 88 r.mu.RUnlock() 89 } 90 } 91} 92 93// Broadcast sends to all clients in the room. 94func (r *Room) Broadcast(data []byte) { 95 r.broadcast <- &broadcastMsg{data: data} 96} 97 98// BroadcastExcept sends to all clients except the given sender. 99func (r *Room) BroadcastExcept(data []byte, except *Client) { 100 r.broadcast <- &broadcastMsg{data: data, except: except} 101} 102 103func (r *Room) RegisterClient(client *Client) { 104 r.register <- client 105} 106 107func (r *Room) UnregisterClient(client *Client) { 108 r.unregister <- client 109} 110 111func (r *Room) GetPresence() []PresenceUser { 112 r.mu.RLock() 113 defer r.mu.RUnlock() 114 users := make([]PresenceUser, 0, len(r.clients)) 115 for client := range r.clients { 116 users = append(users, PresenceUser{ 117 DID: client.DID, 118 Name: client.Name, 119 Handle: client.Handle, 120 Color: client.Color, 121 Avatar: client.Avatar, 122 }) 123 } 124 return users 125} 126 127func (r *Room) broadcastPresence() { 128 presence := PresenceMessage{ 129 Type: "presence", 130 Users: r.GetPresence(), 131 } 132 data, err := json.Marshal(presence) 133 if err != nil { 134 log.Printf("broadcastPresence: marshal failed: %v", err) 135 return 136 } 137 r.Broadcast(data) 138}