3 Channel-Subscriptions
Virgil edited this page 2026-02-19 16:56:48 +00:00

Channel Subscriptions

Channels provide targeted message delivery. Rather than broadcasting every message to every client, channels allow clients to opt in to specific message streams. This is the primary mechanism for efficient, scoped communication.

Back to Home

Overview

A channel is simply a named string (e.g. "process:build-1", "events:deployments", "chat:room-42"). The Hub maintains a map of channel names to sets of subscribed clients. When a message is sent to a channel, only subscribed clients receive it.

                Hub
    ┌───────────────────────────┐
    │                           │
    │  channels map:            │
    │                           │
    │  "process:build-1"        │
    │    ├── Client A           │
    │    └── Client B           │
    │                           │
    │  "process:test-3"         │
    │    └── Client A           │
    │                           │
    │  "events:deploy"          │
    │    └── Client C           │
    │                           │
    └───────────────────────────┘

In this example, a message sent to "process:build-1" reaches Client A and Client B, but not Client C.

Subscribe Flow

Client-Side (JavaScript)

const socket = new WebSocket("ws://localhost:8080/ws");

// Subscribe to a channel
socket.send(JSON.stringify({
  type: "subscribe",
  data: "process:build-1"
}));

What Happens Internally

  1. The client's readPump goroutine receives the message
  2. It parses the JSON and identifies type: "subscribe"
  3. It extracts the channel name from the data field
  4. It calls hub.Subscribe(client, channel)
  5. The Hub creates the channel if it does not already exist
  6. The client is added to the channel's subscriber set
  7. The channel name is recorded in the client's subscriptions map
// Inside readPump
case TypeSubscribe:
    if channel, ok := msg.Data.(string); ok {
        c.hub.Subscribe(c, channel)
    }

No confirmation message is sent back to the client. The subscription takes effect immediately.

Unsubscribe Flow

Client-Side (JavaScript)

socket.send(JSON.stringify({
  type: "unsubscribe",
  data: "process:build-1"
}));

What Happens Internally

  1. The readPump receives the message
  2. It calls hub.Unsubscribe(client, channel)
  3. The client is removed from the channel's subscriber set
  4. If the channel has no remaining subscribers, it is deleted from the channels map (garbage collection)
  5. The channel name is removed from the client's subscriptions map

Automatic Cleanup on Disconnect

When a client disconnects (connection error, timeout, or explicit close), the Hub automatically removes the client from all subscribed channels:

1. readPump detects connection error
2. Sends client to hub.unregister channel
3. Hub.Run() processes unregister:
   a. Removes client from clients map
   b. Closes client's send channel
   c. Iterates client.subscriptions
   d. Removes client from each channel
   e. Deletes any channels that become empty

This means clients never need to explicitly unsubscribe before disconnecting.

Channel Naming Conventions

Channel names are arbitrary strings. The package uses process:{id} internally for process streaming (see Process-Streaming), but you can use any naming scheme:

Pattern Example Use Case
process:{id} process:build-1 Process output streaming
events:{category} events:deployments Categorised event streams
chat:{room} chat:room-42 Chat rooms
user:{id} user:alice Per-user notifications
{custom} system-alerts Any application-specific channel

Sending to a Channel

Server-Side API

// Send a custom message to channel subscribers
err := hub.SendToChannel("events:deployments", ws.Message{
    Type: ws.TypeEvent,
    Data: map[string]any{
        "event": "deploy_started",
        "data":  map[string]string{"version": "1.2.3"},
    },
})

The SendToChannel method:

  1. Sets the Timestamp and Channel fields on the message
  2. Marshals the message to JSON
  3. Acquires a read lock on the Hub
  4. Looks up the channel in the channels map
  5. Sends the serialised message to each subscriber's send channel (non-blocking)
  6. Returns nil if the channel has no subscribers (this is not an error)

No Subscribers Behaviour

Sending to a channel with no subscribers silently succeeds. This is by design -- the sender does not need to know whether anyone is listening.

// This returns nil even if nobody is subscribed
err := hub.SendToChannel("empty-channel", msg)
// err == nil

Querying Subscriptions

Hub-Level

// How many channels exist?
count := hub.ChannelCount()

// How many clients are subscribed to a channel?
subs := hub.ChannelSubscriberCount("process:build-1")

Client-Level

// Get a copy of a client's subscriptions
channels := client.Subscriptions()
// Returns: ["process:build-1", "events:deployments"]

Multiple Subscriptions

A client can subscribe to any number of channels simultaneously. Each subscription is independent:

// Subscribe to multiple channels
socket.send(JSON.stringify({ type: "subscribe", data: "process:build-1" }));
socket.send(JSON.stringify({ type: "subscribe", data: "process:test-3" }));
socket.send(JSON.stringify({ type: "subscribe", data: "events:deploy" }));

Subscribing to the same channel twice is safe and idempotent -- the client is simply already present in the set.

See Also