Documentation
¶
Overview ¶
Package actioncable is a pure-Go (no cgo) reimplementation of the core of Rails' Action Cable, faithful to MRI/Rails 4.0.5's on-the-wire protocol.
It models the WebSocket sub-protocol (welcome/ping/subscribe/confirm/reject/ unsubscribe/message/disconnect envelopes), the per-connection subscription registry, the channel lifecycle hooks, stream subscriptions and pub-sub broadcasting fan-out — everything that makes a real Action Cable JavaScript client interoperate — while leaving the two host-owned concerns as seams:
- the WebSocket transport, injected as a Transport func per Connection;
- the channel action bodies (Ruby methods), injected as a ChannelAction.
The pub-sub backend is an Adapter; an in-process AsyncAdapter and a RedisAdapter (over a RedisPubSub client seam) ship here.
The package is dependency-free, CGO-free, and has no dependency on any Ruby runtime. It is the Action Cable backend for go-embedded-ruby, but is a standalone, reusable module.
Index ¶
- Constants
- Variables
- func BroadcastingName(channelName string, model any) string
- func ChannelName(className string) string
- func EncodeConfirmation(identifier string) []byte
- func EncodeDisconnect(reason string, reconnect bool) []byte
- func EncodeMessage(identifier string, message any) ([]byte, error)
- func EncodePing(epoch int64) []byte
- func EncodeRejection(identifier string) []byte
- func EncodeWelcome() []byte
- func Handler(server *Server, factory ChannelFactory, opts *CableOptions) http.Handler
- func SerializeBroadcasting(objects ...any) string
- type Adapter
- type AsyncAdapter
- type CableOptions
- type Channel
- func (ch *Channel) BroadcastTo(model any, message any) error
- func (ch *Channel) Confirmed() bool
- func (ch *Channel) Name() string
- func (ch *Channel) Periodically(interval time.Duration, fn func()) *PeriodicTimer
- func (ch *Channel) Reject()
- func (ch *Channel) Rejected() bool
- func (ch *Channel) StreamFor(model any) error
- func (ch *Channel) StreamFrom(broadcasting string) error
- func (ch *Channel) Streams() []string
- func (ch *Channel) Transmit(data any) error
- type ChannelAction
- type ChannelFactory
- type Command
- type ConnectHook
- type Connection
- func (c *Connection) Advance(d time.Duration)
- func (c *Connection) Beat(epoch int64)
- func (c *Connection) Closed() bool
- func (c *Connection) Connect() error
- func (c *Connection) Disconnect(reason string, reconnect bool)
- func (c *Connection) Dispatch(raw []byte) error
- func (c *Connection) IdentifiedBy(key string, value any)
- func (c *Connection) Identifier(key string) any
- func (c *Connection) OnConnect(hook ConnectHook) *Connection
- func (c *Connection) RejectUnauthorized() error
- func (c *Connection) Subscription(identifier string) (*Channel, bool)
- func (c *Connection) Subscriptions() int
- type GIDParameterizer
- type Parameterizer
- type PeriodicTimer
- type RedisAdapter
- type RedisPubSub
- type RemoteConnection
- type RemoteConnections
- type Scheduler
- type Server
- type Transport
- type WSConn
Constants ¶
const ( TypeWelcome = "welcome" TypeDisconnect = "disconnect" TypePing = "ping" TypeConfirmation = "confirm_subscription" TypeRejection = "reject_subscription" )
Message type strings the server sends to clients. These mirror ActionCable::INTERNAL[:message_types] exactly.
const ( CommandSubscribe = "subscribe" CommandUnsubscribe = "unsubscribe" CommandMessage = "message" )
Command strings the client sends to the server. These mirror the commands accepted by ActionCable::Connection::Subscriptions.
const ( DisconnectInvalidRequest = "invalid_request" DisconnectServerRestart = "server_restart" DisconnectRemote = "remote" )
Disconnect reasons, mirroring ActionCable::INTERNAL[:disconnect_reasons].
const DefaultMountPath = "/cable"
DefaultMountPath is Action Cable's default mount path.
Variables ¶
ErrUnauthorized is the sentinel a ConnectHook returns — directly or via Connection.RejectUnauthorized — to reject a connection. It is the analogue of ActionCable::Connection::Authorization::UnauthorizedError, which reject_unauthorized_connection raises. When Connection.Connect sees it, the connection is closed with the "unauthorized" reason and reconnect=false and no welcome is sent, mirroring the gem's handle_open rescue.
var Protocols = []string{"actioncable-v1-json", "actioncable-unsupported"}
Protocols are the WebSocket sub-protocols Action Cable advertises, mirroring ActionCable::INTERNAL[:protocols]. The first is the JSON protocol this package implements.
Functions ¶
func BroadcastingName ¶
BroadcastingName returns the stable broadcasting string for a channel and a model — the value ActionCable.server.broadcast fans out on. It mirrors ChannelClass.broadcasting_for(model) == serialize_broadcasting([channel_name, model]).
func ChannelName ¶
ChannelName derives a channel's broadcasting-name component from its Ruby class name, mirroring ActionCable::Channel::Naming#channel_name: name.delete_suffix("Channel").gsub("::", ":").underscore.
"ChatChannel" -> "chat" "Chat::RoomChannel" -> "chat:room" "AdminNotificationsChannel" -> "admin_notifications" "FooChats::BarAppearancesChannel" -> "foo_chats:bar_appearances" "HTTPServerChannel" -> "http_server"
func EncodeConfirmation ¶
EncodeConfirmation returns {"identifier":<id>,"type":"confirm_subscription"}.
func EncodeDisconnect ¶
EncodeDisconnect returns {"type":"disconnect","reason":<reason>,"reconnect":<b>}.
func EncodeMessage ¶
EncodeMessage returns {"identifier":<id>,"message":<message>}. Because message is arbitrary host data it may fail to encode, so an error is returned.
func EncodePing ¶
EncodePing returns the {"type":"ping","message":<epoch>} heartbeat frame.
func EncodeRejection ¶
EncodeRejection returns {"identifier":<id>,"type":"reject_subscription"}.
func EncodeWelcome ¶
func EncodeWelcome() []byte
EncodeWelcome returns the {"type":"welcome"} frame sent on connect.
func Handler ¶
func Handler(server *Server, factory ChannelFactory, opts *CableOptions) http.Handler
Handler returns an http.Handler for the /cable endpoint. It upgrades each request to WebSocket, builds a Connection whose Transport writes text frames to the socket and whose channels come from factory, runs the open handshake (connect hook + welcome), then pumps client frames into Connection.Dispatch until the socket closes. A failed handshake is answered with 400/426 and the socket is not driven. This is the concrete /cable transport; hosts wanting their own may keep using NewConnection with a custom Transport directly.
func SerializeBroadcasting ¶
SerializeBroadcasting joins the serialized components with ":", mirroring ActionCable::Channel::Broadcasting#serialize_broadcasting for an Array.
Types ¶
type Adapter ¶
type Adapter interface {
Broadcast(broadcasting string, payload []byte) error
Subscribe(broadcasting string, handler func(payload []byte)) (unsubscribe func(), err error)
}
Adapter is the pub-sub backend Action Cable fans broadcasts out through. A broadcasting is an opaque string channel; payloads are the already-encoded JSON bytes produced by Server.Broadcast. Both AsyncAdapter and RedisAdapter satisfy it.
Subscribe registers handler for a broadcasting and returns a function that removes that subscription. Handlers are invoked with each broadcast payload.
type AsyncAdapter ¶
type AsyncAdapter struct {
// contains filtered or unexported fields
}
AsyncAdapter is the in-process pub-sub adapter — the analogue of Action Cable's :async adapter. Fan-out is synchronous and deterministic: Broadcast invokes every current subscriber's handler on the calling goroutine and returns when they have all run. It leaks no goroutines.
func NewAsyncAdapter ¶
func NewAsyncAdapter() *AsyncAdapter
NewAsyncAdapter returns an empty in-process adapter.
func (*AsyncAdapter) Broadcast ¶
func (a *AsyncAdapter) Broadcast(broadcasting string, payload []byte) error
Broadcast synchronously delivers payload to every current subscriber of broadcasting. Subscribers are snapshotted first so a handler may (un)subscribe during delivery without racing the iteration.
type CableOptions ¶
type CableOptions struct {
// OnConnect is the connect method run at open for identification/authorization
// (see [ConnectHook]); may be nil.
OnConnect ConnectHook
// PingInterval is the protocol-level ping period. Zero disables the heartbeat.
// The application-level ping frame Action Cable clients expect is a separate,
// host-driven concern; this keeps idle intermediaries from dropping the socket.
PingInterval time.Duration
}
CableOptions configures Handler.
type Channel ¶
type Channel struct {
// Identifier is the raw subscription identifier JSON string.
Identifier string
// Params are the parsed identifier params (including "channel").
Params map[string]any
// Action runs the channel's Ruby bodies. May be nil (no-op hooks).
Action ChannelAction
// contains filtered or unexported fields
}
Channel is one subscription on a connection, the analogue of ActionCable::Channel::Base. It owns the streams it is subscribed to and its periodic timers.
func NewChannel ¶
func NewChannel(conn *Connection, name, identifier string, params map[string]any, action ChannelAction) *Channel
NewChannel builds a channel bound to conn. name is its channel_name component (see ChannelName); identifier is the raw identifier string; params are its parsed params; action runs its bodies (may be nil).
func (*Channel) BroadcastTo ¶
BroadcastTo broadcasts message to the broadcasting for model, mirroring the class method Channel.broadcast_to(model, message).
func (*Channel) Periodically ¶
func (ch *Channel) Periodically(interval time.Duration, fn func()) *PeriodicTimer
Periodically registers fn to run once per interval on the connection's deterministic scheduler, mirroring Channel::PeriodicTimers#periodically.
func (*Channel) Reject ¶
func (ch *Channel) Reject()
Reject rejects the subscription from within the subscribed hook, mirroring Channel::Base#reject.
func (*Channel) Rejected ¶
Rejected reports whether the subscription was rejected in its subscribed hook.
func (*Channel) StreamFor ¶
StreamFor subscribes to the broadcasting for model, mirroring stream_for.
func (*Channel) StreamFrom ¶
StreamFrom subscribes the channel to broadcasting: every value broadcast there is decoded and transmitted to the client as a channel message, mirroring Channel::Streams#stream_from with the default handler. Subscribing to the same broadcasting twice is a no-op.
type ChannelAction ¶
ChannelAction is the seam that runs a channel's Ruby method bodies. The host (the rbgo binding) supplies it, capturing the *Channel in a closure so the body can call the channel's own methods (StreamFrom, Reject, Transmit, ...). The library invokes it as action(channelName, action, data):
- action "subscribed" when the subscription is confirmed (data is nil);
- action "unsubscribed" when it is torn down (data is nil);
- action "receive" or a custom action name for an inbound "message" command, with data being the decoded message payload.
Its return value is the Ruby method's result (unused by the library itself).
type ChannelFactory ¶
type ChannelFactory func(conn *Connection, identifier string, params map[string]any) (*Channel, bool)
ChannelFactory builds the Channel for a subscribe command. It receives the raw identifier string and its parsed params (which include "channel", the Ruby channel class name). It returns the channel and true, or false if no channel class matches the identifier — in which case the subscription is not confirmed, mirroring Rails logging "Subscription class not found".
type Command ¶
type Command struct {
Command string `json:"command"`
Identifier string `json:"identifier"`
Data string `json:"data"`
}
Command is a client-to-server frame: {"command":...,"identifier":...,"data":...}. identifier is itself a JSON string (e.g. `{"channel":"ChatChannel"}`); data, present only for the "message" command, is a JSON string carrying the action payload.
func DecodeCommand ¶
DecodeCommand parses a client-to-server frame.
type ConnectHook ¶
type ConnectHook func(*Connection) error
ConnectHook is the seam for a connection's app-defined connect method (ActionCable::Connection::Base#connect), where identification and authorization happen. It runs once, in Connection.Connect, before the welcome frame. It typically sets identified-by values via Connection.IdentifiedBy and may reject the connection by returning Connection.RejectUnauthorized (or any error wrapping ErrUnauthorized). May be nil (no connect method).
type Connection ¶
type Connection struct {
// Scheduler holds this connection's (and its channels') periodic timers,
// advanced deterministically by the host's event loop.
Scheduler *Scheduler
// contains filtered or unexported fields
}
Connection is a single client connection: its identified-by keys, its subscription registry, and the transport it transmits through. It is the analogue of ActionCable::Connection::Base.
func NewConnection ¶
func NewConnection(server *Server, transport Transport, factory ChannelFactory) *Connection
NewConnection creates a connection that transmits through transport and builds channels through factory.
func (*Connection) Advance ¶
func (c *Connection) Advance(d time.Duration)
Advance moves this connection's timer scheduler forward by d, firing any due channel periodic timers.
func (*Connection) Beat ¶
func (c *Connection) Beat(epoch int64)
Beat transmits a heartbeat ping frame carrying epoch, mirroring the periodic ping Action Cable sends. The host drives it on its own schedule.
func (*Connection) Closed ¶
func (c *Connection) Closed() bool
Closed reports whether the connection has been closed.
func (*Connection) Connect ¶
func (c *Connection) Connect() error
Connect performs the open handshake, mirroring the gem's handle_open: it runs the connect method (if any) for identification/authorization, subscribes to this connection's internal channel so RemoteConnections can reach it, then transmits the welcome frame.
If the connect hook rejects the connection by returning ErrUnauthorized (e.g. via Connection.RejectUnauthorized), Connect closes the connection with the "unauthorized" reason and reconnect=false, sends no welcome, and returns the error — the analogue of handle_open rescuing UnauthorizedError. Any other error from the hook is returned without sending a welcome and without closing (the host decides), leaving the transport untouched.
func (*Connection) Disconnect ¶
func (c *Connection) Disconnect(reason string, reconnect bool)
Disconnect closes the connection, transmitting a disconnect frame with reason and telling the client whether to reconnect.
func (*Connection) Dispatch ¶
func (c *Connection) Dispatch(raw []byte) error
Dispatch routes a single client-to-server frame to the matching handler.
func (*Connection) IdentifiedBy ¶
func (c *Connection) IdentifiedBy(key string, value any)
IdentifiedBy sets an identified-by key (e.g. current_user) for the connection.
func (*Connection) Identifier ¶
func (c *Connection) Identifier(key string) any
Identifier returns a previously set identified-by value.
func (*Connection) OnConnect ¶
func (c *Connection) OnConnect(hook ConnectHook) *Connection
OnConnect installs the connection's connect method (see ConnectHook), returning the connection so it can be chained after NewConnection. It mirrors defining #connect on an ActionCable::Connection::Base subclass.
func (*Connection) RejectUnauthorized ¶
func (c *Connection) RejectUnauthorized() error
RejectUnauthorized rejects the connection, mirroring reject_unauthorized_connection: it returns ErrUnauthorized, which a ConnectHook returns to make Connection.Connect close the connection with the "unauthorized" reason and reconnect=false without sending a welcome frame.
func (*Connection) Subscription ¶
func (c *Connection) Subscription(identifier string) (*Channel, bool)
Subscription returns the channel for identifier, if subscribed.
func (*Connection) Subscriptions ¶
func (c *Connection) Subscriptions() int
Subscriptions returns the number of active subscriptions (mostly for tests).
type GIDParameterizer ¶
type GIDParameterizer interface {
ToGIDParam() string
}
GIDParameterizer is satisfied by a model that exposes a GlobalID param, preferred over ToParam when serializing (mirrors object.to_gid_param).
type Parameterizer ¶
type Parameterizer interface {
ToParam() string
}
Parameterizer is satisfied by a model that knows its own #to_param, used when serializing a broadcasting name (mirrors Rails' object.to_param).
type PeriodicTimer ¶
type PeriodicTimer struct {
// contains filtered or unexported fields
}
PeriodicTimer models a channel's periodic timer (Channel#periodically). It is driven by a deterministic virtual clock via Scheduler.Advance rather than by wall-clock goroutines, so tests are reproducible and nothing leaks.
type RedisAdapter ¶
type RedisAdapter struct {
// contains filtered or unexported fields
}
RedisAdapter is the pub-sub Adapter backed by Redis, the analogue of Action Cable's :redis adapter. It broadcasts across processes/servers by publishing on the Redis channel named after the broadcasting. It carries no Redis implementation itself: it forwards to an injected RedisPubSub client.
func NewRedisAdapter ¶
func NewRedisAdapter(client RedisPubSub) *RedisAdapter
NewRedisAdapter returns a RedisAdapter forwarding to client.
type RedisPubSub ¶
type RedisPubSub interface {
Publish(channel string, payload []byte) error
Subscribe(channel string, handler func(payload []byte)) (unsubscribe func(), err error)
}
RedisPubSub is the minimal Redis pub-sub client seam the RedisAdapter drives. It is deliberately tiny so the host wires in a real client (e.g. go-ruby-redis) without this module taking a runtime dependency on it, and so the adapter is testable with an in-memory fake. Publish maps to Redis PUBLISH; Subscribe maps to SUBSCRIBE and returns an unsubscribe.
type RemoteConnection ¶
type RemoteConnection struct {
// contains filtered or unexported fields
}
RemoteConnection is a handle to the connection(s) matching a set of identifiers, addressed via their shared internal pub-sub channel.
func (*RemoteConnection) Disconnect ¶
func (rc *RemoteConnection) Disconnect(reconnect bool) error
Disconnect asks the matching connection(s) to close, telling the client whether to reconnect. It publishes an internal disconnect message on the connection's internal channel; any live connection with that identity reacts by closing with the "remote" reason. This is the cross-server mechanism RemoteConnection#disconnect uses.
type RemoteConnections ¶
type RemoteConnections struct {
// contains filtered or unexported fields
}
RemoteConnections selects connections to act on by their identifiers.
func (*RemoteConnections) Where ¶
func (rc *RemoteConnections) Where(identifiers map[string]any) *RemoteConnection
Where selects the connection(s) matching the given identified-by values, mirroring remote_connections.where(current_user: user).
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler is a deterministic virtual clock holding a channel's (and a connection's) periodic timers. Advancing it fires every timer that has come due; it never spawns a goroutine.
func (*Scheduler) Advance ¶
Advance moves the virtual clock forward by d, firing each due timer once per whole interval elapsed.
func (*Scheduler) Every ¶
func (s *Scheduler) Every(interval time.Duration, fn func()) *PeriodicTimer
Every registers fn to run once per interval and returns its handle. A non-positive interval registers an inert timer that never fires.
func (*Scheduler) Remove ¶
func (s *Scheduler) Remove(t *PeriodicTimer)
Remove detaches a timer so it no longer fires.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the pub-sub hub, the analogue of ActionCable.server. It owns an Adapter and turns ActionCable.server.broadcast(broadcasting, data) into an encode-then-fan-out over that adapter.
func (*Server) Broadcast ¶
Broadcast JSON-encodes message and fans it out to every stream subscribed to broadcasting — the equivalent of ActionCable.server.broadcast. Subscribers receive the encoded payload; the default stream handler decodes it and transmits it to the client as a channel message.
func (*Server) RemoteConnections ¶
func (s *Server) RemoteConnections() *RemoteConnections
RemoteConnections is the entry point for disconnecting connections identified elsewhere, the analogue of ActionCable.server.remote_connections.
type Transport ¶
type Transport func(payload []byte)
Transport is the seam by which a Connection sends an encoded frame to the client. The host (e.g. the rbgo binding) owns the actual WebSocket and plugs its write here; the payload is a ready-to-send JSON frame.
type WSConn ¶
type WSConn struct {
// Subprotocol is the negotiated sub-protocol (e.g. "actioncable-v1-json").
Subprotocol string
// contains filtered or unexported fields
}
WSConn is a single upgraded WebSocket connection: the hijacked net.Conn plus a buffered reader and a write mutex (the read loop and the heartbeat both write).
func Upgrade ¶
Upgrade performs the RFC 6455 server handshake on r/w, negotiating one of the Action Cable Protocols, and returns the upgraded WSConn. It writes the 101 response itself. Errors: [errNotWebSocket] if r is not an upgrade request, [errBadSubprotocol] if the client offers no supported sub-protocol, or [errNoHijack] if w cannot be hijacked.
