Documentation
¶
Overview ¶
Package realtime provides transport-neutral, single-process room coordination. Each active room is owned by exactly one goroutine (its room loop), which is the sole mutator of that room's membership, timers, and every call into host-supplied Logic. There is no distributed pub/sub and no locking-based shared state.
Index ¶
- Constants
- Variables
- type Coordinator
- type Event
- type EventKind
- type Factory
- type Framer
- type Hub
- func (h *Hub) Close(ctx context.Context) error
- func (h *Hub) Dispatch(ctx context.Context, ev Event) (err error)
- func (h *Hub) DispatchPeer(ctx context.Context, ev Event, peer Peer) (err error)
- func (h *Hub) Join(ctx context.Context, roomID string, principal Principal, peer Peer) (err error)
- func (h *Hub) Leave(ctx context.Context, roomID string, principal Principal, peer Peer) (err error)
- func (h *Hub) SendUser(ctx context.Context, userID string, payload []byte) error
- type Logic
- type Options
- type Peer
- type Principal
- type RoomContext
Constants ¶
const ( DefaultReconnectWindow = 30 * time.Second DefaultPeerQueue = 16 DefaultRoomQueue = 64 )
Variables ¶
var ( // ErrRoomNotFound is returned against a room that has never existed or // has already been evicted. ErrRoomNotFound = errors.New("tango realtime: room not found") // ErrClosed is returned by Join/Dispatch/DispatchPeer once Hub.Close // has begun — including for operations that were already queued, or // blocked trying to enqueue, at the moment it began. ErrClosed = errors.New("tango realtime: hub is closed") // ErrStalePeer is returned by DispatchPeer when peer is not exactly // the currently installed connection for the event's Principal in that // room — stale, replaced, overflow-removed, or never a member. ErrStalePeer = errors.New("tango realtime: peer is not the current connection") // ErrInvalidPeer is returned by Join for a nil Peer, or one whose // dynamic type is not comparable. ErrInvalidPeer = errors.New("tango realtime: peer must be non-nil and comparable") // ErrEmptyFrame is returned by Join when the Framer returns a nil or // empty result for the join snapshot. The snapshot is sequence 1 and // cannot be skipped, so the join fails. The failure does not close the // existing member or admit the new peer; sends Snapshot made through // RoomContext are not rolled back (see Framer). ErrEmptyFrame = errors.New("tango realtime: framed join snapshot is empty") )
Functions ¶
This section is empty.
Types ¶
type Coordinator ¶
type Coordinator interface {
Join(context.Context, string, Principal, Peer) error
Leave(context.Context, string, Principal, Peer) error
DispatchPeer(context.Context, Event, Peer) error
}
Coordinator is the surface realtime/websocket (and any other adapter) depends on. It is kept intentionally narrow: its purpose is adapter testability against a fake implementation, not general swappability. It deliberately excludes Hub.Dispatch — a transport must always act through DispatchPeer, which is bound to its specific live connection, not the unrestricted membership-free path bots and other trusted host-side producers use.
type Event ¶
type Event struct {
Kind EventKind
RoomID string
Principal Principal
Timer string // populated only for EventTimer
Payload []byte
}
Event is one unit of room activity. Payload is opaque to realtime — it is never parsed or inspected by the framework, only by Logic.
type EventKind ¶
type EventKind uint8
EventKind discriminates the shapes of activity a room's event loop processes. It carries no application meaning beyond routing.
type Factory ¶
Factory constructs the Logic for one room ID. One Hub always uses the same Factory, so one Hub corresponds to one room type; a host running several kinds of room constructs several Hubs.
type Framer ¶ added in v0.7.0
Framer is an optional interface a Logic implements to opt its room in to sequenced framing. Frame runs on the room goroutine, like every Logic callback, and a panic in Frame is not recovered: it crashes the process, as a panic in Handle or Snapshot does.
payload is the callee's own per-member copy: Frame may inspect or modify it without affecting other members or the caller. The returned slice becomes the queued buffer and is owned by the room from the moment Frame returns; the implementation must not retain or modify it afterwards. A nil or empty result sends nothing to that member. A non-nil error closes that member exactly as queue overflow does. For the join snapshot, a nil or empty result, or an error, fails Join instead. That failure does not itself close, rewind or skip the existing member, and never admits the new peer. Sends Snapshot made through RoomContext are not rolled back. A send is admitted to the existing member's queue only if Frame returns a non-empty result and the queue accepts it; an admitted send keeps its next sequence. An empty result is skipped and consumes no sequence. A Frame error or queue overflow closes the existing member, the rejected payload consumes no sequence, and the member stays closed. RoomContext reports no error to Snapshot.
On a connection whose Logic implements Framer, every payload admitted to the member's queue is passed through Frame with a sequence that starts at 1 for the join snapshot and increases by 1 for each later admitted payload, in queue order, with no gaps. The sequence is per member (per connection; reconnecting restarts at 1), not per room, and records admission to the outgoing queue, not delivery or acknowledgement by the peer. A payload that is not admitted (excluded or unknown member, empty encoder result, encoder error, queue overflow) uses no sequence; a socket write failure ends the member.
type Hub ¶
type Hub struct {
// contains filtered or unexported fields
}
Hub owns every active room for one Factory. Rooms are not pre-declared: they are created implicitly on a Principal's first Join and evicted implicitly once empty past Options.ReconnectWindow.
func NewHub ¶
NewHub constructs a Hub. A zero Options field is replaced with this package's Default* constant; a negative field is rejected here, at construction, rather than surfacing later as a confusing runtime failure.
func (*Hub) Close ¶
Close is idempotent: the first call rejects further Join/Dispatch with ErrClosed, stops every room's pending timers, closes every live peer, and waits for every room loop to actually exit. Every call (the first and any subsequent one) blocks until that has happened or ctx is done — a canceled ctx returns promptly with ctx.Err() while shutdown continues best-effort in the background; it is not a guarantee rooms have finished.
func (*Hub) Dispatch ¶
Dispatch delivers ev.Payload into ev.RoomID's Logic.Handle. ev.Kind must be EventAction — EventJoin/EventLeave/EventTimer are synthesized internally by the room loop itself, never accepted from a caller. Dispatch does not require the caller to have joined; this is how a bot Principal acts without ever holding a Peer.
func (*Hub) DispatchPeer ¶
DispatchPeer delivers ev.Payload into ev.RoomID's Logic.Handle on behalf of peer, which must currently be the live connection for ev.Principal in that room. Unlike Dispatch, DispatchPeer is membership- and generation-bound: it is what realtime/websocket (and any other transport adapter) must use instead of Dispatch, so a replaced or overflow-removed connection can never keep acting as if it were still current. It returns ErrStalePeer for a peer that is stale, replaced, overflow-removed, or was never a member — checked inside the room loop, before Logic.Handle runs.
func (*Hub) Join ¶
Join installs peer as principal's live connection in roomID, creating the room on first join. It blocks until the room's own event loop has applied the join (installed membership and enqueued Logic.Snapshot for delivery) or ctx is done.
type Logic ¶
type Logic interface {
// Handle processes one Event. Deliveries made via RoomContext before
// returning a non-nil error are not rolled back — validate before
// emitting, not emit-then-validate.
Handle(*RoomContext, Event) error
// Snapshot returns the current-state payload a Principal should
// receive immediately upon joining (or rejoining) the room.
Snapshot(*RoomContext, Principal) ([]byte, error)
}
Logic is the host-supplied domain behavior for one room.
type Options ¶
type Options struct {
// ReconnectWindow is how long an empty room's in-memory state is
// retained before implicit eviction.
ReconnectWindow time.Duration
// PeerQueue is the bounded outbound-delivery queue depth per peer.
PeerQueue int
// RoomQueue is the bounded inbound event-queue depth per room.
RoomQueue int
// Recorder receives bounded Hub operation metrics. Implementations must
// be safe for concurrent use. Nil uses observability.NopRecorder{}.
Recorder observability.Recorder
}
Options configures a Hub. A zero field is replaced with this package's corresponding Default* constant; a negative field is a construction error.
type Peer ¶
Peer is one live, replaceable transport connection for one Principal in one room. Implementations must be comparable (a pointer-receiver type is the natural choice) so the Hub can recognize a stale Leave or DispatchPeer call from an already-replaced connection by identity. Join validates this at the boundary — a nil Peer, or one whose dynamic type is not comparable, is rejected with ErrInvalidPeer rather than ever being admitted into membership, so no later comparison against it can panic.
type Principal ¶
type Principal struct {
UserID string
}
Principal is the stable, transport-independent actor identity a host supplies to Join/Dispatch. It is deliberately just an opaque UserID: realtime does not distinguish a human actor from a bot at this layer — an application needing that distinction owns its own convention.
type RoomContext ¶
type RoomContext struct {
// contains filtered or unexported fields
}
RoomContext is the handle Logic uses to affect its own room. Its methods are only ever called from that room's own event-loop goroutine, while handling one Event or computing one Snapshot — never concurrently, and never valid to retain past that call.
func (*RoomContext) Broadcast ¶
func (r *RoomContext) Broadcast(payload []byte) error
Broadcast delivers payload to every currently live peer in the room.
func (*RoomContext) BroadcastExcept ¶
func (r *RoomContext) BroadcastExcept(userID string, payload []byte) error
BroadcastExcept delivers payload to every live peer except userID.
func (*RoomContext) ResetTimer ¶
func (r *RoomContext) ResetTimer(name string, duration time.Duration) error
ResetTimer (re)schedules a named timer for the room this RoomContext belongs to. Only valid while handling an Event or computing a Snapshot — see RoomContext's doc comment.