Mobile notifications for iOS (#210)
* mobile: wire notification registration and listener
* implement backend components for push notifications
* refactor: agentic comment cleanup
* docs: use proper module name for particle processor
* set required env variables for push notifications
* bump version
* fix: always upsert push token on mobile start
* Revert "fix: always upsert push token on mobile start"
This reverts commit 90ff18a788.
* send push notifications regardless of online status
This commit was merged in pull request #210.
This commit is contained in:
@@ -43,7 +43,6 @@ type Hub struct {
|
||||
remoteEventCh chan *remoteEvent
|
||||
}
|
||||
|
||||
// NewHub creates a new Hub.
|
||||
func NewHub(bridge *RedisBridge, authorizer *Authorizer) *Hub {
|
||||
return &Hub{
|
||||
channels: make(map[string]*Channel),
|
||||
@@ -84,7 +83,6 @@ func (h *Hub) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) {
|
||||
// Authorize channel access
|
||||
if err := h.authorizer.Authorize(ctx, req.channelID, req.conn.humanID); err != nil {
|
||||
req.conn.Send(ServerMessage{
|
||||
Type: TypeError,
|
||||
@@ -94,7 +92,6 @@ func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get or create local channel
|
||||
ch, ok := h.channels[req.channelID]
|
||||
if !ok {
|
||||
ch = newChannel(req.channelID)
|
||||
@@ -104,32 +101,27 @@ func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) {
|
||||
// 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)
|
||||
|
||||
// Track in reverse index
|
||||
if h.connChannels[req.conn] == nil {
|
||||
h.connChannels[req.conn] = make(map[string]bool)
|
||||
}
|
||||
h.connChannels[req.conn][req.channelID] = true
|
||||
|
||||
// Register in Redis and get global presence
|
||||
presence, err := h.bridge.Subscribe(ctx, req.channelID, req.conn.id, req.conn.humanID)
|
||||
if err != nil {
|
||||
slog.Error("redis subscribe failed", "channelId", req.channelID, "error", err)
|
||||
// Still send local presence as fallback
|
||||
// Fall back to local-only presence.
|
||||
presence = ch.localHumanIDs()
|
||||
}
|
||||
|
||||
// Send subscribed ack with presence snapshot
|
||||
req.conn.Send(ServerMessage{
|
||||
Type: TypeSubscribed,
|
||||
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.
|
||||
// Redis self-filter drops our own echo, so same-pod peers need a direct nudge.
|
||||
if !wasPresentLocally {
|
||||
ch.broadcast(ServerMessage{
|
||||
Type: TypeJoin,
|
||||
@@ -147,18 +139,15 @@ func (h *Hub) handleUnsubscribe(ctx context.Context, req *unsubscribeRequest) {
|
||||
|
||||
ch.removeMember(req.conn)
|
||||
|
||||
// Remove from reverse index
|
||||
if chans, ok := h.connChannels[req.conn]; ok {
|
||||
delete(chans, req.channelID)
|
||||
}
|
||||
|
||||
// Update Redis
|
||||
if err := h.bridge.Unsubscribe(ctx, req.channelID, req.conn.id, req.conn.humanID); err != nil {
|
||||
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).
|
||||
// Only emit leave once the humanID has no remaining tabs on this pod.
|
||||
if !ch.hasHumanID(req.conn.humanID) {
|
||||
ch.broadcast(ServerMessage{
|
||||
Type: TypeLeave,
|
||||
@@ -167,7 +156,6 @@ func (h *Hub) handleUnsubscribe(ctx context.Context, req *unsubscribeRequest) {
|
||||
}, req.conn)
|
||||
}
|
||||
|
||||
// Clean up empty local channel
|
||||
if ch.isEmpty() {
|
||||
delete(h.channels, req.channelID)
|
||||
}
|
||||
@@ -179,13 +167,12 @@ func (h *Hub) handleBroadcast(ctx context.Context, req *broadcastRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the sender is actually in the channel
|
||||
if _, isMember := ch.members[req.conn]; !isMember {
|
||||
req.conn.sendError("not subscribed to channel: " + req.channelID)
|
||||
return
|
||||
}
|
||||
|
||||
// Deliver to local connections (except sender)
|
||||
// Local fanout (excluding sender), then publish for other pods.
|
||||
ch.broadcast(ServerMessage{
|
||||
Type: TypeMessage,
|
||||
Channel: req.channelID,
|
||||
@@ -193,7 +180,6 @@ func (h *Hub) handleBroadcast(ctx context.Context, req *broadcastRequest) {
|
||||
Payload: req.payload,
|
||||
}, req.conn)
|
||||
|
||||
// Publish to Redis for other pods
|
||||
h.bridge.Broadcast(ctx, req.channelID, req.conn.humanID, req.payload)
|
||||
}
|
||||
|
||||
@@ -234,7 +220,8 @@ func (h *Hub) handleDisconnect(ctx context.Context, conn *Conn) {
|
||||
func (h *Hub) handleRemoteEvent(evt *remoteEvent) {
|
||||
ch, ok := h.channels[evt.channelID]
|
||||
if !ok {
|
||||
return // no local connections care about this channel
|
||||
// No local subscribers — drop the event.
|
||||
return
|
||||
}
|
||||
|
||||
switch evt.event.Type {
|
||||
@@ -262,12 +249,10 @@ func (h *Hub) handleRemoteEvent(evt *remoteEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe enqueues a subscribe request for the given connection and channel.
|
||||
func (h *Hub) Subscribe(conn *Conn, channelID string) {
|
||||
h.subscribeCh <- &subscribeRequest{conn: conn, channelID: channelID}
|
||||
}
|
||||
|
||||
// disconnect sends a connection to the disconnect channel.
|
||||
func (h *Hub) disconnect(conn *Conn) {
|
||||
h.disconnectCh <- conn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user