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) } // The message created by Refresh 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, } } // The message created by Leave 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, } } // The message created by Stop 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, } }