Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
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}