|
|
|
package room
|
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/rs/xid"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
|
|
|
// internalClient is used by the room itself to track information about a client.
|
|
|
|
type internalClient struct {
|
|
|
|
// id is the id that the client identifies itself with in all clientMessage instances it sends.
|
|
|
|
id xid.ID
|
|
|
|
// outgoingChannel is a channel that the room can send messages to the client on.
|
|
|
|
outgoingChannel chan<- Message
|
|
|
|
// privateChannel is true iff the room can close the outgoingChannel when the client and room have completed their
|
|
|
|
// close handshake.
|
|
|
|
privateChannel bool
|
|
|
|
// broadcast is true iff the client requested to be included on broadcasts on creation.
|
|
|
|
broadcast bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type NewClientOptions struct {
|
|
|
|
// IncomingChannel is the channel to use as the room's channel to send messages to - the new Client's IncomingChannel.
|
|
|
|
// If this is non-nil, the room will not automatically close the IncomingChannel after a shutdown is negotiated.
|
|
|
|
// If this is nil, a new channel will be allocated on join and closed on shutdown.
|
|
|
|
IncomingChannel chan Message
|
|
|
|
// If AcceptBroadcasts is true, the room will send all broadcasts originating from other clients to this client.
|
|
|
|
AcceptBroadcasts bool
|
|
|
|
// If RequestStartingState is true, the room will send a copy of the current state as of when the JoinRequest was
|
|
|
|
// received in the JoinResponse that will be the first message the Client receives.
|
|
|
|
RequestStartingState bool
|
|
|
|
// If sets,
|
|
|
|
Logger *zap.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client is the structure used by clients external to the Room package to communicate with the Room.
|
|
|
|
// It is not expected to be parallel-safe; to run it in parallel, use the NewClient method and send the new client to
|
|
|
|
// the new goroutine.
|
|
|
|
type Client struct {
|
|
|
|
// id is the ClientID used by the client for all communications.
|
|
|
|
id xid.ID
|
|
|
|
// roomId is the unique ID of the room (not its map).
|
|
|
|
roomId xid.ID
|
|
|
|
// incomingChannel is the channel that this client receives messages on.
|
|
|
|
incomingChannel <-chan Message
|
|
|
|
// outgoingChannel is the channel that this client sends messages on.
|
|
|
|
// Becomes nil if the client has been completely shut down.
|
|
|
|
outgoingChannel chan<- ClientMessage
|
|
|
|
// Once Leave or AcknowledgeShutdown have been triggered, this flag is set, preventing use of other messages.
|
|
|
|
shuttingDown bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// ID is the ID used by this client to identify itself to the Room.
|
|
|
|
func (c *Client) ID() xid.ID {
|
|
|
|
return c.id
|
|
|
|
}
|
|
|
|
|
|
|
|
// RoomID is the ID used by the room to differentiate itself from other rooms.
|
|
|
|
// It is not the map's internal ID.
|
|
|
|
func (c *Client) RoomID() xid.ID {
|
|
|
|
return c.id
|
|
|
|
}
|
|
|
|
|
|
|
|
// IncomingChannel is the channel the client can listen on for messages from the room.
|
|
|
|
func (c *Client) IncomingChannel() <-chan Message {
|
|
|
|
return c.incomingChannel
|
|
|
|
}
|
|
|
|
|
|
|
|
// OutgoingChannel is the channel the client can send messages to the room on.
|
|
|
|
func (c *Client) OutgoingChannel() chan<- ClientMessage {
|
|
|
|
if c.outgoingChannel == nil {
|
|
|
|
panic("Already finished shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
return c.outgoingChannel
|
|
|
|
}
|
|
|
|
|
|
|
|
// newClientForRoom uses the necessary parameters to create and join a Client for the given room.
|
|
|
|
func newClientForRoom(roomId xid.ID, outgoingChannel chan<- ClientMessage, opts NewClientOptions) *Client {
|
|
|
|
var privateChannel bool
|
|
|
|
var incomingChannel chan Message
|
|
|
|
if opts.IncomingChannel != nil {
|
|
|
|
incomingChannel = opts.IncomingChannel
|
|
|
|
privateChannel = false
|
|
|
|
} else {
|
|
|
|
incomingChannel = make(chan Message, 1)
|
|
|
|
privateChannel = true
|
|
|
|
}
|
|
|
|
result := Client{
|
|
|
|
id: xid.New(),
|
|
|
|
roomId: roomId,
|
|
|
|
incomingChannel: incomingChannel,
|
|
|
|
outgoingChannel: outgoingChannel,
|
|
|
|
shuttingDown: false,
|
|
|
|
}
|
|
|
|
result.outgoingChannel <- JoinRequest{
|
|
|
|
id: result.id,
|
|
|
|
returnChannel: incomingChannel,
|
|
|
|
privateChannel: privateChannel,
|
|
|
|
broadcast: opts.AcceptBroadcasts,
|
|
|
|
wantCurrentState: opts.RequestStartingState,
|
|
|
|
}
|
|
|
|
return &result
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient creates a new client belonging to the same room as this client with a random ID.
|
|
|
|
// The new client will be automatically joined to the channel.
|
|
|
|
func (c *Client) NewClient(opts NewClientOptions) *Client {
|
|
|
|
if c.shuttingDown {
|
|
|
|
panic("Already started shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
return newClientForRoom(c.roomId, c.outgoingChannel, opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh creates a message which causes the client to request a fresh copy of the state.
|
|
|
|
func (c *Client) Refresh() RefreshRequest {
|
|
|
|
if c.shuttingDown {
|
|
|
|
panic("Already started shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
return RefreshRequest{
|
|
|
|
id: c.id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Leave creates a message which causes the local client to signal that it is shutting down.
|
|
|
|
// It is important to Leave to avoid dangling clients having messages sent to nothing.
|
|
|
|
// After sending Leave, the client must confirm that it has been removed by waiting for a LeaveResponse, accompanied by
|
|
|
|
// the closing of the Client's IncomingChannel if it was a private channel.
|
|
|
|
// No further messages should be sent after Leave except AcknowledgeShutdown if Leave and requestShutdown crossed paths in midair.
|
|
|
|
func (c *Client) Leave() LeaveRequest {
|
|
|
|
if c.shuttingDown {
|
|
|
|
panic("Already started shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
c.shuttingDown = true
|
|
|
|
return LeaveRequest{
|
|
|
|
id: c.id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop creates a message which causes the local client to signal that it is shutting down.
|
|
|
|
// It is important to Stop when the room needs to be shut down.
|
|
|
|
// After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should
|
|
|
|
// be handled normally.
|
|
|
|
// No further messages should be sent after Stop except AcknowledgeShutdown.
|
|
|
|
func (c *Client) Stop() StopRequest {
|
|
|
|
if c.shuttingDown {
|
|
|
|
panic("Already started shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
c.shuttingDown = true
|
|
|
|
return StopRequest{
|
|
|
|
id: c.id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AcknowledgeShutdown causes the local client to signal that it has acknowledged that the room is shutting down.
|
|
|
|
// No further messages can be sent after AcknowledgeShutdown; attempting to do so will block forever, as the
|
|
|
|
// OutgoingChannel has become nil.
|
|
|
|
func (c *Client) AcknowledgeShutdown() ShutdownResponse {
|
|
|
|
if c.outgoingChannel == nil {
|
|
|
|
panic("Already finished shutting down; no new messages should be sent")
|
|
|
|
}
|
|
|
|
c.shuttingDown = true
|
|
|
|
c.outgoingChannel = nil
|
|
|
|
return ShutdownResponse{
|
|
|
|
id: c.id,
|
|
|
|
}
|
|
|
|
}
|