oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hub
2
3import "context"
4
5func admittedCountLocked(r *room) int {
6 n := 0
7 for _, p := range r.peers {
8 if p.Admitted {
9 n++
10 }
11 }
12 return n
13}
14
15func waitingCountLocked(r *room) int {
16 n := 0
17 for _, p := range r.peers {
18 if p.Joined && !p.Admitted {
19 n++
20 }
21 }
22 return n
23}
24
25// rosterFor is the authoritative snapshot for one viewer: admitted peers
26// with the viewer's pair generation, and the waiting queue only when the
27// viewer is a member (spec §4.3).
28func (h *Hub) rosterFor(r *room, viewer *Participation) *Roster {
29 ro := &Roster{}
30 for _, p := range r.peers {
31 if !p.Admitted {
32 continue
33 }
34 rp := RosterPeer{Peer: p.Peer, Name: p.Name, Kind: p.Kind.String(), Media: p.Media}
35 if p.Peer != viewer.Peer {
36 rp.Gen = h.genLocked(r, viewer.Peer, p.Peer)
37 }
38 ro.Peers = append(ro.Peers, rp)
39 }
40 if viewer.Kind == KindMember {
41 for _, p := range r.peers {
42 if p.Joined && !p.Admitted {
43 // Usually a guest, but a peer the tick promoted while the
44 // room was full waits on as a member (spec §2.4).
45 ro.Waiting = append(ro.Waiting, RosterPeer{Peer: p.Peer, Name: p.Name, Kind: p.Kind.String()})
46 }
47 }
48 }
49 return ro
50}
51
52// genLocked returns the pair's generation, starting it at one on first
53// sight (spec §4.6).
54func (h *Hub) genLocked(r *room, a, b PeerID) uint64 {
55 k := keyFor(a, b)
56 if r.gens[k] == 0 {
57 r.gens[k] = 1
58 }
59 return r.gens[k]
60}
61
62// admitLocked is the one transition into the admitted state: the cap
63// under the lock, roster to the newcomer, peer to the others, knock-gone
64// to members if they were waiting. Returns false when the room is full.
65func (h *Hub) admitLocked(r *room, p *Participation) bool {
66 if admittedCountLocked(r) >= MaxAdmitted {
67 return false
68 }
69 wasWaiting := p.Joined && !p.Admitted
70 p.Admitted = true
71 p.Joined = true
72 p.JoinedAt = h.cfg.Now()
73 h.sendLocked(p, Message{Type: TypeRoster, Roster: h.rosterFor(r, p)})
74 for _, q := range r.peers {
75 if q.Admitted && q.Peer != p.Peer && q.sub != nil {
76 // The one-entry roster carries this recipient's own pair
77 // generation with the newcomer; browsers read it from
78 // roster.peers[0].
79 q.sub.send(Message{Type: TypePeer, Peer: p.Peer, Name: p.Name,
80 Roster: &Roster{Peers: []RosterPeer{{Peer: p.Peer, Name: p.Name, Kind: p.Kind.String(), Gen: h.genLocked(r, q.Peer, p.Peer), Media: p.Media}}}})
81 }
82 }
83 if wasWaiting {
84 // Not to the newcomer: a peer the tick promoted to member is
85 // admitted as one, and would otherwise be told its own knock had
86 // gone, a moment after the roster that already said so.
87 h.broadcastLocked(r, Message{Type: TypeKnockGone, From: p.Peer}, func(q *Participation) bool {
88 return q.Peer != p.Peer && admittedMembers(q)
89 })
90 }
91 return true
92}
93
94// refreshRostersLocked sends the authoritative snapshot to every
95// admitted peer, which is what a kind change owes them (spec §2.4).
96func (h *Hub) refreshRostersLocked(r *room) {
97 for _, q := range r.peers {
98 if q.Admitted && q.sub != nil {
99 q.sub.send(Message{Type: TypeRoster, Roster: h.rosterFor(r, q)})
100 }
101 }
102}
103
104// setKindLocked records a re-derived kind. A kind that actually changed
105// on an admitted peer owes every admitted peer a fresh roster, whether
106// the change was seen on the tick or on a request (spec §2.4), so every
107// re-derivation writes Kind through here and nowhere else.
108func (h *Hub) setKindLocked(r *room, p *Participation, kind Kind) {
109 if p.Kind == kind {
110 return
111 }
112 p.Kind = kind
113 if p.Admitted {
114 h.refreshRostersLocked(r)
115 }
116}
117
118// standingLostLocked applies a loss of standing the way spec §2.4 asks.
119// A member loses the room; so does anyone not yet admitted, a guest with
120// the reason their grant earns. An **admitted** guest is untouched: a
121// participation is never ended by a grant expiring, because the
122// participation is the authority now (ruling 14). It reports whether the
123// participation ended, and announces the departure when it did.
124func (h *Hub) standingLostLocked(r *room, p *Participation) bool {
125 if p.Kind != KindMember && p.Admitted {
126 return false
127 }
128 reason := ReasonExpired
129 if p.Kind == KindMember {
130 reason = ReasonStanding
131 }
132 wasAdmitted, wasWaiting := h.endLocked(r, p, reason)
133 h.announceDepartureLocked(r, p, wasAdmitted, wasWaiting)
134 return true
135}
136
137// memberStandingGone re-derives standing for a participation whose kind
138// is `member`, which spec §2.5's paragraph — and §15.12, which corrects
139// the table row above it — require on **every** request that
140// participation makes, not only on the tick: "a member who has just been
141// removed is dropped by the next thing they do, not the next tick."
142// Relay, Reset, Resume and CanResume all go through here first.
143//
144// A guest is never touched and never costs a standing read: ruling 14
145// makes the participation the authority once someone is admitted, and a
146// waiting guest is the tick's to expire.
147//
148// end says whether this call may write **at all**. The mutating callers
149// (Relay, Reset, Resume) end a member who has lost standing here and
150// now, with lost{standing} and the departure the room is owed, and
151// record a kind that has changed; the probe (CanResume) passes false and
152// writes nothing whatever the answer was — not the ending, and not the
153// kind either, because recording a member's demotion to guest would
154// broadcast a fresh roster to every admitted peer, which is a room-wide
155// event caused by a request that is defined as changing nothing (§15.3,
156// Codex S2-10). The next mutating request, or the tick, applies it.
157//
158// The standing read happens with the lock down, like every other one
159// (StandingFor is a database read), so the participation is re-found
160// under the lock afterwards: it may have ended while the read was in
161// flight.
162func (h *Hub) memberStandingGone(ctx context.Context, sqid, session string, peer PeerID, end bool) (bool, error) {
163 h.mu.Lock()
164 p, found := h.findLocked(sqid, session, peer)
165 if !found || p.Kind != KindMember {
166 // Not found is the caller's own lookup to answer, in its own
167 // words; a guest is not this function's business at all.
168 h.mu.Unlock()
169 return false, nil
170 }
171 personID := p.PersonID
172 h.mu.Unlock()
173
174 kind, ok, err := h.cfg.StandingFor(ctx, sqid, personID)
175 if err != nil {
176 return false, err
177 }
178
179 h.mu.Lock()
180 defer h.mu.Unlock()
181 p, found = h.findLocked(sqid, session, peer)
182 if !found {
183 return false, nil
184 }
185 r := h.rooms[sqid]
186 if !ok {
187 if !end {
188 // The kind is read again rather than remembered: the tick may
189 // have made this an admitted guest while the read was out, and
190 // that one keeps everything (ruling 14).
191 return p.Kind == KindMember, nil
192 }
193 return h.standingLostLocked(r, p), nil
194 }
195 if !end {
196 return false, nil
197 }
198 h.setKindLocked(r, p, kind)
199 return false, nil
200}
201
202// Join is the entry, so standing is re-derived now (spec §2.5).
203func (h *Hub) Join(ctx context.Context, sqid, session string, peer PeerID) error {
204 kind, ok, err := h.standing(ctx, sqid, session, peer)
205 if err != nil {
206 return err
207 }
208 waiting := true
209 if h.cfg.WaitingRoom != nil {
210 waiting, err = h.cfg.WaitingRoom(ctx, sqid)
211 if err != nil {
212 return err
213 }
214 }
215 h.mu.Lock()
216 defer h.mu.Unlock()
217 p, found := h.findLocked(sqid, session, peer)
218 if !found {
219 return ErrUnknownPeer // it ended while standing was being read
220 }
221 r := h.rooms[sqid]
222 if !ok {
223 // Standing is checked before the kind is written, because what a
224 // loss costs depends on the kind the peer held. A member is
225 // dropped by the next thing they do, not the next tick (§2.5);
226 // an admitted guest whose grant lapsed keeps everything.
227 if h.standingLostLocked(r, p) {
228 return ErrNotYours
229 }
230 return nil
231 }
232 h.setKindLocked(r, p, kind)
233 if p.Joined && p.Admitted {
234 return nil // idempotent
235 }
236 if p.Joined {
237 // Waiting. A guest waits on; a peer the tick promoted to member
238 // while the room was full may retry here, which is the retry
239 // §2.4 promises them.
240 if kind != KindMember && waiting {
241 return nil
242 }
243 if !h.admitLocked(r, p) {
244 h.sendLocked(p, Message{Type: TypeFull})
245 return ErrMeshFull
246 }
247 return nil
248 }
249 if kind == KindMember || !waiting {
250 if !h.admitLocked(r, p) {
251 h.sendLocked(p, Message{Type: TypeFull})
252 return ErrMeshFull
253 }
254 return nil
255 }
256 if waitingCountLocked(r) >= MaxWaiting {
257 h.sendLocked(p, Message{Type: TypeFull})
258 return ErrTooManyWaiting
259 }
260 p.Joined = true
261 p.JoinedAt = h.cfg.Now()
262 h.broadcastLocked(r, Message{Type: TypeKnock, From: p.Peer, Name: p.Name}, admittedMembers)
263 // Who is already looking, gathered here because it needs the lock and
264 // must not be re-read later: by the time the notification is sent the
265 // room may have changed, and telling someone who *was* present is a
266 // smaller fault than holding the mutex across a network call.
267 if h.cfg.Knocked != nil {
268 watching := make([]int64, 0, len(r.peers))
269 for _, other := range r.peers {
270 if other.Admitted && other.Kind == KindMember {
271 watching = append(watching, other.PersonID)
272 }
273 }
274 go h.cfg.Knocked(r.sqid, watching, p.Name)
275 }
276 return nil
277}
278
279// standing looks the peer up to find its person, then asks the app
280// without holding the lock, because it is a database read.
281func (h *Hub) standing(ctx context.Context, sqid, session string, peer PeerID) (Kind, bool, error) {
282 h.mu.Lock()
283 p, found := h.findLocked(sqid, session, peer)
284 var personID int64
285 if found {
286 personID = p.PersonID
287 }
288 h.mu.Unlock()
289 if !found {
290 return 0, false, ErrUnknownPeer
291 }
292 return h.cfg.StandingFor(ctx, sqid, personID)
293}
294
295// Admit: the actor must be admitted with current member kind; the target
296// waiting with entry authority now; the cap under the lock (spec §4.3).
297//
298// The actor's own authority is settled completely — found, current
299// standing applied, admitted, member — before the target is looked at
300// at all. The other order (found first in an earlier revision) let any
301// joined participation, admitted or not, learn from the shape of the
302// refusal alone whether a candidate peer id existed in the room, and
303// spent a StandingFor call on the target even when the actor had no
304// business asking (review Important 3).
305func (h *Hub) Admit(ctx context.Context, sqid, session string, actor, target PeerID) error {
306 actorKind, actorOK, err := h.standing(ctx, sqid, session, actor)
307 if err != nil {
308 return err
309 }
310
311 h.mu.Lock()
312 a, ok := h.findLocked(sqid, session, actor)
313 if !ok {
314 h.mu.Unlock()
315 return ErrUnknownPeer
316 }
317 r := h.rooms[sqid]
318 if !actorOK {
319 lost := h.standingLostLocked(r, a)
320 h.mu.Unlock()
321 if lost {
322 return ErrNotYours
323 }
324 // An admitted guest whose grant lapsed keeps the room but never
325 // gains the authority to let anyone else in.
326 return ErrNotMember
327 }
328 h.setKindLocked(r, a, actorKind)
329 if !a.Admitted {
330 h.mu.Unlock()
331 return ErrNotAdmitted
332 }
333 if a.Kind != KindMember {
334 h.mu.Unlock()
335 return ErrNotMember
336 }
337 // Only now, with the actor's authority settled, is the target looked
338 // at — first for its bare existence in this room, still under this
339 // same lock, before it costs a StandingFor call at all.
340 tp, found := r.peers[target]
341 if !found || !tp.Joined || tp.Admitted {
342 h.mu.Unlock()
343 return ErrNotWaiting
344 }
345 targetPerson := tp.PersonID
346 h.mu.Unlock()
347
348 targetKind, targetOK, err := h.cfg.StandingFor(ctx, sqid, targetPerson)
349 if err != nil {
350 return err
351 }
352
353 h.mu.Lock()
354 defer h.mu.Unlock()
355 // Both participations are re-found: either may have ended while the
356 // target's standing read was in flight.
357 a, ok = h.findLocked(sqid, session, actor)
358 if !ok {
359 return ErrUnknownPeer
360 }
361 r = h.rooms[sqid]
362 if !a.Admitted {
363 return ErrNotAdmitted
364 }
365 if a.Kind != KindMember {
366 return ErrNotMember
367 }
368 tp, ok = r.peers[target]
369 if !ok || !tp.Joined || tp.Admitted {
370 return ErrNotWaiting
371 }
372 if !targetOK {
373 // Through standingLostLocked, not endLocked: a loss of standing
374 // costs what the kind held earns, and there is exactly one place
375 // that decides which (spec §2.4). The target is always waiting
376 // here — the guard above refuses an admitted one — but it is not
377 // always a guest: the tick promotes a waiting guest to member
378 // and leaves it waiting when the mesh is full, and that one is
379 // owed reason "standing", not "expired".
380 h.standingLostLocked(r, tp)
381 return nil
382 }
383 h.setKindLocked(r, tp, targetKind)
384 if !h.admitLocked(r, tp) {
385 h.sendLocked(a, Message{Type: TypeFull})
386 }
387 return nil
388}
389
390// Leave ends a participation in any state at its own request. It is not
391// a tombstoned ending: a leave is not something to resume from.
392func (h *Hub) Leave(sqid, session string, peer PeerID) error {
393 h.mu.Lock()
394 defer h.mu.Unlock()
395 r, ok := h.rooms[sqid]
396 if !ok {
397 return ErrUnknownPeer
398 }
399 p, found := h.findLocked(sqid, session, peer)
400 if !found {
401 return ErrUnknownPeer
402 }
403 wasAdmitted, wasWaiting := h.endLocked(r, p, ReasonBuffer)
404 delete(h.tombstones, p.Peer)
405 h.announceDepartureLocked(r, p, wasAdmitted, wasWaiting)
406 return nil
407}
408
409// Remove is spec §3.5: the actor holds an admitted member participation
410// in this room (kind re-derived), the target is any other participation.
411func (h *Hub) Remove(ctx context.Context, sqid, session string, actor, target PeerID) error {
412 kind, ok, err := h.standing(ctx, sqid, session, actor)
413 if err != nil {
414 return err
415 }
416 h.mu.Lock()
417 defer h.mu.Unlock()
418 a, found := h.findLocked(sqid, session, actor)
419 if !found {
420 return ErrUnknownPeer
421 }
422 r := h.rooms[sqid]
423 if !ok {
424 if h.standingLostLocked(r, a) {
425 return ErrNotYours
426 }
427 return ErrNotMember // an admitted guest, grant lapsed: still not a member
428 }
429 h.setKindLocked(r, a, kind)
430 if !a.Admitted {
431 return ErrNotAdmitted
432 }
433 if a.Kind != KindMember {
434 return ErrNotMember
435 }
436 tp, found := r.peers[target]
437 if !found || tp.Peer == a.Peer {
438 return ErrUnknownPeer
439 }
440 wasAdmitted, wasWaiting := h.endLocked(r, tp, ReasonRemoved)
441 h.announceDepartureLocked(r, tp, wasAdmitted, wasWaiting)
442 return nil
443}
444
445// Takeover ends the session's live participation in the room with the
446// state-dependent departure (spec §4.2), leaving a replaced tombstone so
447// the displaced stream can be told why.
448func (h *Hub) Takeover(sqid, session string) (PeerID, error) {
449 h.mu.Lock()
450 defer h.mu.Unlock()
451 r, ok := h.rooms[sqid]
452 if !ok {
453 return "", ErrUnknownPeer
454 }
455 id, ok := h.bySession[sqid][session]
456 if !ok {
457 return "", ErrUnknownPeer
458 }
459 p, ok := r.peers[id]
460 if !ok {
461 delete(h.bySession[sqid], session)
462 return "", ErrUnknownPeer
463 }
464 wasAdmitted, wasWaiting := h.endLocked(r, p, ReasonReplaced)
465 h.announceDepartureLocked(r, p, wasAdmitted, wasWaiting)
466 return id, nil
467}
468
469// End is the meeting ending: lost{ended} to every stream, every
470// participation ended, and the room kept only as a headstone so a stream
471// racing the ending is refused (spec §2.4). A room nobody opened needs
472// no headstone: the handler has already read ended_at, and every later
473// entry reads it too.
474func (h *Hub) End(sqid string) {
475 h.mu.Lock()
476 defer h.mu.Unlock()
477 r, ok := h.rooms[sqid]
478 if !ok {
479 return
480 }
481 for _, p := range r.peers {
482 h.endLocked(r, p, ReasonEnded)
483 }
484 r.ended = true
485 delete(h.bySession, sqid)
486 // Nothing reaps rooms today, so a headstone lives as long as the
487 // process. If a future round ever does reap them, it must leave an
488 // ended room in place for at least TombstoneLife: the tombstones
489 // written just above are answered only through their own room
490 // (resumeRefusalLocked), and a room reaped ahead of them turns spec
491 // §4.5's terminal 410 "ended" into a 404, which sends a browser off
492 // to open a fresh stream on a meeting that is over.
493}
494
495// RefreshWaiting rechecks queued guests through the normal entry gate after
496// meeting settings change. Database callbacks must run outside the hub lock.
497func (h *Hub) RefreshWaiting(ctx context.Context, sqid string) error {
498 type waitingPeer struct {
499 session string
500 peer PeerID
501 }
502 h.mu.Lock()
503 var peers []waitingPeer
504 if room := h.rooms[sqid]; room != nil {
505 for _, p := range room.peers {
506 if p.Joined && !p.Admitted {
507 peers = append(peers, waitingPeer{p.Session, p.Peer})
508 }
509 }
510 }
511 h.mu.Unlock()
512 for _, p := range peers {
513 err := h.Join(ctx, sqid, p.session, p.peer)
514 if err != nil && err != ErrMeshFull && err != ErrUnknownPeer && err != ErrNotYours {
515 return err
516 }
517 }
518 return nil
519}
520