Documentation
¶
Overview ¶
Package actioncable is a client for Rails' Action Cable.
A Client owns one WebSocket connection to an Action Cable server and multiplexes any number of channel subscriptions over it. It keeps the connection alive the way the official JavaScript client does: the server beats a ping every three seconds, and a connection that goes quiet for longer than WithStaleAfter is torn down and redialed with backoff. Subscriptions survive reconnects — they are resubscribed as soon as the server says welcome.
client := actioncable.New("wss://example.com/cable")
if err := client.Connect(ctx); err != nil {
return err
}
defer client.Close()
room, err := client.Subscribe(ctx, actioncable.Identifier{
Channel: "RoomChannel",
Params: actioncable.Params{"id": 42},
})
if err != nil {
return err
}
go func() {
for message := range room.Messages() {
var said struct{ Body string }
message.Unmarshal(&said)
fmt.Println(said.Body)
}
}()
room.Perform(ctx, "speak", map[string]any{"body": "Hello!"})
Two things are pluggable. A Transport carries bytes — the built-in WebSocketTransport speaks RFC 6455 over the standard library, and any WebSocket package can be dropped in behind the same interface. A Protocol speaks one Action Cable wire format, negotiated as one WebSocket subprotocol — V1JSON implements actioncable-v1-json, and a new format is a new Protocol rather than a fork of this client. WithProtocols offers several, and the server picks the one it knows.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/basecamp/actioncable-go"
)
func main() {
client := actioncable.New("wss://example.com/cable")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()
room, err := client.Subscribe(ctx, actioncable.Identifier{
Channel: "RoomChannel",
Params: actioncable.Params{"id": 42},
})
if err != nil {
log.Fatal(err)
}
go func() {
for message := range room.Messages() {
var said struct{ Body string }
if err := message.Unmarshal(&said); err == nil {
fmt.Println(said.Body)
}
}
}()
if err := room.Perform(ctx, "speak", map[string]any{"body": "Hello!"}); err != nil {
log.Fatal(err)
}
}
Output:
Index ¶
- Constants
- Variables
- type Client
- type Command
- type CommandName
- type Conn
- type DialOptions
- type DisconnectError
- type Identifier
- type Incoming
- type Kind
- type Logger
- type LoggerFunc
- type Message
- type Option
- func WithAdditionalProtocols(protocols ...Protocol) Option
- func WithBackoff(initial, longest time.Duration) Option
- func WithCookie(cookie string) Option
- func WithHeader(header http.Header) Option
- func WithHeaderFunc(build func(ctx context.Context) (http.Header, error)) Option
- func WithLogger(logger Logger) Option
- func WithMessageBuffer(messages int) Option
- func WithOrigin(origin string) Option
- func WithProtocols(protocols ...Protocol) Option
- func WithStaleAfter(after time.Duration) Option
- func WithSubscribeRetry(retry time.Duration) Option
- func WithTransport(transport Transport) Option
- type Params
- type Protocol
- type Subscription
- type SubscriptionOption
- type Transport
- type V1JSON
- type WebSocketTransport
Examples ¶
Constants ¶
const ( ReasonInvalidRequest = "invalid_request" ReasonServerRestart = "server_restart" ReasonRemote = "remote" )
Disconnect reasons an Action Cable server sends before hanging up.
const SubprotocolUnsupported = "actioncable-unsupported"
SubprotocolUnsupported is the sentinel an Action Cable server names when it speaks none of the subprotocols offered. The client offers it last on every handshake, the way Rails' own clients do, so a server with nothing in common can say so outright instead of leaving the subprotocol blank.
const SubprotocolV1JSON = "actioncable-v1-json"
SubprotocolV1JSON is the subprotocol every Rails Action Cable server speaks.
Variables ¶
var ( // ErrClosed is returned by a client that has been closed, or that stopped // because the server told it not to reconnect. ErrClosed = errors.New("actioncable: client closed") // ErrNotConnected is returned when a command can't be sent because the // connection is down. Subscriptions recover on their own; a Perform or Send // that hits this is lost and must be retried. ErrNotConnected = errors.New("actioncable: not connected") // ErrRejected is returned by Subscribe when the channel's subscribed method // rejected the subscription. ErrRejected = errors.New("actioncable: subscription rejected") // ErrUnsupportedSubprotocol is returned when the server negotiated a // subprotocol the protocol adapter doesn't speak. Reconnecting won't fix // that, so the client stops. ErrUnsupportedSubprotocol = errors.New("actioncable: unsupported subprotocol") // ErrAlreadyConnected is returned by Connect on a client that is already // running. ErrAlreadyConnected = errors.New("actioncable: already connected") // ErrNoProtocols is returned when there is nothing to offer the server, // which means WithProtocols was called without any protocols. ErrNoProtocols = errors.New("actioncable: no protocols to offer") )
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
A Client owns one connection to an Action Cable server and the subscriptions running over it. Create one with New, start it with Connect, and hang up with Close. It is safe for concurrent use.
func New ¶
New builds a client for an Action Cable endpoint, typically wss://host/cable. It does not touch the network until Connect.
Example (Authorized) ¶
Credentials belong on the client, which sends them while the connection is established.
package main
import (
"context"
"log"
"net/http"
"github.com/basecamp/actioncable-go"
)
func main() {
client := actioncable.New("wss://example.com/cable",
actioncable.WithCookie("_session_id=1234"),
actioncable.WithHeader(http.Header{"X-Api-Token": {"secret"}}),
)
defer client.Close()
if err := client.Connect(context.Background()); err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) Close ¶
Close hangs up, stops reconnecting, and closes every subscription's message channel. It is safe to call from a subscription callback, and safe to call twice.
func (*Client) Connect ¶
Connect starts the client and returns once the server has sent its welcome. Failed connection attempts are retried until that happens, ctx is done, or the server tells us not to come back.
ctx bounds the wait, not the client: the connection lives until Close.
func (*Client) Subscribe ¶
func (c *Client) Subscribe(ctx context.Context, identifier Identifier, options ...SubscriptionOption) (*Subscription, error)
Subscribe subscribes to a channel and returns once the server confirms it. The subscription outlives reconnects — it is resubscribed automatically — so it stays valid until Unsubscribe.
It returns ErrRejected when the channel turns the subscription down.
Example ¶
A subscription outlives reconnects, so OnConnected reports whether this is a fresh subscription or one that came back and may have missed messages.
package main
import (
"context"
"fmt"
"log"
"github.com/basecamp/actioncable-go"
)
func main() {
client := actioncable.New("wss://example.com/cable")
ctx := context.Background()
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()
room, err := client.Subscribe(ctx, actioncable.Identifier{Channel: "RoomChannel"},
actioncable.OnConnected(func(reconnected bool) {
if reconnected {
catchUp()
}
}),
)
if err != nil {
log.Fatal(err)
}
for message := range room.Messages() {
fmt.Println(message)
}
}
func catchUp() {}
Output:
type Command ¶
type Command struct {
Name CommandName
Identifier string
Data string
}
A Command is a client-to-server message. Data carries the already encoded action payload and is only set for CommandMessage.
type CommandName ¶
type CommandName string
CommandName is the verb of a client-to-server command.
const ( CommandSubscribe CommandName = "subscribe" CommandUnsubscribe CommandName = "unsubscribe" CommandMessage CommandName = "message" )
type Conn ¶
type Conn interface {
// Subprotocol reports what the server negotiated, empty if it named none.
Subprotocol() string
// Read returns the next complete message. It returns an error once the
// connection is unusable, including when ctx is done.
Read(ctx context.Context) ([]byte, error)
// Write sends one text message.
Write(ctx context.Context, payload []byte) error
Close() error
}
A Conn is one live connection. Read and Write are each called from a single goroutine at a time, but Close may be called concurrently with either, and must interrupt them.
type DialOptions ¶
DialOptions are what the client needs the transport to negotiate: the subprotocols its protocol adapter speaks, and the headers that authenticate the request — a cookie or a token, since an Action Cable server authorizes the upgrade request itself.
type DisconnectError ¶
A DisconnectError reports that the server sent a disconnect frame.
func (*DisconnectError) Error ¶
func (e *DisconnectError) Error() string
type Identifier ¶
An Identifier names one subscription. It is encoded as a JSON object and the server treats that encoding as an opaque key, echoing it back on every frame it sends for the subscription.
actioncable.Identifier{Channel: "RoomChannel", Params: actioncable.Params{"id": 42}}
A channel with no params needs only the name.
func (Identifier) String ¶
func (i Identifier) String() string
type Incoming ¶
An Incoming is a decoded server-to-client frame. Reason and Reconnect are only set on KindDisconnect, Message on KindMessage and KindPing.
type Logger ¶
A Logger takes the client's chatter. The standard library's *log.Logger satisfies it, and so does anything else with a Printf.
type LoggerFunc ¶
LoggerFunc adapts a function to Logger.
func (LoggerFunc) Printf ¶
func (f LoggerFunc) Printf(format string, args ...any)
type Message ¶
type Message json.RawMessage
A Message is the undecoded payload a channel broadcast or transmitted. Its shape is entirely up to the channel, so Unmarshal it into the expected type.
type Option ¶
type Option func(*Client)
An Option configures a client.
func WithAdditionalProtocols ¶
WithAdditionalProtocols offers protocols ahead of the ones already there, so preferring a new protocol doesn't mean restating the ones to fall back to.
func WithBackoff ¶
WithBackoff sets the reconnect delay. It starts at initial, doubles per failed attempt up to longest, and is spread with jitter. Defaults to a second and half a minute.
func WithCookie ¶
WithCookie is shorthand for sending one Cookie header.
func WithHeader ¶
WithHeader sets the headers sent on the upgrade request. An Action Cable server authorizes that request, so this is where a session cookie or a bearer token goes.
func WithHeaderFunc ¶
WithHeaderFunc sets the headers the same way WithHeader does, except that it is asked on every dial rather than once. A client reconnects on its own for as long as it runs, which is longer than a credential that expires lives, and a reconnect carrying the token the first dial used would be turned down for good. What this returns is laid over the headers already set, so an Origin or a token given with WithHeader survives.
An error turns down that dial, and the client tries again on its backoff.
func WithLogger ¶
WithLogger sends the client's chatter — dropped messages, failed connections, retries — somewhere. Nothing is logged by default.
func WithMessageBuffer ¶
WithMessageBuffer sets how many messages a subscription buffers before it starts dropping them. Defaults to 64.
func WithOrigin ¶
WithOrigin sets the Origin header. Rails checks it unless the server disables request forgery protection.
func WithProtocols ¶
WithProtocols sets the protocols offered during the handshake, most preferred first, replacing the default of V1JSON. The server picks one of them and the client speaks it for the rest of the connection.
func WithStaleAfter ¶
WithStaleAfter sets how long a connection may go without a frame before it counts as dead. The server beats every three seconds; the default is six, so two missed beats.
func WithSubscribeRetry ¶
WithSubscribeRetry sets how often an unconfirmed subscribe command is resent. Defaults to half a second, like the JavaScript client's guarantor.
func WithTransport ¶
WithTransport swaps the network handler. The default is WebSocketTransport.
type Params ¶
Params are the extra attributes that identify a subscription alongside its channel name, like the id of the record a channel streams for.
type Protocol ¶
type Protocol interface {
// Subprotocol is the name this protocol negotiates under.
Subprotocol() string
// Encode turns a command into one outgoing message.
Encode(command Command) ([]byte, error)
// Decode turns one incoming message into a frame the client understands.
Decode(payload []byte) (Incoming, error)
}
A Protocol translates between Action Cable commands and the bytes on the wire. It is the seam where an Action Cable protocol plugs in.
One protocol speaks one subprotocol. A client offers every protocol it was given and speaks the one the server picks, so supporting a new protocol means adding one rather than replacing the list.
Implementations must be safe for concurrent use.
type Subscription ¶
type Subscription struct {
// contains filtered or unexported fields
}
A Subscription is one channel subscription on a client. Read what the channel sends from Messages, and talk back with Perform or Send.
func (*Subscription) Key ¶
func (s *Subscription) Key() string
Key is the JSON identifier string the server knows this subscription by, and the one it echoes back on everything it sends here.
func (*Subscription) Messages ¶
func (s *Subscription) Messages() <-chan Message
Messages carries everything the channel broadcasts or transmits to this subscription. It closes when the subscription is unsubscribed or the client is closed.
Read it promptly. Messages that arrive with the buffer full are dropped and logged rather than stalling the connection — WithMessageBuffer sizes the buffer for a slow consumer.
func (*Subscription) Perform ¶
Perform invokes an action on the channel — the equivalent of the JavaScript client's perform. data must encode to a JSON object, and may be nil.
func (*Subscription) Send ¶
func (s *Subscription) Send(ctx context.Context, data any) error
Send delivers data to the channel as-is, without naming an action. Rails routes it to the channel's receive method.
func (*Subscription) Unsubscribe ¶
func (s *Subscription) Unsubscribe(ctx context.Context) error
Unsubscribe tells the server to drop the subscription and closes Messages.
type SubscriptionOption ¶
type SubscriptionOption func(*Subscription)
A SubscriptionOption configures a subscription. Callbacks run on their own goroutine, one at a time, in the order the events happened, so Close, Subscribe, and Unsubscribe all work from inside one.
func OnConnected ¶
func OnConnected(callback func(reconnected bool)) SubscriptionOption
OnConnected is called every time the server confirms the subscription, including after a reconnect — which is what reconnected reports.
func OnDisconnected ¶
func OnDisconnected(callback func(willReconnect bool)) SubscriptionOption
OnDisconnected is called when the connection drops, with whether the client intends to dial again.
func OnRejected ¶
func OnRejected(callback func()) SubscriptionOption
OnRejected is called when the channel rejects the subscription.
type Transport ¶
type Transport interface {
Dial(ctx context.Context, url string, options DialOptions) (Conn, error)
}
A Transport dials the network connection a client talks over. It is the seam where a network handler plugs in: the built-in WebSocketTransport speaks RFC 6455 on the standard library, and wrapping gorilla/websocket, coder/websocket, or an in-memory pipe for tests means implementing these two interfaces and nothing else.
type V1JSON ¶
type V1JSON struct{}
V1JSON implements the actioncable-v1-json protocol: JSON objects in text frames, keyed by command going out and by type coming in.
func (V1JSON) Subprotocol ¶
type WebSocketTransport ¶
type WebSocketTransport struct {
// Dialer opens the TCP connection. A zero value dialer is used when nil.
Dialer *net.Dialer
// TLSConfig configures wss:// connections.
TLSConfig *tls.Config
// HandshakeTimeout bounds the upgrade request. Defaults to 10 seconds.
HandshakeTimeout time.Duration
// WriteTimeout bounds a single write when the caller's context has no
// deadline. Defaults to 10 seconds.
WriteTimeout time.Duration
// MaxMessageSize is the largest message accepted, in bytes. Defaults to 8 MB.
MaxMessageSize int64
}
WebSocketTransport is the built-in transport: an RFC 6455 client written on the standard library, so the package carries no dependencies. It handles the upgrade handshake, masks what it sends, answers pings, and reassembles fragmented messages.
func (*WebSocketTransport) Dial ¶
func (t *WebSocketTransport) Dial(ctx context.Context, rawURL string, options DialOptions) (Conn, error)