package main import ( "crypto/rand" "encoding/hex" "hash/fnv" "sync" "sync/atomic" "time" ) const defaultPageSize = 50 // SessionState is the authoritative, server-side state of one search "session". // The browser never holds the dataset — only this small state lives here, and // the rendered projection is pushed to the client. This is the read model input. type SessionState struct { Name string // pseudonymous guest identity (for attributing writes) Query string Status string // "open" | "closed" | "all" Labels []string Author string // filter to one creator ("" = any) SortField string // "modified" | "created" | "relevance" SortDir string // "desc" | "asc" Limit int // size of the visible window (grows on "load more") Selected int // short_id of the open detail, 0 = none CurSynced bool // last Selected change originated from the client's own cursor (click/l) — don't echo $cur back (see pushProjection) Composing bool // true while the "New issue" composer is open Editing bool // true while editing the selected issue StatsOpen bool // true while the debug 📊 panel is open (pushes stats in-projection) // Command surfaces (server-authoritative UI, fat-morphed in — see command.go). PaletteOpen bool // ⌘K command palette open PaletteQuery string // palette input text (mirrors the $palette signal) PaletteIndex int // highlighted palette row (arrow-nav target) HelpOpen bool // ? keyboard-shortcut cheatsheet open } func newSessionState() SessionState { return SessionState{ Status: "open", SortField: "modified", SortDir: "desc", Limit: defaultPageSize, } } // toQuery projects session state into a read-side Query. func (s SessionState) toQuery() Query { return Query{ Text: s.Query, Status: s.Status, Labels: s.Labels, Author: s.Author, Sort: s.SortField, Dir: s.SortDir, Limit: s.Limit, } } // Session bundles the state with a set of subscribers (the long-lived SSE // streams). A command mutates state then calls notify(), which wakes every // stream so it can recompute and push a fresh projection. type Session struct { mu sync.Mutex state SessionState subs map[chan struct{}]struct{} vw viewer // auth identity of the connected stream, for presence (who's viewing) dirty atomic.Bool // set by writes/commands, cleared by the Hub frame ticker scrollReset atomic.Bool // a result-set change asks the next projection to scroll the list to top lastActive time.Time // last touched; idle-eviction clock starts when subs hit 0 } const ( writeRate = 0.5 // tokens/sec → sustained 1 write / 2s writeBurst = 3.0 ) // writeLimiter applies a token bucket per writer identity (the authenticated // login, or the dev-user name). It is deliberately NOT keyed by session: the sid // is client-chosen, so a per-session bucket would hand a fresh burst to anyone // who rotates sids. type writeLimiter struct { mu sync.Mutex buckets map[string]*bucket } type bucket struct { tokens float64 last time.Time // last refill } func newWriteLimiter() *writeLimiter { return &writeLimiter{buckets: make(map[string]*bucket)} } // allow spends one token from key's bucket. Returns false when the caller is // over their write budget. func (l *writeLimiter) allow(key string, now time.Time) bool { l.mu.Lock() defer l.mu.Unlock() b, ok := l.buckets[key] if !ok { // An absent bucket equals a full one, so occasionally drop fully refilled // buckets to keep the map proportional to recently active writers. if len(l.buckets) >= 1024 { for k, old := range l.buckets { if now.Sub(old.last).Seconds()*writeRate >= writeBurst { delete(l.buckets, k) } } } b = &bucket{tokens: writeBurst, last: now} l.buckets[key] = b } else { b.tokens += now.Sub(b.last).Seconds() * writeRate if b.tokens > writeBurst { b.tokens = writeBurst } b.last = now } if b.tokens >= 1 { b.tokens-- return true } return false } func newSession() *Session { return &Session{ state: newSessionState(), subs: make(map[chan struct{}]struct{}), } } // withState runs fn under the lock and returns a copy of the resulting state. func (s *Session) withState(fn func(*SessionState)) SessionState { s.mu.Lock() fn(&s.state) cp := s.state cp.Labels = append([]string(nil), s.state.Labels...) s.mu.Unlock() return cp } func (s *Session) snapshot() SessionState { return s.withState(func(*SessionState) {}) } // closeOverlays dismisses the command palette and help dialog. Called by every // concrete command (the cmd() wrapper) so picking an action from the palette // closes it; the palette's own query/nav endpoints and /cmd/key deliberately do // NOT close, which is how the overlays stay open while interacting with them. func (s *Session) closeOverlays() { s.withState(func(st *SessionState) { st.PaletteOpen = false st.HelpOpen = false st.PaletteIndex = 0 }) } func (s *Session) subscribe() chan struct{} { ch := make(chan struct{}, 1) s.mu.Lock() s.subs[ch] = struct{}{} s.lastActive = time.Now() s.mu.Unlock() return ch } func (s *Session) unsubscribe(ch chan struct{}) { s.mu.Lock() delete(s.subs, ch) s.lastActive = time.Now() // start the idle clock when a stream leaves s.mu.Unlock() } func (s *Session) touch() { s.mu.Lock() s.lastActive = time.Now() s.mu.Unlock() } // notify marks the session dirty. It does NOT push to streams directly — the // Hub's frame ticker does that, so many writes within a frame collapse into one // projection. Lock-free and O(1), keeping fan-out work off the write path. func (s *Session) notify() { s.dirty.Store(true) } // wake signals all subscribed streams to re-project (non-blocking, coalescing). // Called by the Hub frame ticker, never from the write path directly. func (s *Session) wake() { s.mu.Lock() for ch := range s.subs { select { case ch <- struct{}{}: default: // a refresh is already pending for this stream (backpressure) } } s.mu.Unlock() } // Hub stores sessions keyed by an opaque cookie id and flushes dirty sessions on // a fixed frame cadence. type Hub struct { mu sync.Mutex sessions map[string]*Session frame time.Duration ttl time.Duration // evict sessions idle (no subscribers) this long max int // cap on stored sessions (sids are client-chosen — see getOrCreate) } // maxSessions caps the hub. The sid is client-chosen, so without a bound an // unauthenticated loop of random sids would accumulate sessions until OOM — // maxConns only limits SSE streams, not sessions. ~50k idle sessions is a few // tens of MB, far beyond any legitimate fleet of tabs. const maxSessions = 50000 // newHub starts the frame ticker and idle-eviction sweeper. frame is the // batching window; ttl is how long a session with no live streams survives // before being reclaimed (covers SSE reconnect blips). func newHub(frame, ttl time.Duration) *Hub { h := &Hub{sessions: make(map[string]*Session), frame: frame, ttl: ttl, max: maxSessions} go h.runFrames() go h.runSweeper() return h } // runSweeper reclaims sessions that have had no subscribers for longer than ttl. func (h *Hub) runSweeper() { interval := h.ttl / 4 if interval < 10*time.Second { interval = 10 * time.Second } t := time.NewTicker(interval) defer t.Stop() for range t.C { h.sweep(time.Now()) } } // sweep drops every session that has had no subscribers for longer than ttl, // as of now. func (h *Hub) sweep(now time.Time) { h.mu.Lock() for id, s := range h.sessions { s.mu.Lock() idle := len(s.subs) == 0 && now.Sub(s.lastActive) > h.ttl s.mu.Unlock() if idle { delete(h.sessions, id) } } h.mu.Unlock() } // runFrames wakes dirty sessions once per frame. O(dirty) work per tick. func (h *Hub) runFrames() { t := time.NewTicker(h.frame) defer t.Stop() for range t.C { h.mu.Lock() sessions := make([]*Session, 0, len(h.sessions)) for _, s := range h.sessions { sessions = append(sessions, s) } h.mu.Unlock() for _, s := range sessions { if s.dirty.Swap(false) { s.wake() } } } } func (h *Hub) count() int { h.mu.Lock() defer h.mu.Unlock() return len(h.sessions) } // getOrCreate returns the session for id, creating it if absent. At the max cap // the oldest idle session is evicted to make room; if every session has a live // stream the new one is returned unregistered — it serves this request but is // not retained, so a hostile sid churn cannot grow the map. func (h *Hub) getOrCreate(id string) *Session { h.mu.Lock() defer h.mu.Unlock() if s, ok := h.sessions[id]; ok { s.touch() return s } s := newSession() s.state.Name = guestName(id) s.lastActive = time.Now() if len(h.sessions) < h.max || h.evictOldestIdle() { h.sessions[id] = s } return s } // peek returns the session for id only if it already exists. The index SSR // restore path uses this instead of getOrCreate: the dbugs_tab cookie is // client-chosen input, and a fabricated or stale value must not allocate hub // entries on unauthenticated page loads. func (h *Hub) peek(id string) (*Session, bool) { h.mu.Lock() s, ok := h.sessions[id] h.mu.Unlock() if ok { s.touch() } return s, ok } // evictOldestIdle drops the subscriber-less session with the oldest lastActive, // reporting whether one was found. Caller must hold h.mu. func (h *Hub) evictOldestIdle() bool { var victim string var oldest time.Time for id, s := range h.sessions { s.mu.Lock() idle := len(s.subs) == 0 last := s.lastActive s.mu.Unlock() if idle && (victim == "" || last.Before(oldest)) { victim, oldest = id, last } } if victim == "" { return false } delete(h.sessions, victim) return true } // notifyAll marks every session dirty — used after a write that can change any // view (create/edit). Just atomic stores; waking + re-projection is amortized by // the frame ticker, and per-connection dedup skips views that didn't change. func (h *Hub) notifyAll() { h.mu.Lock() defer h.mu.Unlock() for _, s := range h.sessions { s.dirty.Store(true) } } // notifyStatsOpen marks sessions with the debug panel open dirty, so the 1s // sampler can refresh their stats through the single projection pipe (no // dedicated stats connection). Only dev sessions opt in, so this is ~free. func (h *Hub) notifyStatsOpen() { h.mu.Lock() sessions := make([]*Session, 0, len(h.sessions)) for _, s := range h.sessions { sessions = append(sessions, s) } h.mu.Unlock() for _, s := range sessions { s.mu.Lock() open := s.state.StatsOpen s.mu.Unlock() if open { s.dirty.Store(true) } } } // notifyDetailWatchers marks only sessions currently viewing the given issue — // a scoped push for comments, which change one issue's detail and nothing else // (no list/facets change), so non-watchers needn't wake at all. func (h *Hub) notifyDetailWatchers(shortID int) { h.mu.Lock() sessions := make([]*Session, 0, len(h.sessions)) for _, s := range h.sessions { sessions = append(sessions, s) } h.mu.Unlock() for _, s := range sessions { s.mu.Lock() watching := s.state.Selected == shortID s.mu.Unlock() if watching { s.dirty.Store(true) } } } // setViewer records the connected stream's auth identity for presence. func (s *Session) setViewer(v viewer) { s.mu.Lock() s.vw = v s.mu.Unlock() } // viewersOf returns the distinct people currently viewing the given issue. A // session counts only while it has a live stream (subs > 0), so closed/hidden // tabs drop off. Deduped by identity — signed-in Login, else the guest pseudonym // — so one person across several tabs reads as one. O(sessions); fine at showcase // scale, swap for an incremental presence index if it ever shows up in a profile. func (h *Hub) viewersOf(shortID int) []presence { h.mu.Lock() sessions := make([]*Session, 0, len(h.sessions)) for _, s := range h.sessions { sessions = append(sessions, s) } h.mu.Unlock() seen := map[string]bool{} var out []presence for _, s := range sessions { s.mu.Lock() viewing := s.state.Selected == shortID && len(s.subs) > 0 login, avatar, name := s.vw.Login, s.vw.Avatar, s.state.Name s.mu.Unlock() if !viewing { continue } label := login if label == "" { label = name // guest } if label == "" || seen[label] { continue } seen[label] = true out = append(out, presence{Avatar: avatar, Label: label, Initials: initials(label), Color: avatarColor(label)}) } return out } // avatarColor maps an identity to a stable chip color from a small palette. var avatarColors = []string{"#5b8cff", "#46a758", "#7c66dc", "#e5484d", "#f5a623", "#12a5b5", "#d6409f", "#8b6f47"} func avatarColor(s string) string { h := fnv.New32a() h.Write([]byte(s)) return avatarColors[h.Sum32()%uint32(len(avatarColors))] } var ( guestAdjs = []string{"swift", "calm", "bright", "quiet", "brave", "keen", "warm", "lucky", "bold", "sly", "merry", "wise"} guestAnimals = []string{"otter", "lynx", "heron", "fox", "wren", "moth", "ibex", "koi", "newt", "raven", "shrew", "vole"} ) // guestName derives a stable pseudonym from the session id. func guestName(id string) string { h := fnv.New32a() h.Write([]byte(id)) n := h.Sum32() return guestAdjs[n%uint32(len(guestAdjs))] + "-" + guestAnimals[(n/uint32(len(guestAdjs)))%uint32(len(guestAnimals))] } func newSessionID() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) }