Help us shape the future of Go! Take the 2026 Go Developer Survey

broadcast

package
v0.26.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 18 Imported by: 2

Documentation

Overview

Package broadcast pushes server events to browsers over named channels, in the spirit of Laravel Broadcasting: public, private ("private-") and presence ("presence-") channels, each authorized by a rule registered for its name pattern, fanned out in memory or through a Broker (drivers/redis) so every app instance delivers every event.

b := broadcast.New(broadcast.Config{
	Broker:     redisClient, // nil = this process only
	User:       func(ctx context.Context) (any, error) { return auth.UserFrom[*models.User](ctx, "web") },
	Authorizer: gate,
})
err := b.Public("posts")
err = b.Private("user.{id}", func(ctx context.Context, u any, p map[string]string) (bool, error) {
	return u.(*models.User).ID == p["id"], nil
})
r.Get("/broadcast/sse", b.SSE) // or drivers/websocket's Broadcast handler

err = b.Broadcast(ctx, PostPublished{Post: post}) // Channels() []string

Decisions fail closed: a channel without a matching rule, a rule that errors and a client without a user are all denied.

See https://github.com/AlexGalhardo/vukem/blob/main/docs/frontend.md.

Example
package main

import (
	"context"
	"fmt"

	"github.com/AlexGalhardo/vukem/broadcast"
)

type exPostPublished struct {
	Title string `json:"title"`
}

func (exPostPublished) Channels() []string { return []string{"posts"} }

func main() {
	b := broadcast.New(broadcast.Config{}) // Broker: redisClient across instances
	defer b.Close()
	if err := b.Public("posts"); err != nil {
		fmt.Println(err)
		return
	}

	// Browsers subscribe through r.Get("/broadcast/sse", b.SSE); here a
	// subscription is opened directly.
	ctx := context.Background()
	sub, err := b.Subscribe(ctx, "posts")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer sub.Close()

	if err := b.Broadcast(ctx, exPostPublished{Title: "Hello"}); err != nil {
		fmt.Println(err)
		return
	}
	msg := <-sub.Messages()
	fmt.Println(msg.Channel, msg.Event, string(msg.Data))

	// A channel without a rule is denied.
	_, err = b.Subscribe(ctx, "private-user.1")
	fmt.Println(err != nil)
}
Output:
posts exPostPublished {"title":"Hello"}
true

Index

Examples

Constants

View Source
const (
	PrivatePrefix  = "private-"
	PresencePrefix = "presence-"
)

Channel name prefixes that select the channel kind.

View Source
const (
	EventHere    = "presence:here"
	EventJoining = "presence:joining"
	EventLeaving = "presence:leaving"
)

Reserved event names for presence notifications.

View Source
const (
	MaxNameLength      = 128
	DefaultMaxChannels = 32
)

Limits on names and subscriptions.

Variables

View Source
var (
	// ErrInvalidChannel rejects a malformed channel name (400).
	ErrInvalidChannel = &errs.HTTPError{Status: http.StatusBadRequest, Message: "Invalid channel name."}
	// ErrTooManyChannels rejects a subscription over Config.MaxChannels (400).
	ErrTooManyChannels = &errs.HTTPError{Status: http.StatusBadRequest, Message: "Too many channels."}
	// ErrForbidden denies a subscription (403); unauthenticated clients get
	// it too, so a probe cannot tell the two apart.
	ErrForbidden = &errs.HTTPError{Status: http.StatusForbidden, Message: "This channel is unauthorized."}
)

Functions

func Listener

func Listener[E Event](s *Server) events.Registration

Listener broadcasts every dispatched E synchronously: Dispatch returns the publish error.

bus.Register(broadcast.Listener[PostPublished](b))

func ListenerAsync

func ListenerAsync[E Event](s *Server) events.Registration

ListenerAsync broadcasts every dispatched E in the background; failures are logged by the bus.

func Queued

func Queued[E Event](s *Server, kind string, opts ...queue.Option) events.Registration

Queued broadcasts every dispatched E from a queue job of kind, so the publish survives restarts and is retried (Laravel's ShouldBroadcast). E must round-trip through JSON; register the bus's Jobs on the queue.

func ValidName

func ValidName(name string) bool

ValidName reports whether name is a valid channel or event name: 1 to 128 characters from A-Z a-z 0-9 and _ - = @ . : (no commas, spaces or control characters, so lists and wire formats cannot be forged).

Types

type Config

type Config struct {
	// Broker carries events between instances; nil works in one process.
	Broker sse.Broker
	// User resolves the current user from a request context; nil or an
	// error denies every private and presence channel.
	User func(ctx context.Context) (any, error)
	// Authorizer backs PrivateGate rules (authz.Gate implements it).
	Authorizer web.Authorizer
	// MaxChannels caps the channels of one connection (0 = 32).
	MaxChannels int
}

Config configures a Server.

type Event

type Event interface {
	Channels() []string
}

Event is something broadcast on channels. The payload is BroadcastWith() when the event has it, the event itself otherwise, encoded as JSON; the event name is BroadcastAs() or the Go type name.

type Member

type Member struct {
	ID   string `json:"id"`
	Info any    `json:"info,omitempty"`
}

Member is a user present on a presence channel. ID identifies the user (several connections of one user count once); Info is shown to the other members.

type Message

type Message struct {
	Channel string          `json:"channel"`
	Event   string          `json:"event"`
	Data    json.RawMessage `json:"data"`
}

Message is one event delivered to a subscriber.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server authorizes subscriptions and fans broadcast events out.

func New

func New(cfg Config) *Server

New returns a server; Close releases its hub.

func (*Server) Authorize

func (s *Server) Authorize(ctx context.Context, channel string) (Member, error)

Authorize decides whether the client of ctx may subscribe to channel and returns its presence member. Errors are ErrInvalidChannel or ErrForbidden, with the cause wrapped for logs.

func (*Server) Broadcast

func (s *Server) Broadcast(ctx context.Context, event Event) error

Broadcast publishes event on each of its channels.

func (*Server) Close

func (s *Server) Close()

Close ends every subscription.

func (*Server) MaxChannels

func (s *Server) MaxChannels() int

MaxChannels is the per-connection channel limit transports enforce.

func (*Server) Presence

func (s *Server) Presence(
	pattern string,
	fn func(ctx context.Context, user any, params map[string]string) (Member, bool, error),
) error

Presence authorizes "presence-" channels matching pattern with fn, which returns the member the other subscribers see (its ID must not be empty).

func (*Server) Private

func (s *Server) Private(
	pattern string,
	fn func(ctx context.Context, user any, params map[string]string) (bool, error),
) error

Private authorizes "private-" channels matching pattern (given without the prefix, e.g. "user.{id}") with fn, called with the current user and the pattern parameters.

func (*Server) PrivateGate

func (s *Server) PrivateGate(
	pattern, ability string,
	load func(ctx context.Context, params map[string]string) (any, error),
) error

PrivateGate authorizes "private-" channels matching pattern through Config.Authorizer: ability is checked on the subject load returns for the parameters (load nil checks a gate ability with no subject).

func (*Server) Public

func (s *Server) Public(pattern string) error

Public declares public channels matching pattern ("posts", "posts.{id}"): anyone may subscribe. Undeclared public channels are denied, so a typo or a per-user name never becomes readable by default.

func (*Server) Publish

func (s *Server) Publish(ctx context.Context, channel, event string, data any) error

Publish sends one event named event with data (encoded as JSON) on channel, for callers without an Event type.

func (*Server) SSE

func (s *Server) SSE(c *web.Ctx) error

SSE is the server-sent events transport, a route handler for GET /broadcast/sse?channels=posts,private-user.42. Every channel is authorized before the stream starts; any refusal answers 400 or 403 and nothing is streamed. Each event is sent with its name as the SSE event type and the Message as JSON data; presence channels first get an EventHere event listing the members.

r.Get("/broadcast/sse", b.SSE)

func (*Server) SSEHandler

func (s *Server) SSEHandler() http.Handler

SSEHandler is SSE for plain net/http muxes; refusals are written with http.Error.

func (*Server) Subscribe

func (s *Server) Subscribe(ctx context.Context, channels ...string) (*Subscription, error)

Subscribe authorizes every channel (all or nothing) and subscribes to them; presence channels announce the member to the others. ctx carries the user (usually the request context) and bounds the subscription.

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

Subscription receives the events of authorized channels until Close, ctx cancellation, Server.Close, or until it falls too far behind (then Messages closes and the client should reconnect).

func (*Subscription) Channels

func (sub *Subscription) Channels() []string

Channels lists the subscribed channels.

func (*Subscription) Close

func (sub *Subscription) Close()

Close unsubscribes and announces leaving presence channels; it is safe to call more than once.

func (*Subscription) Members

func (sub *Subscription) Members(channel string) ([]Member, bool)

Members lists the members present on a subscribed presence channel (this instance's view), or false for other channels.

func (*Subscription) Messages

func (sub *Subscription) Messages() <-chan Message

Messages delivers the events; it closes when the subscription ends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL