fix(sync): scan events keep instance-wide fan-out as the sanctioned BroadcastToAll

Verifying the per-user chokepoint end to end surfaced a UI regression
it would have caused: web/src/header.ts runs the sitewide scan
indicator on scan_progress/scan_complete events in every browser, and
routing those to the triggering identity alone froze the indicator for
everyone else at its page-load state (active-summary stays the
load-time fallback, but live movement died). The source's own comment
declares the indicator sitewide by design, and scan payloads carry no
user data — job ids, file counts, progress percentages only.

Resolution honors both truths: BroadcastToAll is re-added as the ONE
sanctioned instance-wide fan-out, its doc comment making the decision
explicit (no user-derived data; anything carrying books, notes, or
annotations belongs in BroadcastToUser), and the two scan emitters use
it. TestBroadcastToAllReachesEveryone pins the exception alongside
TestBroadcastIsPerUser — audiences are now named decisions the
compiler and the tests both enforce.
This commit is contained in:
John O'Keefe committed 2026-11-02 18:17:37 -05:00
1 parent a431dfa10b
commit dd963993eb
4 files changed
+77 -13

No files matched your search

+22
View File
@@ -71,6 +71,28 @@ func NewConnectionManager() *ConnectionManager {
}
}
// BroadcastToAll delivers an event to every connected client on the
// instance. This is the ONE sanctioned instance-wide fan-out, and using
// it is a decision: the payload must contain no user-derived data. Its
// only caller is the scan-event emitter — scan activity is instance
// activity (job ids, file counts, progress percentages), and the
// sitewide scan indicator in web/src/header.ts is built on receiving it
// in every browser. Anything that carries a user's books, notes, or
// annotations belongs in BroadcastToUser.
func (m *ConnectionManager) BroadcastToAll(msg BroadcastMessage) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, conn := range m.connections {
select {
case conn.Send <- msg:
default:
// Channel full, skip this connection
log.Printf("WebSocket: Channel full for %s, skipping broadcast", conn.DeviceName)
}
}
}
// BroadcastToUser sends a message to all connections for a specific user
func (m *ConnectionManager) BroadcastToUser(userID string, msg BroadcastMessage) {
m.mu.RLock()