diff --git a/go/internal/pusher/hub.go b/go/internal/pusher/hub.go index 480e019..1de54e4 100644 --- a/go/internal/pusher/hub.go +++ b/go/internal/pusher/hub.go @@ -101,6 +101,9 @@ func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) { h.channels[req.channelID] = ch } + // Capture before addMember so multi-tab joins don't emit a spurious join. + wasPresentLocally := ch.hasHumanID(req.conn.humanID) + // Add to local channel ch.addMember(req.conn, req.conn.humanID) @@ -124,6 +127,16 @@ func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) { Channel: req.channelID, Presence: presence, }) + + // Notify other local members. The Redis self-filter drops our own echo, + // so same-pod peers would otherwise never hear about this join. + if !wasPresentLocally { + ch.broadcast(ServerMessage{ + Type: TypeJoin, + Channel: req.channelID, + HumanID: req.conn.humanID, + }, req.conn) + } } func (h *Hub) handleUnsubscribe(ctx context.Context, req *unsubscribeRequest) { @@ -144,6 +157,16 @@ func (h *Hub) handleUnsubscribe(ctx context.Context, req *unsubscribeRequest) { slog.Error("redis unsubscribe failed", "channelId", req.channelID, "error", err) } + // Notify other local members iff the humanID is fully gone from this pod + // (multi-tab: other conns keep them present, so no leave fires). + if !ch.hasHumanID(req.conn.humanID) { + ch.broadcast(ServerMessage{ + Type: TypeLeave, + Channel: req.channelID, + HumanID: req.conn.humanID, + }, req.conn) + } + // Clean up empty local channel if ch.isEmpty() { delete(h.channels, req.channelID) @@ -192,6 +215,14 @@ func (h *Hub) handleDisconnect(ctx context.Context, conn *Conn) { slog.Error("redis unsubscribe on disconnect failed", "channelId", channelID, "error", err) } + if !ch.hasHumanID(conn.humanID) { + ch.broadcast(ServerMessage{ + Type: TypeLeave, + Channel: channelID, + HumanID: conn.humanID, + }, conn) + } + if ch.isEmpty() { delete(h.channels, channelID) }