package api import ( "context" "net/http" "strconv" "strings" "sync" "time" "github.com/ponzischeme89/memby/server/internal/appupdate" ) // membyProtocolVersion changes only when the client/server wire contract is no longer // mutually compatible. App release versions remain independent and are handled by the // update policy. const membyProtocolVersion = 1 // updatePolicyCache keeps the policy in memory. It is read on every home request, and a // database round trip per home load to answer "nothing to say" would be wasteful. type updatePolicyCache struct { mu sync.RWMutex value appupdate.Policy } func (c *updatePolicyCache) get() appupdate.Policy { c.mu.RLock() defer c.mu.RUnlock() return c.value } func (c *updatePolicyCache) set(value appupdate.Policy) { c.mu.Lock() defer c.mu.Unlock() c.value = value } // LoadUpdatePolicy primes the cached policy. Called at boot and after every change. func (s *Server) LoadUpdatePolicy(ctx context.Context) error { policy, err := s.store.UpdatePolicy(ctx) if err != nil { return err } s.updatePolicy.set(policy) return nil } // WatchUpdatePolicy re-reads the policy periodically, so a change made directly in the // database is picked up without a restart. func (s *Server) WatchUpdatePolicy(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if err := s.LoadUpdatePolicy(ctx); err != nil { s.log.Warn("update policy refresh failed", "error", err) } } } } // clientVersion reads the version a TV reports. Absent means an older build that predates // the header — [appupdate.Decide] treats that as "say nothing". func clientVersion(r *http.Request) string { return strings.TrimSpace(r.Header.Get("X-Memby-Version")) } func clientProtocol(r *http.Request) string { return strings.TrimSpace(r.Header.Get("X-Memby-Protocol")) } func clientProtocolNumber(r *http.Request) int { reported, _ := strconv.Atoi(clientProtocol(r)) return reported } func compatibilityFor(r *http.Request) (bool, string) { reported, err := strconv.Atoi(clientProtocol(r)) if err != nil || reported != membyProtocolVersion { if clientProtocol(r) == "" { return false, "This Memby app is too old to verify compatibility with the server. Update the app." } return false, "Memby app/server mismatch: app protocol " + clientProtocol(r) + ", server protocol " + strconv.Itoa(membyProtocolVersion) + ". Update the app or server." } return true, "" } // handleUpdate answers the client's version check. // // Its own public endpoint rather than a field on /v1/home: update policy belongs to the // app build, not a viewer or login. The only client input is its build-version header and // the answer comes from memory, so checking it never reads or mutates a user session. func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) { decision := appupdate.Decide(s.updatePolicy.get(), clientVersion(r)) writeJSON(w, http.StatusOK, decision) }