oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1// Package live is the fan-out between one person's edit and everybody
2// else's open tab.
3//
4// It knows nothing about cells, sheets, HTTP or storage: a Message
5// carries an opaque, already-marshalled payload and a version, and the
6// hub moves it from one publisher to every subscriber on a tab. That
7// boundary is deliberate and load-bearing. AGENTS.md forbids cell
8// content in any log line, and a fan-out package that could see inside
9// its payloads is a package whose first debugging session breaks that
10// rule. This one cannot.
11package live
12
13import (
14 "sync"
15 "sync/atomic"
16)
17
18// subBuffer is how many messages a subscriber may fall behind by before
19// it is declared lost.
20//
21// Deep enough that an ordinary tab under a burst — a paste that
22// recalculates a column, several people typing at once — never overruns,
23// and shallow enough that a thousand wedged tabs cost kilobytes rather
24// than megabytes. The alternative, an unbounded buffer, turns one
25// browser that stopped reading into an instance-wide memory leak with no
26// upper bound and no symptom until the process dies.
27const subBuffer = 64
28
29// Message is one fan-out payload.
30//
31// Data is already-marshalled bytes. The hub never looks inside it, and
32// nothing here ever logs it.
33type Message struct {
34 Version uint64
35 Data []byte
36}
37
38// Sub is one open connection's subscription. Get one from
39// Hub.Subscribe; always Close it.
40type Sub struct {
41 hub *Hub
42 tabID uint64
43 ch chan Message
44 lost atomic.Bool
45 once sync.Once
46}
47
48// C is the channel to select on. It is never closed — Close removes the
49// subscription from the hub instead — so a reader distinguishes "no
50// messages" from "gone" through its own context, not through a channel
51// close it would have to race the publisher for.
52func (s *Sub) C() <-chan Message { return s.ch }
53
54// Lost reports whether this subscriber overran subBuffer and has missed
55// at least one message. A lost subscriber must not keep painting: its
56// grid would have holes in it that nothing later fills. The SSE handler
57// tells the client to resync instead.
58func (s *Sub) Lost() bool { return s.lost.Load() }
59
60// Close unsubscribes. It is idempotent: an SSE handler defers it and may
61// also reach it on a write error, and doing the removal twice must not
62// panic or disturb a subscription some later Subscribe handed out.
63func (s *Sub) Close() {
64 s.once.Do(func() { s.hub.remove(s) })
65}
66
67// Hub fans messages out to the subscribers of a tab. The zero value is
68// not usable; call NewHub. One per process.
69type Hub struct {
70 mu sync.Mutex
71 tabs map[uint64]map[*Sub]struct{}
72}
73
74// NewHub builds an empty hub.
75func NewHub() *Hub {
76 return &Hub{tabs: map[uint64]map[*Sub]struct{}{}}
77}
78
79// Subscribe registers a new subscriber for a tab.
80func (h *Hub) Subscribe(tabID uint64) *Sub {
81 s := &Sub{hub: h, tabID: tabID, ch: make(chan Message, subBuffer)}
82 h.mu.Lock()
83 defer h.mu.Unlock()
84 subs, ok := h.tabs[tabID]
85 if !ok {
86 subs = map[*Sub]struct{}{}
87 h.tabs[tabID] = subs
88 }
89 subs[s] = struct{}{}
90 return s
91}
92
93// Publish delivers a message to every current subscriber of a tab.
94//
95// The send is non-blocking, always. A subscriber whose buffer is full is
96// marked lost and skipped — never waited for — because the caller is a
97// request handler that has already committed a database write, and
98// blocking it on a browser that stopped reading would hold a writer
99// connection open for as long as that browser felt like it.
100//
101// Publish is safe to call with no subscribers, which is the common case:
102// one person editing alone.
103func (h *Hub) Publish(tabID uint64, m Message) {
104 h.mu.Lock()
105 subs := make([]*Sub, 0, len(h.tabs[tabID]))
106 for s := range h.tabs[tabID] {
107 subs = append(subs, s)
108 }
109 h.mu.Unlock()
110
111 // Sent outside the lock: a slow select here would otherwise block
112 // every other tab's Subscribe and Close as well.
113 for _, s := range subs {
114 select {
115 case s.ch <- m:
116 default:
117 s.lost.Store(true)
118 }
119 }
120}
121
122// Subscribers reports how many subscriptions a tab currently has.
123// Exposed for tests and for a presence count later; it deliberately
124// counts connections rather than people.
125func (h *Hub) Subscribers(tabID uint64) int {
126 h.mu.Lock()
127 defer h.mu.Unlock()
128 return len(h.tabs[tabID])
129}
130
131// remove drops a subscription, and the tab's map entry with it once the
132// last subscriber leaves — so an instance that has served ten thousand
133// tabs over a week does not keep ten thousand empty maps.
134func (h *Hub) remove(s *Sub) {
135 h.mu.Lock()
136 defer h.mu.Unlock()
137 subs, ok := h.tabs[s.tabID]
138 if !ok {
139 return
140 }
141 delete(subs, s)
142 if len(subs) == 0 {
143 delete(h.tabs, s.tabID)
144 }
145}
146