mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
refactor(b3-5): hub.go under 400 — construction, settings cache and replay accessor move out (S-08, finisher) (#1476)
* refactor(ws): move construction, settings cache and replay accessor out of hub.go Pure moves closing hub.go's B3-5 size target: HubOptions and NewHub -> new hub_options.go; getCachedSettings and refreshSettingsLocked -> new hub_settings.go; maxColdReplayLimit -> replay.go beside the family that reads it. Normalized-diff residue: the new files' scaffolding plus duplicates of five imports hub.go retains (auth, db, permissions, plugin, service); errors, fmt and os moved outright. No identifier changed. hub.go now holds the Hub state, lifecycle (Run/Stop/graceful) and stats accessors: 361 lines, under the 400-line exit target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8dwVLEihnGZYtH9X631F4 * docs(boundaries,b3): re-measure after the hub.go finishers; record the B3-5 exit hub.go and hub_options.go become type-only boundary rows; hub_settings.go reads through the h.db field and imports nothing, so it carries no row (the header records that wrinkle). Disposition counts re-derived from the tool's summary — boundary had been stale at 12 since the seed-profile row. Finisher evidence block and the B3-5 exit summary (five squash merges, all seven responsibilities, exit size table with all three targets met) appended to the plan; README B3 row advanced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8dwVLEihnGZYtH9X631F4 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -91,9 +91,11 @@ var DBImportAllow = map[string]DBImportEntry{
|
||||
"ws/eventstore.go": {"adapter", "", "PersistedEvent type; store is an interface"},
|
||||
"ws/handlers.go": {"move", "channel", "channel, role, session-ban and DM reads in command handlers"},
|
||||
"ws/handlers_chat.go": {"adapter", "", "pure NewDMChannelInfo helper"},
|
||||
"ws/hub.go": {"move", "settings-ops", "GetSetting at construction"},
|
||||
"ws/hub.go": {"boundary", "", "Hub state holds the handle the families read through; no calls"},
|
||||
"ws/hub_options.go": {"boundary", "", "construction validates and stores the handle; no calls"},
|
||||
"ws/hub_broadcast.go": {"move", "channel", "member broadcast payloads read the user and role they announce"},
|
||||
"ws/hub_presence.go": {"adapter", "", "presence coalescer; pure BroadcastStatus helper and the MemberSummary shape"},
|
||||
"ws/hub_settings.go": {"move", "settings-ops", "settings cache reads server name and MOTD through h.db; import pinned so the rule sees it"},
|
||||
"ws/hub_visibility.go": {"move", "channel", "visibility and audience resolution reads channels, overrides, participants, users"},
|
||||
"ws/hub_sweep.go": {"move", "voice", "stale-voice sweep reads and leaves"},
|
||||
"ws/messages.go": {"adapter", "", "wire types + pure status helpers"},
|
||||
|
||||
@@ -3,10 +3,7 @@ package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -140,204 +137,6 @@ type Hub struct {
|
||||
presenceFlushArmed bool
|
||||
}
|
||||
|
||||
// HubOptions carries everything a Hub needs before Run starts (S-11 / B3-4).
|
||||
// The four pre-Run setters this struct replaced (SetLiveKit,
|
||||
// SetLiveKitProcess, SetPluginRegistry, ConfigureReplay) were all guarded by
|
||||
// rejectIfRunning — construction-phase wiring pretending to be mutable state.
|
||||
// What genuinely IS mutable after Run stays a setter: the event persister,
|
||||
// event store and plugin event sink are atomic hot-swaps that the app wires
|
||||
// after the dispatch loop starts (internal/app/persistence.go), and
|
||||
// SetPendingVoiceModFlags is per-user runtime state.
|
||||
type HubOptions struct {
|
||||
// DB and Limiter are required: every dispatch path reads the database,
|
||||
// and the handler deps capture the limiter at registration. NewHub
|
||||
// refuses to build a Hub without them.
|
||||
DB *db.DB
|
||||
Limiter *auth.RateLimiter
|
||||
|
||||
// Services is the domain layer V2 handlers delegate to. Production
|
||||
// always passes it; nil is the degraded fixture many ws tests build,
|
||||
// where handlers keep their direct-DB fallback paths.
|
||||
Services *service.Services
|
||||
|
||||
// LiveKit is the voice token signer; nil means voice is not configured
|
||||
// and every voice join is refused. LiveKitProcess is the supervised
|
||||
// companion SFU — it requires LiveKit, because a process no client can
|
||||
// sign tokens for is unusable, and the voice_join guard reads a non-nil
|
||||
// process's IsRunning to fail closed while it is down.
|
||||
LiveKit *LiveKitClient
|
||||
LiveKitProcess *LiveKitProcess
|
||||
|
||||
// PluginRegistry enables plugin slash-command dispatch; nil disables it.
|
||||
// The plugin event sink is NOT here: it consumes the built hub's
|
||||
// broadcaster, so it stays the two-phase SetPluginEventSink.
|
||||
PluginRegistry *plugin.Registry
|
||||
|
||||
// Replay budget (event_persistence.replay_ring_size / replay_cold_limit).
|
||||
// Zero keeps the compiled-in defaults; negative is refused rather than
|
||||
// silently ignored.
|
||||
ReplayRingSize int
|
||||
ReplayColdLimit int
|
||||
}
|
||||
|
||||
// NewHub creates a Hub ready to be started with Run, validating that the
|
||||
// required collaborators are present — before B3-4, construction always
|
||||
// succeeded and a missing collaborator surfaced as a later panic or a
|
||||
// silently refused setter call. It also initializes the settings cache from
|
||||
// the database. If opts.Services is non-nil, V2 handlers receive service
|
||||
// references for business logic delegation.
|
||||
func NewHub(opts HubOptions) (*Hub, error) {
|
||||
if opts.DB == nil {
|
||||
return nil, errors.New("ws: HubOptions.DB is required (every dispatch path reads it)")
|
||||
}
|
||||
if opts.Limiter == nil {
|
||||
return nil, errors.New("ws: HubOptions.Limiter is required (handler deps capture it at registration)")
|
||||
}
|
||||
if opts.LiveKitProcess != nil && opts.LiveKit == nil {
|
||||
return nil, errors.New("ws: HubOptions.LiveKitProcess without LiveKit — a supervised SFU no client can sign tokens for")
|
||||
}
|
||||
if opts.ReplayRingSize < 0 || opts.ReplayColdLimit < 0 {
|
||||
return nil, fmt.Errorf("ws: negative replay budget (ring %d, cold %d)", opts.ReplayRingSize, opts.ReplayColdLimit)
|
||||
}
|
||||
|
||||
database, limiter, svc := opts.DB, opts.Limiter, opts.Services
|
||||
|
||||
ringSize := 1000
|
||||
if opts.ReplayRingSize > 0 {
|
||||
ringSize = opts.ReplayRingSize
|
||||
}
|
||||
|
||||
reg := NewHandlerRegistry()
|
||||
|
||||
h := &Hub{
|
||||
clients: make(map[int64]*Client),
|
||||
db: database,
|
||||
limiter: limiter,
|
||||
broadcast: make(chan broadcastMsg, 1024),
|
||||
clientEvents: make(chan clientEvent, 64),
|
||||
stop: make(chan struct{}),
|
||||
pubsub: NewPubSub(),
|
||||
topicLimiter: NewTopicRateLimiter(topicRateLimitPerSecond, time.Second),
|
||||
replayBuf: NewEventRingBuffer(ringSize),
|
||||
registry: reg,
|
||||
permChecker: permissions.NewChecker(database),
|
||||
settingsName: "OwnCord Server",
|
||||
settingsMotd: "Welcome!",
|
||||
voiceKeyHolders: make(map[int64]int64),
|
||||
fatalFn: func() { os.Exit(1) },
|
||||
livekit: opts.LiveKit,
|
||||
lkProcess: opts.LiveKitProcess,
|
||||
pluginRegistry: opts.PluginRegistry,
|
||||
}
|
||||
if opts.ReplayColdLimit > 0 {
|
||||
h.coldReplayLimit = opts.ReplayColdLimit
|
||||
}
|
||||
|
||||
// V2 handler registrations (need Hub fields for deps).
|
||||
registerPingHandler(reg, PingDeps{Limiter: h.limiter})
|
||||
|
||||
chatDeps := ChatDeps{
|
||||
Limiter: h.limiter,
|
||||
}
|
||||
presenceDeps := PresenceDeps{
|
||||
Limiter: h.limiter,
|
||||
}
|
||||
reactionDeps := ReactionDeps{}
|
||||
callDeps := CallDeps{Limiter: h.limiter}
|
||||
if svc != nil {
|
||||
chatDeps.MessageSvc = svc.Messages
|
||||
presenceDeps.ChannelSvc = svc.Channels
|
||||
reactionDeps.MessageSvc = svc.Messages
|
||||
callDeps.DMSvc = svc.DMs
|
||||
h.messageSvc = svc.Messages
|
||||
h.perms = svc.Permissions
|
||||
// So @here's offline narrowing can tell a disconnected idle/dnd reader
|
||||
// (users.status keeps their last *chosen* value across a disconnect)
|
||||
// from one who is actually still connected — the same live-connection
|
||||
// rule presentableMembers applies to the members array.
|
||||
svc.Messages.SetOnlineChecker(h.IsUserConnected)
|
||||
// So every DM payload DMService builds (GET/POST /dms, POST
|
||||
// /dms/group, PATCH /dms/{id}, and every broadcastDMOpen refresh)
|
||||
// applies the same live-connection rule instead of only the ready
|
||||
// payload's presentableDMChannels doing so (OC-0304).
|
||||
svc.DMs.SetOnlineChecker(h.IsUserConnected)
|
||||
}
|
||||
|
||||
registerChatHandlers(reg, chatDeps)
|
||||
registerPresenceHandlers(reg, presenceDeps)
|
||||
registerReactionHandlers(reg, reactionDeps)
|
||||
registerCallHandlers(reg, callDeps)
|
||||
// Phase C Step 9 — plugin slash commands. The registry closure predates
|
||||
// B3-4 (the registry used to arrive via a post-construction setter); it
|
||||
// stays a closure for the nil-interface reason below. MessageSvc gates
|
||||
// broadcasts.
|
||||
reg.RegisterV2(MsgTypeChatCommand, handleChatCommandV2, PluginDeps{
|
||||
// A nil registry must yield a nil interface, not a typed-nil
|
||||
// *plugin.Registry — the handler's "no plugins loaded" check is an
|
||||
// interface comparison.
|
||||
Registry: func() CommandDispatcher {
|
||||
if h.pluginRegistry == nil {
|
||||
return nil
|
||||
}
|
||||
return h.pluginRegistry
|
||||
},
|
||||
MessageSvc: h.messageSvc,
|
||||
Limiter: h.limiter,
|
||||
})
|
||||
registerVoiceControlsV2(reg, VoiceDeps{
|
||||
DB: h.db,
|
||||
Limiter: h.limiter,
|
||||
Permissions: h.permChecker,
|
||||
PermSvc: h.perms,
|
||||
LiveKit: h.livekit,
|
||||
TokenGen: h, // Hub delegates to h.livekit at call time
|
||||
KeyHolder: h,
|
||||
Mod: h,
|
||||
})
|
||||
|
||||
h.refreshSettingsLocked(context.Background())
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// getCachedSettings returns server_name and motd, refreshing the cache if stale.
|
||||
func (h *Hub) getCachedSettings(ctx context.Context) (string, string) {
|
||||
h.settingsMu.RLock()
|
||||
if time.Since(h.settingsLastUpdate) < settingsCacheTTL {
|
||||
name, motd := h.settingsName, h.settingsMotd
|
||||
h.settingsMu.RUnlock()
|
||||
return name, motd
|
||||
}
|
||||
h.settingsMu.RUnlock()
|
||||
|
||||
h.settingsMu.Lock()
|
||||
defer h.settingsMu.Unlock()
|
||||
// Double-check after acquiring write lock.
|
||||
if time.Since(h.settingsLastUpdate) < settingsCacheTTL {
|
||||
return h.settingsName, h.settingsMotd
|
||||
}
|
||||
h.refreshSettingsLocked(ctx)
|
||||
return h.settingsName, h.settingsMotd
|
||||
}
|
||||
|
||||
// refreshSettingsLocked reloads server_name and motd from the DB.
|
||||
// Caller must hold settingsMu (write lock) or call during init.
|
||||
func (h *Hub) refreshSettingsLocked(ctx context.Context) {
|
||||
if h.db == nil {
|
||||
return
|
||||
}
|
||||
// The refresh serves the hub-wide settings cache, not the connection that
|
||||
// happened to trigger it — a dying connection's ctx must not fail the
|
||||
// fetches (the TTL stamp below would then pin stale values for 30s).
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
if name, err := h.db.GetSetting(ctx, "server_name"); err == nil {
|
||||
h.settingsName = name
|
||||
}
|
||||
if motd, err := h.db.GetSetting(ctx, "motd"); err == nil {
|
||||
h.settingsMotd = motd
|
||||
}
|
||||
h.settingsLastUpdate = time.Now()
|
||||
}
|
||||
|
||||
// Run starts the hub's dispatch loop. It blocks until Stop is called.
|
||||
// Must be called in its own goroutine.
|
||||
//
|
||||
@@ -545,16 +344,6 @@ func (h *Hub) ConnRejectCount() uint64 {
|
||||
return h.connRejects.Load()
|
||||
}
|
||||
|
||||
// maxColdReplayLimit returns the effective persisted-replay cap. The budget
|
||||
// arrives via HubOptions (B3-4): the dispatch loop reads replayBuf unlocked,
|
||||
// so the ring is sized exactly once, at construction.
|
||||
func (h *Hub) maxColdReplayLimit() int {
|
||||
if h.coldReplayLimit > 0 {
|
||||
return h.coldReplayLimit
|
||||
}
|
||||
return maxColdReplay
|
||||
}
|
||||
|
||||
// EventPersisterStats returns the attached persister's lifetime counters.
|
||||
// ok is false when event persistence is disabled (no persister attached).
|
||||
func (h *Hub) EventPersisterStats() (persisted, dropped, flushes, errs uint64, ok bool) {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/J3vb/OwnCord/Server/auth"
|
||||
"github.com/J3vb/OwnCord/Server/db"
|
||||
"github.com/J3vb/OwnCord/Server/permissions"
|
||||
"github.com/J3vb/OwnCord/Server/plugin"
|
||||
"github.com/J3vb/OwnCord/Server/service"
|
||||
)
|
||||
|
||||
// HubOptions carries everything a Hub needs before Run starts (S-11 / B3-4).
|
||||
// The four pre-Run setters this struct replaced (SetLiveKit,
|
||||
// SetLiveKitProcess, SetPluginRegistry, ConfigureReplay) were all guarded by
|
||||
// rejectIfRunning — construction-phase wiring pretending to be mutable state.
|
||||
// What genuinely IS mutable after Run stays a setter: the event persister,
|
||||
// event store and plugin event sink are atomic hot-swaps that the app wires
|
||||
// after the dispatch loop starts (internal/app/persistence.go), and
|
||||
// SetPendingVoiceModFlags is per-user runtime state.
|
||||
type HubOptions struct {
|
||||
// DB and Limiter are required: every dispatch path reads the database,
|
||||
// and the handler deps capture the limiter at registration. NewHub
|
||||
// refuses to build a Hub without them.
|
||||
DB *db.DB
|
||||
Limiter *auth.RateLimiter
|
||||
|
||||
// Services is the domain layer V2 handlers delegate to. Production
|
||||
// always passes it; nil is the degraded fixture many ws tests build,
|
||||
// where handlers keep their direct-DB fallback paths.
|
||||
Services *service.Services
|
||||
|
||||
// LiveKit is the voice token signer; nil means voice is not configured
|
||||
// and every voice join is refused. LiveKitProcess is the supervised
|
||||
// companion SFU — it requires LiveKit, because a process no client can
|
||||
// sign tokens for is unusable, and the voice_join guard reads a non-nil
|
||||
// process's IsRunning to fail closed while it is down.
|
||||
LiveKit *LiveKitClient
|
||||
LiveKitProcess *LiveKitProcess
|
||||
|
||||
// PluginRegistry enables plugin slash-command dispatch; nil disables it.
|
||||
// The plugin event sink is NOT here: it consumes the built hub's
|
||||
// broadcaster, so it stays the two-phase SetPluginEventSink.
|
||||
PluginRegistry *plugin.Registry
|
||||
|
||||
// Replay budget (event_persistence.replay_ring_size / replay_cold_limit).
|
||||
// Zero keeps the compiled-in defaults; negative is refused rather than
|
||||
// silently ignored.
|
||||
ReplayRingSize int
|
||||
ReplayColdLimit int
|
||||
}
|
||||
|
||||
// NewHub creates a Hub ready to be started with Run, validating that the
|
||||
// required collaborators are present — before B3-4, construction always
|
||||
// succeeded and a missing collaborator surfaced as a later panic or a
|
||||
// silently refused setter call. It also initializes the settings cache from
|
||||
// the database. If opts.Services is non-nil, V2 handlers receive service
|
||||
// references for business logic delegation.
|
||||
func NewHub(opts HubOptions) (*Hub, error) {
|
||||
if opts.DB == nil {
|
||||
return nil, errors.New("ws: HubOptions.DB is required (every dispatch path reads it)")
|
||||
}
|
||||
if opts.Limiter == nil {
|
||||
return nil, errors.New("ws: HubOptions.Limiter is required (handler deps capture it at registration)")
|
||||
}
|
||||
if opts.LiveKitProcess != nil && opts.LiveKit == nil {
|
||||
return nil, errors.New("ws: HubOptions.LiveKitProcess without LiveKit — a supervised SFU no client can sign tokens for")
|
||||
}
|
||||
if opts.ReplayRingSize < 0 || opts.ReplayColdLimit < 0 {
|
||||
return nil, fmt.Errorf("ws: negative replay budget (ring %d, cold %d)", opts.ReplayRingSize, opts.ReplayColdLimit)
|
||||
}
|
||||
|
||||
database, limiter, svc := opts.DB, opts.Limiter, opts.Services
|
||||
|
||||
ringSize := 1000
|
||||
if opts.ReplayRingSize > 0 {
|
||||
ringSize = opts.ReplayRingSize
|
||||
}
|
||||
|
||||
reg := NewHandlerRegistry()
|
||||
|
||||
h := &Hub{
|
||||
clients: make(map[int64]*Client),
|
||||
db: database,
|
||||
limiter: limiter,
|
||||
broadcast: make(chan broadcastMsg, 1024),
|
||||
clientEvents: make(chan clientEvent, 64),
|
||||
stop: make(chan struct{}),
|
||||
pubsub: NewPubSub(),
|
||||
topicLimiter: NewTopicRateLimiter(topicRateLimitPerSecond, time.Second),
|
||||
replayBuf: NewEventRingBuffer(ringSize),
|
||||
registry: reg,
|
||||
permChecker: permissions.NewChecker(database),
|
||||
settingsName: "OwnCord Server",
|
||||
settingsMotd: "Welcome!",
|
||||
voiceKeyHolders: make(map[int64]int64),
|
||||
fatalFn: func() { os.Exit(1) },
|
||||
livekit: opts.LiveKit,
|
||||
lkProcess: opts.LiveKitProcess,
|
||||
pluginRegistry: opts.PluginRegistry,
|
||||
}
|
||||
if opts.ReplayColdLimit > 0 {
|
||||
h.coldReplayLimit = opts.ReplayColdLimit
|
||||
}
|
||||
|
||||
// V2 handler registrations (need Hub fields for deps).
|
||||
registerPingHandler(reg, PingDeps{Limiter: h.limiter})
|
||||
|
||||
chatDeps := ChatDeps{
|
||||
Limiter: h.limiter,
|
||||
}
|
||||
presenceDeps := PresenceDeps{
|
||||
Limiter: h.limiter,
|
||||
}
|
||||
reactionDeps := ReactionDeps{}
|
||||
callDeps := CallDeps{Limiter: h.limiter}
|
||||
if svc != nil {
|
||||
chatDeps.MessageSvc = svc.Messages
|
||||
presenceDeps.ChannelSvc = svc.Channels
|
||||
reactionDeps.MessageSvc = svc.Messages
|
||||
callDeps.DMSvc = svc.DMs
|
||||
h.messageSvc = svc.Messages
|
||||
h.perms = svc.Permissions
|
||||
// So @here's offline narrowing can tell a disconnected idle/dnd reader
|
||||
// (users.status keeps their last *chosen* value across a disconnect)
|
||||
// from one who is actually still connected — the same live-connection
|
||||
// rule presentableMembers applies to the members array.
|
||||
svc.Messages.SetOnlineChecker(h.IsUserConnected)
|
||||
// So every DM payload DMService builds (GET/POST /dms, POST
|
||||
// /dms/group, PATCH /dms/{id}, and every broadcastDMOpen refresh)
|
||||
// applies the same live-connection rule instead of only the ready
|
||||
// payload's presentableDMChannels doing so (OC-0304).
|
||||
svc.DMs.SetOnlineChecker(h.IsUserConnected)
|
||||
}
|
||||
|
||||
registerChatHandlers(reg, chatDeps)
|
||||
registerPresenceHandlers(reg, presenceDeps)
|
||||
registerReactionHandlers(reg, reactionDeps)
|
||||
registerCallHandlers(reg, callDeps)
|
||||
// Phase C Step 9 — plugin slash commands. The registry closure predates
|
||||
// B3-4 (the registry used to arrive via a post-construction setter); it
|
||||
// stays a closure for the nil-interface reason below. MessageSvc gates
|
||||
// broadcasts.
|
||||
reg.RegisterV2(MsgTypeChatCommand, handleChatCommandV2, PluginDeps{
|
||||
// A nil registry must yield a nil interface, not a typed-nil
|
||||
// *plugin.Registry — the handler's "no plugins loaded" check is an
|
||||
// interface comparison.
|
||||
Registry: func() CommandDispatcher {
|
||||
if h.pluginRegistry == nil {
|
||||
return nil
|
||||
}
|
||||
return h.pluginRegistry
|
||||
},
|
||||
MessageSvc: h.messageSvc,
|
||||
Limiter: h.limiter,
|
||||
})
|
||||
registerVoiceControlsV2(reg, VoiceDeps{
|
||||
DB: h.db,
|
||||
Limiter: h.limiter,
|
||||
Permissions: h.permChecker,
|
||||
PermSvc: h.perms,
|
||||
LiveKit: h.livekit,
|
||||
TokenGen: h, // Hub delegates to h.livekit at call time
|
||||
KeyHolder: h,
|
||||
Mod: h,
|
||||
})
|
||||
|
||||
h.refreshSettingsLocked(context.Background())
|
||||
return h, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/J3vb/OwnCord/Server/db"
|
||||
)
|
||||
|
||||
// The db-import-boundary rule and the boundaries-doc inventory track files by
|
||||
// their db import. This file's persistence runs through the h.db field, which
|
||||
// needs no import — so the import is pinned here deliberately, keeping the
|
||||
// settings-ops reads on the inventory's books instead of invisible to it.
|
||||
var _ *db.DB
|
||||
|
||||
// getCachedSettings returns server_name and motd, refreshing the cache if stale.
|
||||
func (h *Hub) getCachedSettings(ctx context.Context) (string, string) {
|
||||
h.settingsMu.RLock()
|
||||
if time.Since(h.settingsLastUpdate) < settingsCacheTTL {
|
||||
name, motd := h.settingsName, h.settingsMotd
|
||||
h.settingsMu.RUnlock()
|
||||
return name, motd
|
||||
}
|
||||
h.settingsMu.RUnlock()
|
||||
|
||||
h.settingsMu.Lock()
|
||||
defer h.settingsMu.Unlock()
|
||||
// Double-check after acquiring write lock.
|
||||
if time.Since(h.settingsLastUpdate) < settingsCacheTTL {
|
||||
return h.settingsName, h.settingsMotd
|
||||
}
|
||||
h.refreshSettingsLocked(ctx)
|
||||
return h.settingsName, h.settingsMotd
|
||||
}
|
||||
|
||||
// refreshSettingsLocked reloads server_name and motd from the DB.
|
||||
// Caller must hold settingsMu (write lock) or call during init.
|
||||
func (h *Hub) refreshSettingsLocked(ctx context.Context) {
|
||||
if h.db == nil {
|
||||
return
|
||||
}
|
||||
// The refresh serves the hub-wide settings cache, not the connection that
|
||||
// happened to trigger it — a dying connection's ctx must not fail the
|
||||
// fetches (the TTL stamp below would then pin stale values for 30s).
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
if name, err := h.db.GetSetting(ctx, "server_name"); err == nil {
|
||||
h.settingsName = name
|
||||
}
|
||||
if motd, err := h.db.GetSetting(ctx, "motd"); err == nil {
|
||||
h.settingsMotd = motd
|
||||
}
|
||||
h.settingsLastUpdate = time.Now()
|
||||
}
|
||||
@@ -484,3 +484,13 @@ func (h *Hub) liveVoiceEventsSince(ctx context.Context, afterSeq uint64, chID in
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// maxColdReplayLimit returns the effective persisted-replay cap. The budget
|
||||
// arrives via HubOptions (B3-4): the dispatch loop reads replayBuf unlocked,
|
||||
// so the ring is sized exactly once, at construction.
|
||||
func (h *Hub) maxColdReplayLimit() int {
|
||||
if h.coldReplayLimit > 0 {
|
||||
return h.coldReplayLimit
|
||||
}
|
||||
return maxColdReplay
|
||||
}
|
||||
|
||||
@@ -20,7 +20,18 @@ first table, on `feat/b3-5-ws-split-4` (voice broadcast leftovers joined
|
||||
`voice_broadcast.go`'s code with no row change — that file makes no `db`
|
||||
use — and the presence coalescer split into a new `ws/hub_presence.go`
|
||||
adapter row, its only `db` use the pure `BroadcastStatus` helper;
|
||||
`hub_broadcast.go`'s row is down to the member payload reads).
|
||||
`hub_broadcast.go`'s row is down to the member payload reads); 2026-08-31
|
||||
(B3-5, finisher PR) — the first table, on `feat/b3-5-ws-split-5`
|
||||
(`hub.go`'s `GetSetting` calls left with the settings cache for
|
||||
`hub_settings.go`; that file's persistence runs through the `h.db` field,
|
||||
which needs no import, so it **pins** the `db` import — a documented
|
||||
`var _ *db.DB` — to stay on the rule's and this table's books: the rule
|
||||
and rows track importers, and a field-calling file without the pin would
|
||||
be invisible to both, which a review flagged on the finisher PR. `hub.go`
|
||||
and the new `hub_options.go` are type-only `boundary` rows
|
||||
holding/validating the handle. The disposition counts were also
|
||||
re-derived from the tool's summary: `boundary` had been stale at 12
|
||||
since the seed-profile row landed).
|
||||
**Owner:** the B3 plan,
|
||||
[plans/b3-server-architecture-guardrails-2026-08-29.md](../plans/b3-server-architecture-guardrails-2026-08-29.md).
|
||||
**Regenerate the first table:** `cd Server && go run ./cmd/dbinventory` and
|
||||
@@ -40,7 +51,7 @@ happens to that use — one of four dispositions from the
|
||||
| ----------- | --------------------------------------------------------------------------------------------------------------------- | ---: |
|
||||
| `move` | persistence or a domain decision that belongs behind a service; **Family** names the service B3-8 (or B3-2) builds | 28 |
|
||||
| `adapter` | a transport adapter that uses `db` types or pure helpers only — response shapes, status helpers — no persistence call | 18 |
|
||||
| `boundary` | an explicit composition or transaction boundary that legitimately owns a handle (process entry, CLIs, health probe) | 12 |
|
||||
| `boundary` | an explicit composition or transaction boundary that legitimately owns a handle (process entry, CLIs, health probe) | 15 |
|
||||
| `remove` | the import is unnecessary and goes | 0 |
|
||||
|
||||
The rows live in code, not only here: `Server/invariants/db_import_boundary.go`
|
||||
@@ -50,8 +61,10 @@ file stopped importing `db`. The `db` surface only shrinks — B3-2 deleted the
|
||||
two auth handler rows (28 → 26 `move`), B3-8 deletes a family's rows as it
|
||||
moves. A B3-5 file split can spread one row's code across two rows without
|
||||
adding any new `db` use (`serve.go` → `replay.go`, then the visibility
|
||||
gather into `hub_visibility.go`: 26 → 28 `move` rows while the calls behind
|
||||
them only moved), so it is the surface, not the row count, that ratchets.
|
||||
gather into `hub_visibility.go`; the finisher turned `hub.go` type-only
|
||||
while `hub_settings.go` took its `move` row — 26 → 28 `move` across the
|
||||
series with the calls behind them only moved), so it is the surface, not
|
||||
the row count, that ratchets.
|
||||
|
||||
## How the measurement works
|
||||
|
||||
@@ -127,9 +140,11 @@ which is a row worth reading, and none exists today.
|
||||
| `ws/eventstore.go` | `PersistedEvent×3` | — | — | type-only | adapter | — | PersistedEvent type; store is an interface |
|
||||
| `ws/handlers.go` | `User` | — | `GetChannel` `GetRoleForUser` `GetSessionWithBanStatus` `IsDMParticipant` | calls | move | channel | channel, role, session-ban and DM reads in command handlers |
|
||||
| `ws/handlers_chat.go` | — | `NewDMChannelInfo()` | — | calls | adapter | — | pure NewDMChannelInfo helper |
|
||||
| `ws/hub.go` | `DB×2` | — | `GetSetting×2` | calls | move | settings-ops | GetSetting at construction |
|
||||
| `ws/hub.go` | `DB` | — | — | type-only | boundary | — | Hub state holds the handle the families read through; no calls |
|
||||
| `ws/hub_broadcast.go` | `Channel×2` `Emoji` `Role` | — | `GetRoleForUser` `GetUserByID` | calls | move | channel | member broadcast payloads read the user and role they announce |
|
||||
| `ws/hub_options.go` | `DB` | — | — | type-only | boundary | — | construction validates and stores the handle; no calls |
|
||||
| `ws/hub_presence.go` | — | `BroadcastStatus()` | — | calls | adapter | — | presence coalescer; pure BroadcastStatus helper and the MemberSummary shape |
|
||||
| `ws/hub_settings.go` | `DB` | — | `GetSetting×2` | calls | move | settings-ops | settings cache reads server name and MOTD through h.db; import pinned so the rule sees it |
|
||||
| `ws/hub_sweep.go` | `User` | — | `GetAllVoiceStates` `GetChannel` `GetChannelVoiceStates` `GetSessionsWithBanStatusBatch` `LeaveVoiceChannelIfMatch×2` | calls | move | voice | stale-voice sweep reads and leaves |
|
||||
| `ws/hub_visibility.go` | `Channel×2` `ChannelOverride` `DB` `User` | — | `GetChannel×2` `GetChannelOverridesFor` `GetDMParticipantIDs` `GetRoleByID` `GetUserByID` `GetUserDMChannelIDs` `ListChannels×2` | calls | move | channel | visibility and audience resolution reads channels, overrides, participants, users |
|
||||
| `ws/messages.go` | `Channel×4` `DMChannelInfo×2` `DMUser×2` `Emoji` `Role×3` `User×2` `VoiceState` | `BroadcastStatus()` `StatusForViewer()` | — | calls | adapter | — | wire types + pure status helpers |
|
||||
@@ -141,8 +156,8 @@ which is a row worth reading, and none exists today.
|
||||
| `ws/voice_join.go` | `Channel×3` `ChannelOverride` `VoiceState×5` | `ErrChannelFull` | `GetChannel×2` `GetChannelOverridesFor` `GetChannelVoiceStates` `GetRoleForUser` `GetVoiceState×6` `JoinVoiceChannel` `JoinVoiceChannelIfCapacity` `LeaveVoiceChannelIfMatch` `SetVoiceServerDeafen` `SetVoiceServerMute` | calls | move | voice | voice state reads and writes |
|
||||
| `ws/voice_moderation.go` | `Role` `VoiceState×3` | `WriteAudit()` | `CountChannelVoiceUsers` `GetChannel×2` `GetRoleForUser` `GetVoiceState×2` `SetVoiceServerDeafen×2` `SetVoiceServerMute×2` | calls | move | voice | mute/deafen/move persist voice state |
|
||||
|
||||
59 files import `db` outside `db/` and `service/` (. 1, admin 16, api 10, auth 2, cmd/gendocs 1, cmd/seed 2, internal/app 6, plugin 1, ws 20); 19 are type-only; 0 unlisted.
|
||||
Dispositions: adapter 18, boundary 13, move 28. Move targets: auth 7, channel 7, connection 2, role 1, settings-ops 4, upload 2, user 2, voice 3.
|
||||
61 files import `db` outside `db/` and `service/` (. 1, admin 16, api 10, auth 2, cmd/gendocs 1, cmd/seed 2, internal/app 6, plugin 1, ws 22); 21 are type-only; 0 unlisted.
|
||||
Dispositions: adapter 18, boundary 15, move 28. Move targets: auth 7, channel 7, connection 2, role 1, settings-ops 4, upload 2, user 2, voice 3.
|
||||
|
||||
<!-- dbinventory:end -->
|
||||
|
||||
|
||||
+16
-16
@@ -10,22 +10,22 @@ authority**.
|
||||
|
||||
## Active — these drive current work
|
||||
|
||||
| Plan | State |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [beta-product-requirements-2026-08-23](beta-product-requirements-2026-08-23.md) | Approved beta scope, frozen. 57 `BPR-*` requirements. **Amended 2026-08-31:** BPR-032 reworded to the slim single-current-epoch policy (owner decision; the scope HP-2 accepted). |
|
||||
| [repo-health-roadmap-2026-08-23](repo-health-roadmap-2026-08-23.md) | Phase order and gates, B0–B10. **B0, B1 and B2 complete** (HP-0, HP-1 and HP-2 all accepted); **B3 in progress** — the B3 plan row below tracks it. B4–B10 not started. **Amended 2026-08-28:** dated lines in B3–B10, a "Phase execution pattern" section, and a truthful status header. **Amended 2026-08-29:** B3/B7/B9 lines binding the layout-refactor supplement below; current slice updated for B2-2. **Amended 2026-08-31:** B1 exit reworded to identical-tree integration evidence; B6/B8/B10 `server-info` and epoch-window lines aligned with the B2-2 slim decision; B10 gains the BPR-051 comprehension-read row (owner decisions 2026-08-31). |
|
||||
| [repo-health-issue-register-2026-08-23](repo-health-issue-register-2026-08-23.md) | 116 planning rows. Public-safe; not a replacement for the ledger. **Amended 2026-08-31:** OC-0349–0375 and OC-0379 enumerated with owner-approved phases; G-03 closure evidence reworded to identical-tree integration evidence; BG-07 re-scoped to BPR-032 as amended. |
|
||||
| [beta-requirements-traceability-2026-08-23](beta-requirements-traceability-2026-08-23.md) | Requirement → phase → evidence map. No row is release-qualified. |
|
||||
| [b0-baseline-2026-08-25](b0-baseline-2026-08-25.md) | **Supersedes the roadmap's "current evidence snapshot."** B0 measurements and dispositions. |
|
||||
| [b1-repository-foundation-2026-08-25](b1-repository-foundation-2026-08-25.md) | **B1-0 through B1-8 all done.** B1 execution plan. Re-verifies every RL-\* claim against HEAD; several are refuted. |
|
||||
| [hp-0-scorecard-2026-08-25](hp-0-scorecard-2026-08-25.md) | **HP-0 accepted 2026-08-25.** The single baseline-acceptance artifact. Part-closes `R-08`. |
|
||||
| [hp-1-scorecard-2026-08-27](hp-1-scorecard-2026-08-27.md) | **HP-1 accepted 2026-08-27.** Structural-diff proofs for the flatten and module rename, plus the B1 exit gate. |
|
||||
| [b2-protocol-trust-compat-2026-08-28](b2-protocol-trust-compat-2026-08-28.md) | **B2 complete — HP-2 accepted 2026-08-29.** B2-0, B2-1, B2-8 done 2026-08-28; B2-2 (B2-3/B2-4 folded in), B2-5, B2-6, B2-7, B2-9 done 2026-08-29. Scorecard below. |
|
||||
| [hp-2-scorecard-2026-08-29](hp-2-scorecard-2026-08-29.md) | **HP-2 accepted 2026-08-29.** Seven questions answered with commands; B2 exit gate, nine conditions met (1 at the slim epoch scope, 4 with one E2EE gap disclaimed). Owner follow-ups that do not gate B3: BPR-051 reader line, SEC-01/SEC-04 advisory IDs. |
|
||||
| [b3-server-architecture-guardrails-2026-08-29](b3-server-architecture-guardrails-2026-08-29.md) | **B3 in progress from 2026-08-29.** Execution plan: B3-0 inventory → B3-1/B3-2 auth slice → HP-3 → lifecycle, hub options, `ws` split, families; guardrails and the alpha dataset beside the slice. B3-0 (inventory + `db-import-boundary` rule) and B3-1 (auth characterization) merged 2026-08-29; B3-2 (auth vertical slice) merged 2026-08-30; B3-9 (five of the six B3-tagged findings; OC-0323 rides B3-8) merged 2026-08-30; HP-3 accepted 2026-08-30; B3-3 (lifecycle extraction into `Server/internal/app/`) merged 2026-08-30 (#1464); B3-4 (hub constructor options) merged 2026-08-31 (#1470); B3-7 (deterministic alpha dataset) merged 2026-08-31 (#1469); B3-5 (`ws` split) in progress — split PR 1 (handshake auth + fresh-connect) merged 2026-08-31 (#1472); split PR 2 (replay family + connection registry) merged 2026-08-31 (#1473); split PR 3 (visibility gather) merged 2026-08-31 (#1474); split PR 4 (voice leftovers + presence coalescer) opened 2026-08-31. |
|
||||
| [hp-3-scorecard-2026-08-29](hp-3-scorecard-2026-08-29.md) | **HP-3 accepted 2026-08-30 by the owner.** Five questions on the auth vertical slice answered with commands: frozen set green at every pre-squash SHA, `api` db importers 12 → 10, B2 contracts unchanged, the pattern written as D4 in server.md, guardrails as they exist. |
|
||||
| [b3-bench-baseline-2026-08-30](b3-bench-baseline-2026-08-30.md) | **Recorded 2026-08-30, not gated.** The six B3-6 `Benchmark*` through `benchstat` at `ec8ef24a`, produced by `make bench-baseline`. Nothing in CI reads these numbers; the performance gate is B6's. Regenerating writes a new dated file — replace this row and delete the superseded document, so only the newest baseline is kept. |
|
||||
| [audit-2026-08-19-remediation](audit-2026-08-19-remediation.md) | Phases 1–6 done 2026-08-20; **phase 7 pending**. Its header still reads "in progress 2026-08-19" — stale; the phase table is correct. |
|
||||
| Plan | State |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [beta-product-requirements-2026-08-23](beta-product-requirements-2026-08-23.md) | Approved beta scope, frozen. 57 `BPR-*` requirements. **Amended 2026-08-31:** BPR-032 reworded to the slim single-current-epoch policy (owner decision; the scope HP-2 accepted). |
|
||||
| [repo-health-roadmap-2026-08-23](repo-health-roadmap-2026-08-23.md) | Phase order and gates, B0–B10. **B0, B1 and B2 complete** (HP-0, HP-1 and HP-2 all accepted); **B3 in progress** — the B3 plan row below tracks it. B4–B10 not started. **Amended 2026-08-28:** dated lines in B3–B10, a "Phase execution pattern" section, and a truthful status header. **Amended 2026-08-29:** B3/B7/B9 lines binding the layout-refactor supplement below; current slice updated for B2-2. **Amended 2026-08-31:** B1 exit reworded to identical-tree integration evidence; B6/B8/B10 `server-info` and epoch-window lines aligned with the B2-2 slim decision; B10 gains the BPR-051 comprehension-read row (owner decisions 2026-08-31). |
|
||||
| [repo-health-issue-register-2026-08-23](repo-health-issue-register-2026-08-23.md) | 116 planning rows. Public-safe; not a replacement for the ledger. **Amended 2026-08-31:** OC-0349–0375 and OC-0379 enumerated with owner-approved phases; G-03 closure evidence reworded to identical-tree integration evidence; BG-07 re-scoped to BPR-032 as amended. |
|
||||
| [beta-requirements-traceability-2026-08-23](beta-requirements-traceability-2026-08-23.md) | Requirement → phase → evidence map. No row is release-qualified. |
|
||||
| [b0-baseline-2026-08-25](b0-baseline-2026-08-25.md) | **Supersedes the roadmap's "current evidence snapshot."** B0 measurements and dispositions. |
|
||||
| [b1-repository-foundation-2026-08-25](b1-repository-foundation-2026-08-25.md) | **B1-0 through B1-8 all done.** B1 execution plan. Re-verifies every RL-\* claim against HEAD; several are refuted. |
|
||||
| [hp-0-scorecard-2026-08-25](hp-0-scorecard-2026-08-25.md) | **HP-0 accepted 2026-08-25.** The single baseline-acceptance artifact. Part-closes `R-08`. |
|
||||
| [hp-1-scorecard-2026-08-27](hp-1-scorecard-2026-08-27.md) | **HP-1 accepted 2026-08-27.** Structural-diff proofs for the flatten and module rename, plus the B1 exit gate. |
|
||||
| [b2-protocol-trust-compat-2026-08-28](b2-protocol-trust-compat-2026-08-28.md) | **B2 complete — HP-2 accepted 2026-08-29.** B2-0, B2-1, B2-8 done 2026-08-28; B2-2 (B2-3/B2-4 folded in), B2-5, B2-6, B2-7, B2-9 done 2026-08-29. Scorecard below. |
|
||||
| [hp-2-scorecard-2026-08-29](hp-2-scorecard-2026-08-29.md) | **HP-2 accepted 2026-08-29.** Seven questions answered with commands; B2 exit gate, nine conditions met (1 at the slim epoch scope, 4 with one E2EE gap disclaimed). Owner follow-ups that do not gate B3: BPR-051 reader line, SEC-01/SEC-04 advisory IDs. |
|
||||
| [b3-server-architecture-guardrails-2026-08-29](b3-server-architecture-guardrails-2026-08-29.md) | **B3 in progress from 2026-08-29.** Execution plan: B3-0 inventory → B3-1/B3-2 auth slice → HP-3 → lifecycle, hub options, `ws` split, families; guardrails and the alpha dataset beside the slice. B3-0 (inventory + `db-import-boundary` rule) and B3-1 (auth characterization) merged 2026-08-29; B3-2 (auth vertical slice) merged 2026-08-30; B3-9 (five of the six B3-tagged findings; OC-0323 rides B3-8) merged 2026-08-30; HP-3 accepted 2026-08-30; B3-3 (lifecycle extraction into `Server/internal/app/`) merged 2026-08-30 (#1464); B3-4 (hub constructor options) merged 2026-08-31 (#1470); B3-7 (deterministic alpha dataset) merged 2026-08-31 (#1469); B3-5 (`ws` split) in progress — split PR 1 (handshake auth + fresh-connect) merged 2026-08-31 (#1472); split PR 2 (replay family + connection registry) merged 2026-08-31 (#1473); split PR 3 (visibility gather) merged 2026-08-31 (#1474); split PR 4 (voice leftovers + presence coalescer) merged 2026-08-31 (#1475); finisher PR (hub.go < 400 + exit evidence) opened 2026-08-31 — B3-5 complete when it merges; B3-8 families next. |
|
||||
| [hp-3-scorecard-2026-08-29](hp-3-scorecard-2026-08-29.md) | **HP-3 accepted 2026-08-30 by the owner.** Five questions on the auth vertical slice answered with commands: frozen set green at every pre-squash SHA, `api` db importers 12 → 10, B2 contracts unchanged, the pattern written as D4 in server.md, guardrails as they exist. |
|
||||
| [b3-bench-baseline-2026-08-30](b3-bench-baseline-2026-08-30.md) | **Recorded 2026-08-30, not gated.** The six B3-6 `Benchmark*` through `benchstat` at `ec8ef24a`, produced by `make bench-baseline`. Nothing in CI reads these numbers; the performance gate is B6's. Regenerating writes a new dated file — replace this row and delete the superseded document, so only the newest baseline is kept. |
|
||||
| [audit-2026-08-19-remediation](audit-2026-08-19-remediation.md) | Phases 1–6 done 2026-08-20; **phase 7 pending**. Its header still reads "in progress 2026-08-19" — stale; the phase table is correct. |
|
||||
|
||||
## Partially implemented
|
||||
|
||||
|
||||
@@ -925,6 +925,54 @@ that responsibility 6's size target needed:
|
||||
construction/options and the replay-limit accessor), `voice_broadcast.go`
|
||||
38 → 115, `hub_presence.go` 153.
|
||||
|
||||
**Evidence — finisher PR and B3-5 exit, 2026-08-31** — branch
|
||||
`feat/b3-5-ws-split-5` from `dev` `df717e48` (split PR 4's squash).
|
||||
|
||||
- **Move 8** — one pure-move commit closes `hub.go`'s size target:
|
||||
`HubOptions` and `NewHub` → new `hub_options.go`; `getCachedSettings`
|
||||
and `refreshSettingsLocked` → new `hub_settings.go`;
|
||||
`maxColdReplayLimit` → `replay.go`, beside the family that reads it
|
||||
(211 lines out, 229 in). Residue: the new files' scaffolding plus
|
||||
duplicates of five imports `hub.go` retains (`auth`, `db`,
|
||||
`permissions`, `plugin`, `service`); `errors`, `fmt` and `os` moved
|
||||
outright. Gate results in the PR's test plan.
|
||||
- **Inventory**: `hub.go`'s `GetSetting` calls left with the settings
|
||||
cache, so its row turns type-only `boundary` (the handle the families
|
||||
read through); `hub_options.go` is a type-only `boundary` row
|
||||
(validates and stores the handle). `hub_settings.go` reads through the
|
||||
`h.db` field, which needs no import — Codex's P2 on the finisher PR
|
||||
caught that this made the settings-ops item invisible to the
|
||||
import-tracking rule and table — so the file pins the `db` import
|
||||
(a documented `var _ *db.DB`) and keeps its `move`/`settings-ops` row,
|
||||
`GetSetting×2` visible. The disposition counts were re-derived from
|
||||
the tool's summary (`boundary` had been stale at 12 since the
|
||||
seed-profile row landed).
|
||||
|
||||
**B3-5 exit.** All seven responsibilities relocated across five squash
|
||||
merges — #1472 (handshake auth; fresh-connect), #1473 (replay; registry),
|
||||
#1474 (visibility), #1475 (voice leftovers; presence coalescer split),
|
||||
plus this finisher — every move a pure move with its normalised-diff
|
||||
residue listed, `git diff -M --summary` recording no file-level rename
|
||||
(all function-level motion between files), and the
|
||||
`-tags deadlock -count=10 ./ws/` + `-race` pass green after every move
|
||||
with `TestEpoch1Fixtures` inside those runs. The plan's named destination
|
||||
`registry.go` was already the message-type `HandlerRegistry`, so the
|
||||
connection registry lives in `hub_registry.go` (recorded in split PR 2).
|
||||
Exit sizes, measured on this branch:
|
||||
|
||||
| File | Before B3-5 | After | Target |
|
||||
| ------------------ | ----------: | ----: | ------- |
|
||||
| `serve.go` | 990 | 183 | < 500 ✓ |
|
||||
| `hub.go` | 866 | 361 | < 400 ✓ |
|
||||
| `hub_broadcast.go` | 1032 | 424 | < 500 ✓ |
|
||||
|
||||
New files: `replay.go` 496, `hub_registry.go` 253, `hub_visibility.go`
|
||||
487, `hub_presence.go` 153, `hub_options.go` 174, `hub_settings.go` 45;
|
||||
grown files: `serve_auth.go` 105 → 207, `serve_ready.go` 375 → 564,
|
||||
`voice_broadcast.go` 38 → 115. The `db`-inventory file table in
|
||||
`server-boundaries.md` was re-measured in every split PR; no new `db`
|
||||
call was added anywhere in the series.
|
||||
|
||||
## B3-6 — Permanent guardrails
|
||||
|
||||
Roadmap workstreams 1, 2, 3, 10, 11, 13, 14, 15, 16. Runs beside B3-0..B3-2;
|
||||
|
||||
Reference in New Issue
Block a user