Hacky TURBO CODING MODE minimum viable project

main
Mari 3 years ago
parent 34e64c7130
commit ab29ec20be
  1. 4
      client/src/App.tsx
  2. 4
      client/src/actions/ServerAction.ts
  3. 1
      client/src/reducers/ServerReducer.ts
  4. 14
      client/src/state/Coordinates.ts
  5. 12
      client/src/state/HexMap.ts
  6. 4
      client/src/ui/HexColorPicker.tsx
  7. 4
      client/src/websocket/MapFromPb.ts
  8. 13
      client/src/websocket/WebsocketReactAdapter.tsx
  9. 5
      client/src/websocket/WebsocketTranslator.ts
  10. 13
      server/action/action.go
  11. 193
      server/host/HttpServer.go
  12. 38
      server/room/actor.go
  13. 32
      server/room/client.go
  14. 30
      server/room/clientmessage.go
  15. 47
      server/room/message.go
  16. 35
      server/state/map.go
  17. 37
      server/websocket/connection.go
  18. 17
      server/ws/client.go
  19. 49
      server/ws/client.pbconv.go
  20. 276
      server/ws/connection.go
  21. 21
      server/ws/reader.go
  22. 2
      server/ws/server.go
  23. 40
      server/ws/server.pbconv.go
  24. 15
      server/ws/shared.go
  25. 16
      server/ws/writer.go

@ -34,11 +34,11 @@ function App() {
top: 10, top: 10,
left: 10, left: 10,
size: 50, size: 50,
displayMode: map.layout layout: map.layout
} : null), [map]) } : null), [map])
const mapElement = !!offsets && !!map ? <HexMapRenderer map={map} offsets={offsets} /> : null; const mapElement = !!offsets && !!map ? <HexMapRenderer map={map} offsets={offsets} /> : null;
const {width, height} = useMemo(() => !!offsets && !!map ? sizeFromLinesAndCells({ const {width, height} = useMemo(() => !!offsets && !!map ? sizeFromLinesAndCells({
bottomMargin: 10, cells: map.cellsPerLine, lines: map.lines, offsets: offsets, rightMargin: 10 bottomMargin: 10, cells: map.cellsPerLine, lines: map.lines, offsets: offsets, rightMargin: 10,
}) : {width: 1, height: 1}, [map, offsets]); }) : {width: 1, height: 1}, [map, offsets]);
const colorPickerElement = !!user ? <HexColorPicker color={user?.activeColor} onChangeComplete={(colorResult) => dispatch({ const colorPickerElement = !!user ? <HexColorPicker color={user?.activeColor} onChangeComplete={(colorResult) => dispatch({
type: USER_ACTIVE_COLOR, type: USER_ACTIVE_COLOR,

@ -75,7 +75,7 @@ export enum SocketState {
OPEN = "OPEN", OPEN = "OPEN",
} }
export const SERVER_SOCKET_STARTUP = "SERVER_SOCKET_STARTUP" export const SERVER_SOCKET_STARTUP = "SERVER_SOCKET_STARTUP"
/** Synthesized when the websocket begins connecting, i.e., enters the Connecting or Open states.. */ /** Synthesized when the ws begins connecting, i.e., enters the Connecting or Open states.. */
export interface ServerSocketStartupAction extends BaseAction { export interface ServerSocketStartupAction extends BaseAction {
readonly type: typeof SERVER_SOCKET_STARTUP readonly type: typeof SERVER_SOCKET_STARTUP
readonly state: SocketState readonly state: SocketState
@ -95,7 +95,7 @@ export function isServerActCommand(action: AppAction): action is ServerActComman
} }
export const SERVER_MALFORMED = "SERVER_MALFORMED" export const SERVER_MALFORMED = "SERVER_MALFORMED"
/** Synthesized when the client can't understand a command the server sent, or when an error event appears on the websocket. */ /** Synthesized when the client can't understand a command the server sent, or when an error event appears on the ws. */
export interface ServerMalformedAction extends BaseAction { export interface ServerMalformedAction extends BaseAction {
readonly type: typeof SERVER_MALFORMED, readonly type: typeof SERVER_MALFORMED,
readonly error: Error | null, readonly error: Error | null,

@ -239,6 +239,7 @@ function serverActReducer(oldState: AppState, action: ServerActCommand): AppStat
} }
function serverMalformedReducer(oldState: NetworkState, action: ServerMalformedAction): NetworkState { function serverMalformedReducer(oldState: NetworkState, action: ServerMalformedAction): NetworkState {
console.log("Got serverMalformed", action.error)
return clientReducer(oldState, { return clientReducer(oldState, {
type: CLIENT_GOODBYE, type: CLIENT_GOODBYE,
code: 1002, // protocol error code: 1002, // protocol error

@ -54,7 +54,7 @@ export interface RenderOffsets {
/** How big each hexagon should be. The "radius" from the center to any vertex. */ /** How big each hexagon should be. The "radius" from the center to any vertex. */
readonly size: number readonly size: number
/** The way the hex map should be displayed. Usually the same as the origin map. */ /** The way the hex map should be displayed. Usually the same as the origin map. */
readonly displayMode: HexLayout readonly layout: HexLayout
} }
export interface RenderSize { export interface RenderSize {
@ -66,7 +66,7 @@ export interface RenderSize {
const POINTY_AXIS_FACTOR = 2; const POINTY_AXIS_FACTOR = 2;
const FLAT_AXIS_FACTOR = Math.sqrt(3); const FLAT_AXIS_FACTOR = Math.sqrt(3);
export function storageCoordinatesToRenderCoordinates({line, cell}: StorageCoordinates, renderOffsets: RenderOffsets): RenderCoordinates { export function storageCoordinatesToRenderCoordinates({line, cell}: StorageCoordinates, renderOffsets: RenderOffsets): RenderCoordinates {
const {orientation, indentedLines} = renderOffsets.displayMode; const {orientation, indentedLines} = renderOffsets.layout;
const flatLength = renderOffsets.size * FLAT_AXIS_FACTOR; const flatLength = renderOffsets.size * FLAT_AXIS_FACTOR;
const pointyLength = renderOffsets.size * POINTY_AXIS_FACTOR; const pointyLength = renderOffsets.size * POINTY_AXIS_FACTOR;
@ -105,7 +105,7 @@ export function sizeFromLinesAndCells({offsets, lines, cells, rightMargin = 0, b
const { const {
top: topMargin, top: topMargin,
left: leftMargin, left: leftMargin,
displayMode: { layout: {
orientation, orientation,
indentedLines indentedLines
} }
@ -118,12 +118,12 @@ export function sizeFromLinesAndCells({offsets, lines, cells, rightMargin = 0, b
const hasIndents = lines > 1 || indentedLines === LineParity.EVEN const hasIndents = lines > 1 || indentedLines === LineParity.EVEN
// Every line will be 3/4 of a cell length apart in the pointy direction; // Every cell will be 3/4 of a cell length apart in the pointy direction;
// however, the last line is still one full pointy cell long. // however, the last line is still one full pointy cell long.
const pointyLength = pointyMargins + ((cells - 1) * pointyCellLength * 3 / 4) + pointyCellLength; const pointyLength = pointyMargins + ((lines - 1) * pointyCellLength * 3 / 4) + pointyCellLength;
// Every cell will be one full cell length apart in the flat direction; // Every line will be one full cell length apart in the flat direction;
// however, if there are indents, another half cell is needed to accommodate them. // however, if there are indents, another half cell is needed to accommodate them.
const flatLength = flatMargins + lines * flatCellLength + (hasIndents ? flatCellLength / 2 : 0); const flatLength = flatMargins + cells * flatCellLength + (hasIndents ? flatCellLength / 2 : 0);
return orientation === HexagonOrientation.FLAT_TOP ? { width: pointyLength, height: flatLength } : { width: flatLength, height: pointyLength } return orientation === HexagonOrientation.FLAT_TOP ? { width: pointyLength, height: flatLength } : { width: flatLength, height: pointyLength }
} }

@ -71,20 +71,20 @@ export interface HexMap {
readonly xid: string readonly xid: string
} }
export function initializeMap({lines, cellsPerLine, displayMode, xid}: {lines: number, cellsPerLine: number, displayMode: HexLayout, xid: string}): HexMap { export function initializeMap({lines, cellsPerLine, layout, xid}: {lines: number, cellsPerLine: number, layout: HexLayout, xid: string}): HexMap {
const lineCells: HexLine[] = []; const layer: HexLine[] = [];
const emptyLine: HexCell[] = []; const emptyLine: HexCell[] = [];
for (let cell = 0; cell < cellsPerLine; cell += 1) { for (let cell = 0; cell < cellsPerLine; cell += 1) {
emptyLine.push(EMPTY_CELL) emptyLine.push(EMPTY_CELL)
} }
for (let line = 0; line < lines; line += 1) { for (let line = 0; line < lines; line += 1) {
lineCells.push(emptyLine) layer.push(emptyLine)
} }
return { return {
lines, lines,
cellsPerLine: cellsPerLine, cellsPerLine,
layout: displayMode, layout,
layer: lineCells, layer,
xid xid
} }
} }

@ -20,7 +20,7 @@ function HexSwatch({color, index, offsets, classNames, onClick}: {color: string,
} }
const ACTIVE_OFFSETS: RenderOffsets = { const ACTIVE_OFFSETS: RenderOffsets = {
displayMode: { layout: {
orientation: HexagonOrientation.POINTY_TOP, orientation: HexagonOrientation.POINTY_TOP,
indentedLines: LineParity.ODD indentedLines: LineParity.ODD
}, },
@ -30,7 +30,7 @@ const ACTIVE_OFFSETS: RenderOffsets = {
} }
const SWATCH_OFFSETS: RenderOffsets = { const SWATCH_OFFSETS: RenderOffsets = {
displayMode: { layout: {
orientation: HexagonOrientation.FLAT_TOP, orientation: HexagonOrientation.FLAT_TOP,
indentedLines: LineParity.EVEN indentedLines: LineParity.EVEN
}, },

@ -65,8 +65,8 @@ function mapFromPb(map: HexMapPB): HexMap {
} }
return { return {
xid: encode(map.xid), xid: encode(map.xid),
lines: 0, lines: map.lines,
cellsPerLine: 0, cellsPerLine: map.cellsPerLine,
layout: layoutFromPb(map.layout), layout: layoutFromPb(map.layout),
layer: map.layer.lines.map((line): HexCell[] => { layer: map.layer.lines.map((line): HexCell[] => {
return line.cells.map(cellFromPb) return line.cells.map(cellFromPb)

@ -5,9 +5,9 @@ import {
ClientActCommand, ClientActCommand,
ClientGoodbyeAction, ClientGoodbyeAction,
ClientHelloCommand, ClientHelloCommand,
ClientRefreshCommand, ClientRefreshCommand, isClientGoodbyeCommand,
isClientCommand, isClientHelloCommand,
isClientGoodbyeCommand, isClientRefreshCommand,
SendableAction, SendableAction,
SentAction SentAction
} from "../actions/ClientAction"; } from "../actions/ClientAction";
@ -58,10 +58,11 @@ export function WebsocketReactAdapter({url, protocols = ["v1.hexmap.deliciousrey
} }
}, [nextID, dispatch, pendingMessages, state]) }, [nextID, dispatch, pendingMessages, state])
useEffect(() => { useEffect(() => {
if (state === ServerConnectionState.CONNECTED && specialMessage !== null && specialMessage !== lastSpecialMessage) { if (specialMessage !== null && specialMessage !== lastSpecialMessage) {
if (isClientCommand(specialMessage)) { if ((state === ServerConnectionState.AWAITING_HELLO && isClientHelloCommand(specialMessage))
|| (state === ServerConnectionState.AWAITING_REFRESH && isClientRefreshCommand(specialMessage))) {
connector.current.send(specialMessage); connector.current.send(specialMessage);
} else if (isClientGoodbyeCommand(specialMessage)) { } else if (state === ServerConnectionState.AWAITING_GOODBYE && isClientGoodbyeCommand(specialMessage)) {
connector.current.close(specialMessage.code, specialMessage.reason) connector.current.close(specialMessage.code, specialMessage.reason)
} }
setLastSpecialMessage(specialMessage); setLastSpecialMessage(specialMessage);

@ -1,4 +1,4 @@
/** Translates between websocket messages and Commands. */ /** Translates between ws messages and Commands. */
import {ClientCommand} from "../actions/ClientAction"; import {ClientCommand} from "../actions/ClientAction";
import { import {
SERVER_GOODBYE, SERVER_GOODBYE,
@ -71,6 +71,9 @@ export class WebsocketTranslator {
} }
close(code: number, reason: string) { close(code: number, reason: string) {
if (this.socket === null || this.socket.readyState !== WebSocket.OPEN) {
return
}
this.socket?.close(code, reason) this.socket?.close(code, reason)
} }

@ -110,3 +110,16 @@ func (c UserActiveColor) Apply(s *state.Synced) error {
s.User.ActiveColor = c.Color s.User.ActiveColor = c.Color
return nil return nil
} }
// IDed contains a pair of ID and Action, as sent by the client.
type IDed struct {
// ID contains the arbitrary ID that was sent by the client, for identifying the action in future messages.
ID uint32 `json:"id"`
// Action contains the action that was actually being sent.
Action Client `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddUint32("id", i.ID)
return encoder.AddObject("action", i.Action)
}

@ -0,0 +1,193 @@
package main
import (
"context"
"git.reya.zone/reya/hexmap/server/room"
"git.reya.zone/reya/hexmap/server/state"
"git.reya.zone/reya/hexmap/server/ws"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
const SaveDir = "/home/reya/hexmaps"
func save(m state.HexMap, l *zap.Logger) error {
filename := filepath.Join(SaveDir, "map."+strconv.FormatInt(time.Now().Unix(), 16))
l.Debug("Saving to file", zap.String("filename", filename))
marshaled, err := proto.Marshal(m.ToPB())
l.Debug("Marshaled proto")
if err != nil {
return err
}
l.Debug("Opening file")
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0x644)
if err != nil {
return err
}
l.Debug("Writing to file")
_, err = file.Write(marshaled)
if err != nil {
return err
}
l.Debug("Closing file")
err = file.Close()
if err != nil {
return err
}
l.Info("Saved to file", zap.String("filename", filename))
return nil
}
func load(l *zap.Logger) (*state.HexMap, error) {
filename := filepath.Join(SaveDir, "map.LOAD")
l.Debug("Loading from file", zap.String("filename", filename))
file, err := os.Open(filename)
if err != nil {
return nil, err
}
l.Debug("Reading file")
marshaled, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
pb := &state.HexMapPB{}
l.Debug("Extracting protobuf from file")
err = proto.Unmarshal(marshaled, pb)
if err != nil {
return nil, err
}
l.Debug("Closing file")
err = file.Close()
if err != nil {
return nil, err
}
m, err := pb.ToGo()
if err != nil {
return nil, err
}
l.Info("Loaded from file", zap.String("filename", filename))
return &m, nil
}
func BackupMap(client *room.Client, l *zap.Logger) {
var err error
myState := &state.Synced{}
l.Info("Starting backup system")
for {
msg := <-client.IncomingChannel()
switch typedMsg := msg.(type) {
case *room.JoinResponse:
myState = typedMsg.CurrentState()
err := save(myState.Map, l)
if err != nil {
l.Error("Failed saving during join response", zap.Error(err))
}
case *room.ActionBroadcast:
err = typedMsg.Action().Apply(myState)
if err == nil {
err = save(myState.Map, l)
if err != nil {
l.Error("Failed saving during action broadcast", zap.Error(err))
}
}
case *room.ShutdownRequest:
client.OutgoingChannel() <- client.AcknowledgeShutdown()
return
}
}
}
func ServeWS(logger *zap.Logger) (err error) {
m := http.NewServeMux()
httpLogger := logger.Named("HTTP")
hexes, err := load(logger)
if err != nil {
hexes = state.NewHexMap(state.Layout{
Orientation: state.PointyTop,
IndentedLines: state.EvenLines,
}, 25, 10)
}
rm := room.New(room.NewOptions{
BaseLogger: logger.Named("Room"),
StartingState: &state.Synced{
Map: *hexes,
User: state.UserState{
ActiveColor: state.Color{
R: 0,
G: 0,
B: 0,
A: 255,
},
},
},
StartingClientOptions: room.NewClientOptions{
IncomingChannel: nil,
AcceptBroadcasts: true,
RequestStartingState: true,
},
})
go BackupMap(rm, logger.Named("BackupMap"))
m.Handle("/map", &ws.HTTPHandler{
Upgrader: websocket.Upgrader{
Subprotocols: []string{"v1.hexmap.deliciousreya.net"},
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://hexmap.deliciousreya.net"
},
},
Logger: logger.Named("WS"),
Room: rm,
})
srv := http.Server{
Addr: "127.0.0.1:5238",
Handler: m,
ErrorLog: zap.NewStdLog(httpLogger),
}
m.HandleFunc("/exit", func(writer http.ResponseWriter, request *http.Request) {
// Some light dissuasion of accidental probing.
// To keep good people out.
if request.FormValue("superSecretPassword") != "Gesture/Retrial5/Untrained/Countable/Extrude/Jeep/Cheese/Carbon" {
writer.WriteHeader(403)
_, err = writer.Write([]byte("... What are you trying to pull?"))
return
}
writer.WriteHeader(200)
_, err := writer.Write([]byte("OK, shutting down, bye!"))
if err != nil {
logger.Warn("Error while writing goodbye response", zap.Error(err))
}
time.AfterFunc(500*time.Millisecond, func() {
err := srv.Shutdown(context.Background())
if err != nil {
logger.Error("Error while shutting down the server", zap.Error(err))
}
})
})
err = srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
return err
}
rm.OutgoingChannel() <- rm.Stop()
for {
msg := <-rm.IncomingChannel()
switch msg.(type) {
case *room.ShutdownRequest:
rm.OutgoingChannel() <- rm.AcknowledgeShutdown()
return nil
}
}
}
func main() {
logger, err := zap.NewDevelopment()
err = ServeWS(logger)
if err != nil {
logger.Fatal("Error while serving HTTP", zap.Error(err))
}
}

@ -18,28 +18,28 @@ func (r *room) act() {
msgLogger := r.logger.With(zap.Stringer("client", client)) msgLogger := r.logger.With(zap.Stringer("client", client))
msgLogger.Debug("Message received, handling", zap.Object("message", raw)) msgLogger.Debug("Message received, handling", zap.Object("message", raw))
switch msg := raw.(type) { switch msg := raw.(type) {
case JoinRequest: case *JoinRequest:
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel) r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel)
r.acknowledgeJoin(msg.id, msg.wantCurrentState) r.acknowledgeJoin(msg.id, msg.wantCurrentState)
case RefreshRequest: case *RefreshRequest:
r.sendRefresh(msg.id) r.sendRefresh(msg.id)
case ApplyRequest: case *ApplyRequest:
msgLogger.Debug("Received action to apply from client", zap.Uint32("actionId", msg.action.ID)) msgLogger.Debug("Received action to apply from client", zap.Uint32("actionId", msg.action.ID))
result := r.applyAction(msg.action.Action) result := r.applyAction(msg.action.Action)
if result != nil { if result == nil {
r.broadcastAction(client, msg.action.ID, msg.action.Action) r.broadcastAction(client, msg.action.ID, msg.action.Action)
} }
r.acknowledgeAction(client, msg.action.ID, result) r.acknowledgeAction(client, msg.action.ID, result)
case LeaveRequest: case *LeaveRequest:
// So long, then. We can close immediately here; they promised not to send any more messages after this // So long, then. We can close immediately here; they promised not to send any more messages after this
// unless we were shutting down, which we're not. // unless we were shutting down, which we're not.
r.acknowledgeLeave(client) r.acknowledgeLeave(client)
r.closeClient(client) r.closeClient(client)
case StopRequest: case *StopRequest:
// As requested, we shut down. Our deferred gracefulShutdown will catch us as we fall. // As requested, we shut down. Our deferred gracefulShutdown will catch us as we fall.
msgLogger.Info("Received StopRequest from client, shutting down") msgLogger.Info("Received StopRequest from client, shutting down")
return return
case ShutdownResponse: case *ShutdownResponse:
// Uh... thank... you. I'm not... Never mind. I guess this means you're leaving? // Uh... thank... you. I'm not... Never mind. I guess this means you're leaving?
msgLogger.Error("Received unexpected ShutdownResponse from client while not shutting down") msgLogger.Error("Received unexpected ShutdownResponse from client while not shutting down")
r.closeClient(client) r.closeClient(client)
@ -95,7 +95,7 @@ func (r *room) acknowledgeJoin(id xid.ID, includeState bool) {
s = r.stateCopy() s = r.stateCopy()
} }
logger.Debug("Sending JoinResponse to client") logger.Debug("Sending JoinResponse to client")
client.outgoingChannel <- JoinResponse{ client.outgoingChannel <- &JoinResponse{
id: r.id, id: r.id,
currentState: s, currentState: s,
} }
@ -113,7 +113,7 @@ func (r *room) sendRefresh(id xid.ID) {
logger.Debug("Preparing state copy for client") logger.Debug("Preparing state copy for client")
s = r.stateCopy() s = r.stateCopy()
logger.Debug("Sending RefreshResponse to client") logger.Debug("Sending RefreshResponse to client")
client.outgoingChannel <- RefreshResponse{ client.outgoingChannel <- &RefreshResponse{
id: r.id, id: r.id,
currentState: s, currentState: s,
} }
@ -126,7 +126,7 @@ func (r *room) applyAction(action action.Action) error {
} }
// broadcastAction sends an action to everyone other than the original client which requested it. // broadcastAction sends an action to everyone other than the original client which requested it.
func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Action) { func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Server) {
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action)) logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action))
broadcast := ActionBroadcast{ broadcast := ActionBroadcast{
id: r.id, id: r.id,
@ -138,7 +138,7 @@ func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32,
for id, client := range r.clients { for id, client := range r.clients {
if id.Compare(originalClientID) != 0 { if id.Compare(originalClientID) != 0 {
logger.Debug("Sending ActionBroadcast to client", zap.Stringer("client", id)) logger.Debug("Sending ActionBroadcast to client", zap.Stringer("client", id))
client.outgoingChannel <- broadcast client.outgoingChannel <- &broadcast
} }
} }
} }
@ -153,7 +153,7 @@ func (r *room) acknowledgeAction(id xid.ID, actionID uint32, error error) {
return return
} }
logger.Debug("Sending ApplyResponse to client") logger.Debug("Sending ApplyResponse to client")
client.outgoingChannel <- ApplyResponse{ client.outgoingChannel <- &ApplyResponse{
id: id, id: id,
actionID: actionID, actionID: actionID,
result: error, result: error,
@ -171,7 +171,7 @@ func (r *room) acknowledgeLeave(id xid.ID) {
return return
} }
logger.Debug("Sending LeaveResponse to client") logger.Debug("Sending LeaveResponse to client")
client.outgoingChannel <- LeaveResponse{id: r.id} client.outgoingChannel <- &LeaveResponse{id: r.id}
} }
// closeClient causes the room to remove the client with the given id from its clients. // closeClient causes the room to remove the client with the given id from its clients.
@ -206,25 +206,25 @@ func (r *room) gracefulShutdown() {
msgLogger := r.logger.With(zap.Stringer("client", client)) msgLogger := r.logger.With(zap.Stringer("client", client))
msgLogger.Debug("Post-shutdown message received, handling", zap.Object("message", raw)) msgLogger.Debug("Post-shutdown message received, handling", zap.Object("message", raw))
switch msg := raw.(type) { switch msg := raw.(type) {
case JoinRequest: case *JoinRequest:
// Don't you hate it when someone comes to the desk right as you're getting ready to pack up? // Don't you hate it when someone comes to the desk right as you're getting ready to pack up?
// Can't ignore them - they have our channel, and they might be sending things to it. We have to add them // Can't ignore them - they have our channel, and they might be sending things to it. We have to add them
// and then immediately send them a ShutdownRequest and wait for them to answer it. // and then immediately send them a ShutdownRequest and wait for them to answer it.
msgLogger.Debug("Received join request from client while shutting down") msgLogger.Debug("Received join request from client while shutting down")
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel) r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel)
r.requestShutdownFrom(client) r.requestShutdownFrom(client)
case RefreshRequest: case *RefreshRequest:
// Ugh, seriously, now? Fine. You can have this - you might be our friend the persistence actor. // Ugh, seriously, now? Fine. You can have this - you might be our friend the persistence actor.
r.sendRefresh(client) r.sendRefresh(client)
case LeaveRequest: case *LeaveRequest:
// We sent them a shutdown already, so unfortunately, we can't close them immediately. We have to wait for // We sent them a shutdown already, so unfortunately, we can't close them immediately. We have to wait for
// them to tell us they've heard that we're shutting down. // them to tell us they've heard that we're shutting down.
msgLogger.Debug("Received leave request from client while shutting down") msgLogger.Debug("Received leave request from client while shutting down")
r.acknowledgeLeave(client) r.acknowledgeLeave(client)
case StopRequest: case *StopRequest:
// Yes. We're doing that. Check your inbox, I already sent you the shutdown. // Yes. We're doing that. Check your inbox, I already sent you the shutdown.
msgLogger.Debug("Received stop request from client while shutting down") msgLogger.Debug("Received stop request from client while shutting down")
case ShutdownResponse: case *ShutdownResponse:
// The only way we would be getting one of these is if the client knows it doesn't have to send us anything // The only way we would be getting one of these is if the client knows it doesn't have to send us anything
// else. Therefore, we can remove them now. // else. Therefore, we can remove them now.
// Similarly, we know that they'll receive the LeaveResponse they need and shut down. // Similarly, we know that they'll receive the LeaveResponse they need and shut down.
@ -261,7 +261,7 @@ func (r *room) requestShutdownFrom(id xid.ID) {
if !ok { if !ok {
r.logger.Error("No such client when requesting shutdown from client", clientField) r.logger.Error("No such client when requesting shutdown from client", clientField)
} }
client.outgoingChannel <- shutdown client.outgoingChannel <- &shutdown
} }
// finalShutdown causes the room to do any final cleanup not involving its clients before stopping. // finalShutdown causes the room to do any final cleanup not involving its clients before stopping.

@ -1,8 +1,8 @@
package room package room
import ( import (
"git.reya.zone/reya/hexmap/server/action"
"github.com/rs/xid" "github.com/rs/xid"
"go.uber.org/zap"
) )
// internalClient is used by the room itself to track information about a client. // internalClient is used by the room itself to track information about a client.
@ -28,8 +28,6 @@ type NewClientOptions struct {
// If RequestStartingState is true, the room will send a copy of the current state as of when the JoinRequest was // 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. // received in the JoinResponse that will be the first message the Client receives.
RequestStartingState bool RequestStartingState bool
// If sets,
Logger *zap.Logger
} }
// Client is the structure used by clients external to the Room package to communicate with the Room. // Client is the structure used by clients external to the Room package to communicate with the Room.
@ -91,7 +89,7 @@ func newClientForRoom(roomId xid.ID, outgoingChannel chan<- ClientMessage, opts
outgoingChannel: outgoingChannel, outgoingChannel: outgoingChannel,
shuttingDown: false, shuttingDown: false,
} }
result.outgoingChannel <- JoinRequest{ result.outgoingChannel <- &JoinRequest{
id: result.id, id: result.id,
returnChannel: incomingChannel, returnChannel: incomingChannel,
privateChannel: privateChannel, privateChannel: privateChannel,
@ -111,26 +109,36 @@ func (c *Client) NewClient(opts NewClientOptions) *Client {
} }
// Refresh creates a message which causes the client to request a fresh copy of the state. // Refresh creates a message which causes the client to request a fresh copy of the state.
func (c *Client) Refresh() RefreshRequest { func (c *Client) Refresh() *RefreshRequest {
if c.shuttingDown { if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent") panic("Already started shutting down; no new messages should be sent")
} }
return RefreshRequest{ return &RefreshRequest{
id: c.id, id: c.id,
} }
} }
func (c *Client) Apply(a action.IDed) *ApplyRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
return &ApplyRequest{
id: c.id,
action: a,
}
}
// Leave creates a message which causes the local client to signal that it is shutting down. // 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. // 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 // 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. // 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. // No further messages should be sent after Leave except AcknowledgeShutdown if Leave and requestShutdown crossed paths in midair.
func (c *Client) Leave() LeaveRequest { func (c *Client) Leave() *LeaveRequest {
if c.shuttingDown { if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent") panic("Already started shutting down; no new messages should be sent")
} }
c.shuttingDown = true c.shuttingDown = true
return LeaveRequest{ return &LeaveRequest{
id: c.id, id: c.id,
} }
} }
@ -140,12 +148,12 @@ func (c *Client) Leave() LeaveRequest {
// After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should // After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should
// be handled normally. // be handled normally.
// No further messages should be sent after Stop except AcknowledgeShutdown. // No further messages should be sent after Stop except AcknowledgeShutdown.
func (c *Client) Stop() StopRequest { func (c *Client) Stop() *StopRequest {
if c.shuttingDown { if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent") panic("Already started shutting down; no new messages should be sent")
} }
c.shuttingDown = true c.shuttingDown = true
return StopRequest{ return &StopRequest{
id: c.id, id: c.id,
} }
} }
@ -153,13 +161,13 @@ func (c *Client) Stop() StopRequest {
// AcknowledgeShutdown causes the local client to signal that it has acknowledged that the room is shutting down. // 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 // No further messages can be sent after AcknowledgeShutdown; attempting to do so will block forever, as the
// OutgoingChannel has become nil. // OutgoingChannel has become nil.
func (c *Client) AcknowledgeShutdown() ShutdownResponse { func (c *Client) AcknowledgeShutdown() *ShutdownResponse {
if c.outgoingChannel == nil { if c.outgoingChannel == nil {
panic("Already finished shutting down; no new messages should be sent") panic("Already finished shutting down; no new messages should be sent")
} }
c.shuttingDown = true c.shuttingDown = true
c.outgoingChannel = nil c.outgoingChannel = nil
return ShutdownResponse{ return &ShutdownResponse{
id: c.id, id: c.id,
} }
} }

@ -1,7 +1,7 @@
package room package room
import ( import (
"git.reya.zone/reya/hexmap/server/websocket" action2 "git.reya.zone/reya/hexmap/server/action"
"github.com/rs/xid" "github.com/rs/xid"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@ -31,7 +31,7 @@ type JoinRequest struct {
wantCurrentState bool wantCurrentState bool
} }
func (j JoinRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (j *JoinRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinRequest") encoder.AddString("type", "JoinRequest")
encoder.AddString("id", j.id.String()) encoder.AddString("id", j.id.String())
encoder.AddBool("broadcast", j.broadcast) encoder.AddBool("broadcast", j.broadcast)
@ -39,7 +39,7 @@ func (j JoinRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil return nil
} }
func (j JoinRequest) ClientID() xid.ID { func (j *JoinRequest) ClientID() xid.ID {
return j.id return j.id
} }
@ -48,30 +48,30 @@ type RefreshRequest struct {
id xid.ID id xid.ID
} }
func (r RefreshRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (r *RefreshRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "RefreshRequest") encoder.AddString("type", "RefreshRequest")
encoder.AddString("id", r.id.String()) encoder.AddString("id", r.id.String())
return nil return nil
} }
func (r RefreshRequest) ClientID() xid.ID { func (r *RefreshRequest) ClientID() xid.ID {
return r.id return r.id
} }
// ApplyRequest is the message sent on the room's IncomingChannel by a client which has received an action from the // ApplyRequest is the message sent on the room's IncomingChannel by a client which has received an action from the
// websocket. // ws.
type ApplyRequest struct { type ApplyRequest struct {
id xid.ID id xid.ID
action websocket.IDed action action2.IDed
} }
func (f ApplyRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (f *ApplyRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyRequest") encoder.AddString("type", "ApplyRequest")
encoder.AddString("id", f.id.String()) encoder.AddString("id", f.id.String())
return encoder.AddObject("action", f.action) return encoder.AddObject("action", f.action)
} }
func (f ApplyRequest) ClientID() xid.ID { func (f *ApplyRequest) ClientID() xid.ID {
return f.id return f.id
} }
@ -82,13 +82,13 @@ type LeaveRequest struct {
id xid.ID id xid.ID
} }
func (l LeaveRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (l *LeaveRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "LeaveRequest") encoder.AddString("type", "LeaveRequest")
encoder.AddString("id", l.id.String()) encoder.AddString("id", l.id.String())
return nil return nil
} }
func (l LeaveRequest) ClientID() xid.ID { func (l *LeaveRequest) ClientID() xid.ID {
return l.id return l.id
} }
@ -98,13 +98,13 @@ type StopRequest struct {
id xid.ID id xid.ID
} }
func (s StopRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (s *StopRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "StopRequest") encoder.AddString("type", "StopRequest")
encoder.AddString("id", s.id.String()) encoder.AddString("id", s.id.String())
return nil return nil
} }
func (s StopRequest) ClientID() xid.ID { func (s *StopRequest) ClientID() xid.ID {
return s.id return s.id
} }
@ -113,12 +113,12 @@ type ShutdownResponse struct {
id xid.ID id xid.ID
} }
func (s ShutdownResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (s *ShutdownResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ShutdownResponse") encoder.AddString("type", "ShutdownResponse")
encoder.AddString("id", s.id.String()) encoder.AddString("id", s.id.String())
return nil return nil
} }
func (s ShutdownResponse) ClientID() xid.ID { func (s *ShutdownResponse) ClientID() xid.ID {
return s.id return s.id
} }

@ -22,18 +22,18 @@ type JoinResponse struct {
currentState *state.Synced currentState *state.Synced
} }
func (j JoinResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (j *JoinResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinResponse") encoder.AddString("type", "JoinResponse")
encoder.AddString("id", j.id.String()) encoder.AddString("id", j.id.String())
return encoder.AddObject("currentState", j.currentState) return encoder.AddObject("currentState", j.currentState)
} }
func (j JoinResponse) RoomID() xid.ID { func (j *JoinResponse) RoomID() xid.ID {
return j.id return j.id
} }
// CurrentState returns the state of the room as of when the JoinRequest was processed. // CurrentState returns the state of the room as of when the JoinRequest was processed.
func (j JoinResponse) CurrentState() *state.Synced { func (j *JoinResponse) CurrentState() *state.Synced {
return j.currentState return j.currentState
} }
@ -44,18 +44,18 @@ type RefreshResponse struct {
currentState *state.Synced currentState *state.Synced
} }
func (r RefreshResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (r *RefreshResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinResponse") encoder.AddString("type", "JoinResponse")
encoder.AddString("id", r.id.String()) encoder.AddString("id", r.id.String())
return encoder.AddObject("currentState", r.currentState) return encoder.AddObject("currentState", r.currentState)
} }
func (r RefreshResponse) RoomID() xid.ID { func (r *RefreshResponse) RoomID() xid.ID {
return r.id return r.id
} }
// CurrentState returns the state of the room as of when the RefreshRequest was processed. // CurrentState returns the state of the room as of when the RefreshRequest was processed.
func (r RefreshResponse) CurrentState() *state.Synced { func (r *RefreshResponse) CurrentState() *state.Synced {
return r.currentState return r.currentState
} }
@ -68,7 +68,7 @@ type ApplyResponse struct {
result error result error
} }
func (a ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (a *ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyResponse") encoder.AddString("type", "ApplyResponse")
encoder.AddString("id", a.id.String()) encoder.AddString("id", a.id.String())
encoder.AddUint32("actionId", a.actionID) encoder.AddUint32("actionId", a.actionID)
@ -79,17 +79,21 @@ func (a ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil return nil
} }
func (a ApplyResponse) RoomID() xid.ID { func (a *ApplyResponse) RoomID() xid.ID {
return a.id return a.id
} }
func (a *ApplyResponse) ActionID() uint32 {
return a.actionID
}
// Success returns true if the action succeeded, false if it failed. // Success returns true if the action succeeded, false if it failed.
func (a ApplyResponse) Success() bool { func (a *ApplyResponse) Success() bool {
return a.result == nil return a.result == nil
} }
// Failure returns the error if the action failed, or nil if it succeeded. // Failure returns the error if the action failed, or nil if it succeeded.
func (a ApplyResponse) Failure() error { func (a *ApplyResponse) Failure() error {
return a.result return a.result
} }
@ -101,10 +105,10 @@ type ActionBroadcast struct {
// originalActionID is the ID that the client that sent the action sent. // originalActionID is the ID that the client that sent the action sent.
originalActionID uint32 originalActionID uint32
// action is the action that succeeded. // action is the action that succeeded.
action action.Action action action.Server
} }
func (a ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (a *ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ActionBroadcast") encoder.AddString("type", "ActionBroadcast")
encoder.AddString("id", a.id.String()) encoder.AddString("id", a.id.String())
encoder.AddString("originalClientId", a.originalClientID.String()) encoder.AddString("originalClientId", a.originalClientID.String())
@ -112,22 +116,31 @@ func (a ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return encoder.AddObject("action", a.action) return encoder.AddObject("action", a.action)
} }
func (a ActionBroadcast) RoomID() xid.ID { func (a *ActionBroadcast) RoomID() xid.ID {
return a.id return a.id
} }
func (a *ActionBroadcast) OriginalClientID() xid.ID {
return a.originalClientID
}
func (a *ActionBroadcast) OriginalActionID() uint32 {
return a.originalActionID
}
func (a *ActionBroadcast) Action() action.Server {
return a.action
}
// LeaveResponse is the message sent by the room when it has accepted that a client has left, and will send it no further messages. // LeaveResponse is the message sent by the room when it has accepted that a client has left, and will send it no further messages.
type LeaveResponse struct { type LeaveResponse struct {
id xid.ID id xid.ID
} }
func (l LeaveResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (l *LeaveResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "LeaveResponse") encoder.AddString("type", "LeaveResponse")
encoder.AddString("id", l.id.String()) encoder.AddString("id", l.id.String())
return nil return nil
} }
func (l LeaveResponse) RoomID() xid.ID { func (l *LeaveResponse) RoomID() xid.ID {
return l.id return l.id
} }
@ -136,12 +149,12 @@ type ShutdownRequest struct {
id xid.ID id xid.ID
} }
func (s ShutdownRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (s *ShutdownRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ShutdownRequest") encoder.AddString("type", "ShutdownRequest")
encoder.AddString("id", s.id.String()) encoder.AddString("id", s.id.String())
return nil return nil
} }
func (s ShutdownRequest) RoomID() xid.ID { func (s *ShutdownRequest) RoomID() xid.ID {
return s.id return s.id
} }

@ -119,11 +119,11 @@ func (l HexLayer) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
// GetCellAt returns a reference to the cell at the given coordinates. // GetCellAt returns a reference to the cell at the given coordinates.
// If the coordinates are out of bounds for this map, an error will be returned. // If the coordinates are out of bounds for this map, an error will be returned.
func (l HexLayer) GetCellAt(c StorageCoordinates) (*HexCell, error) { func (l HexLayer) GetCellAt(c StorageCoordinates) (*HexCell, error) {
if int(c.Line) > len(l) { if int(c.Line) >= len(l) {
return nil, fmt.Errorf("line %d out of bounds (%d)", c.Line, len(l)) return nil, fmt.Errorf("line %d out of bounds (%d)", c.Line, len(l))
} }
line := l[c.Line] line := l[c.Line]
if int(c.Cell) > len(line) { if int(c.Cell) >= len(line) {
return nil, fmt.Errorf("cell %d out of bounds (%d)", c.Cell, len(line)) return nil, fmt.Errorf("cell %d out of bounds (%d)", c.Cell, len(line))
} }
return &(line[c.Cell]), nil return &(line[c.Cell]), nil
@ -149,18 +149,43 @@ type HexMap struct {
// This is the number of cells joined together, flat-edge to flat-edge, in each line. // This is the number of cells joined together, flat-edge to flat-edge, in each line.
CellsPerLine uint8 `json:"cellsPerLine"` CellsPerLine uint8 `json:"cellsPerLine"`
// Layout is the orientation and line parity used to display the map. // Layout is the orientation and line parity used to display the map.
Layout Layout `json:"displayMode"` Layout Layout `json:"layout"`
// Layer contains the actual map data. // Layer contains the actual map data.
// Layer itself is a slice with Lines elements, each of which is a line; // Layer itself is a slice with Lines elements, each of which is a line;
// each of those lines is a slice of CellsPerLine cells. // each of those lines is a slice of CellsPerLine cells.
Layer HexLayer `json:"lineCells"` Layer HexLayer `json:"layer"`
}
func NewHexMap(layout Layout, lines uint8, cellsPerLine uint8) *HexMap {
layer := make(HexLayer, lines)
for index := range layer {
line := make(HexLine, cellsPerLine)
for index := range line {
line[index] = HexCell{
Color: Color{
R: 255,
G: 255,
B: 255,
A: 255,
},
}
}
layer[index] = line
}
return &HexMap{
XID: xid.New(),
Layout: layout,
Lines: lines,
CellsPerLine: cellsPerLine,
Layer: layer,
}
} }
func (m HexMap) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (m HexMap) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("id", m.XID.String()) encoder.AddString("id", m.XID.String())
encoder.AddUint8("lines", m.Lines) encoder.AddUint8("lines", m.Lines)
encoder.AddUint8("cellsPerLine", m.CellsPerLine) encoder.AddUint8("cellsPerLine", m.CellsPerLine)
displayModeErr := encoder.AddObject("displayMode", m.Layout) displayModeErr := encoder.AddObject("layout", m.Layout)
lineCellsErr := encoder.AddArray("lineCells", m.Layer) lineCellsErr := encoder.AddArray("lineCells", m.Layer)
if displayModeErr != nil { if displayModeErr != nil {
return displayModeErr return displayModeErr

@ -1,37 +0,0 @@
package websocket
import (
"github.com/gorilla/websocket"
"time"
)
const (
// ReadTimeLimit is the maximum time the server is willing to wait after receiving a message before receiving another one.
ReadTimeLimit = 60 * time.Second
// WriteTimeLimit is the maximum time the server is willing to wait to send a message.
WriteTimeLimit = 10 * time.Second
// ControlTimeLimit is the maximum time the server is willing to wait to send a control message like Ping or Close.
ControlTimeLimit = (WriteTimeLimit * 5) / 10
// PingDelay is the time between pings.
// It must be less than ReadTimeLimit to account for latency and delays on either side.
PingDelay = (ReadTimeLimit * 7) / 10
)
// A Connection corresponds to a pair of actors.
type Connection struct {
conn *websocket.Conn
r reader
w writer
}
// ReadChannel returns the channel that can be used to read client messages from the connection.
// After receiving SocketClosed, the reader will close its channel.
func (c *Connection) ReadChannel() <-chan ClientCommand {
return c.r.channel
}
// WriteChannel returns the channel that can be used to send server messages on the connection.
// After sending SocketClosed, the writer will close its channel; do not send any further messages on the channel.
func (c *Connection) WriteChannel() chan<- ServerCommand {
return c.w.channel
}

@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"git.reya.zone/reya/hexmap/server/action" "git.reya.zone/reya/hexmap/server/action"
@ -36,20 +36,7 @@ func (c ClientRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil return nil
} }
// IDed contains a pair of ID and Action, as sent by the client. type IDPairs []action.IDed
type IDed struct {
// ID contains the arbitrary ID that was sent by the client, for identifying the action in future messages.
ID uint32 `json:"id"`
// Action contains the action that was actually being sent.
Action action.Client `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddUint32("id", i.ID)
return encoder.AddObject("action", i.Action)
}
type IDPairs []IDed
func (a IDPairs) MarshalLogArray(encoder zapcore.ArrayEncoder) error { func (a IDPairs) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error = nil var finalErr error = nil

@ -1,6 +1,7 @@
package websocket package ws
import ( import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/state" "git.reya.zone/reya/hexmap/server/state"
) )
@ -19,13 +20,13 @@ func (x *ClientCommandPB) ToGo() (ClientCommand, error) {
} }
} }
func (x *ClientHelloPB) ToGo() ClientHello { func (x *ClientHelloPB) ToGo() *ClientHello {
return ClientHello{ return &ClientHello{
Version: x.Version, Version: x.Version,
} }
} }
func (c ClientHello) ToClientPB() *ClientCommandPB { func (c *ClientHello) ToClientPB() *ClientCommandPB {
return &ClientCommandPB{ return &ClientCommandPB{
Command: &ClientCommandPB_Hello{ Command: &ClientCommandPB_Hello{
Hello: &ClientHelloPB{ Hello: &ClientHelloPB{
@ -35,11 +36,11 @@ func (c ClientHello) ToClientPB() *ClientCommandPB {
} }
} }
func (*ClientRefreshPB) ToGo() ClientRefresh { func (*ClientRefreshPB) ToGo() *ClientRefresh {
return ClientRefresh{} return &ClientRefresh{}
} }
func (c ClientRefresh) ToClientPB() *ClientCommandPB { func (c *ClientRefresh) ToClientPB() *ClientCommandPB {
return &ClientCommandPB{ return &ClientCommandPB{
Command: &ClientCommandPB_Refresh{ Command: &ClientCommandPB_Refresh{
Refresh: &ClientRefreshPB{}, Refresh: &ClientRefreshPB{},
@ -47,42 +48,38 @@ func (c ClientRefresh) ToClientPB() *ClientCommandPB {
} }
} }
func (x *ClientActPB_IDed) ToGo() (IDed, error) { func (x *ClientActPB_IDed) ToGo() (action.IDed, error) {
action, err := x.Action.ToGo() act, err := x.Action.ToGo()
if err != nil { if err != nil {
return IDed{}, nil return action.IDed{}, nil
} }
return IDed{ return action.IDed{
ID: x.Id, ID: x.Id,
Action: action, Action: act,
}, nil }, nil
} }
func (i IDed) ToPB() *ClientActPB_IDed { func (x *ClientActPB) ToGo() (*ClientAct, error) {
return &ClientActPB_IDed{
Id: i.ID,
Action: i.Action.ToClientPB(),
}
}
func (x *ClientActPB) ToGo() (ClientAct, error) {
actions := make(IDPairs, len(x.Actions)) actions := make(IDPairs, len(x.Actions))
for index, ided := range x.Actions { for index, ided := range x.Actions {
action, err := ided.ToGo() action, err := ided.ToGo()
if err != nil { if err != nil {
return ClientAct{}, err return nil, err
} }
actions[index] = action actions[index] = action
} }
return ClientAct{ return &ClientAct{
Actions: actions, Actions: actions,
}, nil }, nil
} }
func (c ClientAct) ToClientPB() *ClientCommandPB { func (c *ClientAct) ToClientPB() *ClientCommandPB {
actions := make([]*ClientActPB_IDed, len(c.Actions)) actions := make([]*ClientActPB_IDed, len(c.Actions))
for index, ided := range c.Actions { for index, ided := range c.Actions {
actions[index] = ided.ToPB() actions[index] = &ClientActPB_IDed{
Id: ided.ID,
Action: ided.Action.ToClientPB(),
}
} }
return &ClientCommandPB{ return &ClientCommandPB{
Command: &ClientCommandPB_Act{ Command: &ClientCommandPB_Act{
@ -93,10 +90,10 @@ func (c ClientAct) ToClientPB() *ClientCommandPB {
} }
} }
func (ClientMalformed) ToClientPB() *ClientCommandPB { func (*ClientMalformed) ToClientPB() *ClientCommandPB {
return nil return nil
} }
func (SocketClosed) ToClientPB() *ClientCommandPB { func (*SocketClosed) ToClientPB() *ClientCommandPB {
return nil return nil
} }

@ -0,0 +1,276 @@
package ws
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/room"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"net/http"
"time"
)
const (
// ReadTimeLimit is the maximum time the server is willing to wait after receiving a message before receiving another one.
ReadTimeLimit = 60 * time.Second
// WriteTimeLimit is the maximum time the server is willing to wait to send a message.
WriteTimeLimit = 10 * time.Second
// ControlTimeLimit is the maximum time the server is willing to wait to send a control message like Ping or Close.
ControlTimeLimit = (WriteTimeLimit * 5) / 10
// PingDelay is the time between pings.
// It must be less than ReadTimeLimit to account for latency and delays on either side.
PingDelay = (ReadTimeLimit * 7) / 10
)
type HTTPHandler struct {
Upgrader websocket.Upgrader
Logger *zap.Logger
Room *room.Client
}
func destroyBadProtocolSocket(c *websocket.Conn, logger *zap.Logger) {
err := c.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, "Invalid subprotocols"), time.Now().Add(ControlTimeLimit))
if err != nil {
logger.Error("Failed to write close message")
}
err = c.SetReadDeadline(time.Now().Add(ControlTimeLimit))
if err != nil {
logger.Error("Failed to set read deadline")
}
for {
_, _, err := c.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseProtocolError) {
logger.Error("Websocket connection shut down ignominiously", zap.Error(err))
}
return
}
}
}
func (h *HTTPHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
c, err := h.Upgrader.Upgrade(responseWriter, request, http.Header{})
if err != nil {
h.Logger.Error("Failed to upgrade ws connection", zap.Error(err))
return
}
if c.Subprotocol() == "" {
h.Logger.Error("No matching subprotocol", zap.String("clientProtocols", request.Header.Get("Sec-Websocket-Protocol")))
go destroyBadProtocolSocket(c, h.Logger)
return
}
result := NewConnection(c, h.Logger.Named("Connection"))
exchange(result, h.Logger.Named("Link"), func(o room.NewClientOptions) *room.Client {
return h.Room.NewClient(o)
})
}
func exchange(c *Connection, l *zap.Logger, clientMaker func(options room.NewClientOptions) *room.Client) {
wsr := c.ReadChannel()
wsw := c.WriteChannel()
l.Info("Connection established")
closeWith := &SocketClosed{
Code: websocket.CloseAbnormalClosure,
Text: "I don't know what happened. But goodbye!",
}
defer func() {
l.Info("Shutting down")
// Wait for the websocket connection to shut down.
wsw <- closeWith
if wsr != nil {
for {
msg := <-wsr
switch msg.(type) {
case *SocketClosed:
return
}
}
}
}()
// State 1: Waiting for a hello.
// Anything else is death.
cmd := <-wsr
switch typedCmd := cmd.(type) {
case *ClientHello:
if typedCmd.Version != ProtocolVersion {
l.Warn("Bad Hello version")
// Disgusting. I can't even look at you.
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "Wrong protocol version",
}
return
}
l.Info("Got Hello")
default:
l.Warn("Got NON-hello")
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "You don't even say hello?",
}
return
}
l.Info("Waiting for room.")
// State 2: Waiting for the room to notice us.
rm := clientMaker(room.NewClientOptions{
AcceptBroadcasts: true,
RequestStartingState: true,
})
rmr := rm.IncomingChannel()
rmw := rm.OutgoingChannel()
var leaveWith room.ClientMessage = nil
defer func() {
l.Info("Leaving room")
if leaveWith == nil {
leaveWith = rm.Leave()
}
rmw <- leaveWith
if _, ok := leaveWith.(*room.ShutdownResponse); ok {
// The room was already shutting down.
return
}
for {
msg := <-rmr
switch msg.(type) {
case *room.LeaveResponse:
return
case *room.ShutdownRequest:
rmw <- rm.AcknowledgeShutdown()
return
}
}
}()
l.Info("Waiting for JoinResponse")
msg := <-rmr
switch typedMsg := msg.(type) {
case *room.JoinResponse:
l.Info("Got JoinResponse")
wsw <- &ServerHello{
Version: ProtocolVersion,
State: typedMsg.CurrentState(),
}
case *room.ShutdownRequest:
l.Info("Got ShutdownRequest")
// Room was shutting down when we joined, oops!
closeWith = &SocketClosed{
Code: websocket.CloseGoingAway,
Text: "Shutting down right as you joined. Sorry!",
}
return
default:
l.Info("Got non-JoinResponse/ShutdownRequest")
// Uh. That's concerning. We don't have anything to send our client.
// Let's just give up.
return
}
l.Info("Waiting for messages")
for {
select {
case cmd := <-wsr:
switch typedCmd := cmd.(type) {
case *ClientHello:
l.Info("Got unnecessary ClientHello")
// Huh???
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "Enough hellos. Goodbye.",
}
return
case *ClientRefresh:
l.Info("Got ClientRefresh")
rmw <- rm.Refresh()
case *ClientAct:
l.Info("Got ClientAct")
for _, act := range typedCmd.Actions {
rmw <- rm.Apply(act)
}
case *SocketClosed:
l.Info("Got SocketClosed", zap.Object("close", typedCmd))
closeWith = typedCmd
return
case *ClientMalformed:
l.Warn("Got ClientMalformed")
return
}
case msg := <-rmr:
switch typedMsg := msg.(type) {
case *room.JoinResponse:
// Huh????
l.Info("Got unnecesary JoinResponse")
return
case *room.RefreshResponse:
l.Info("Got RefreshResponse")
wsw <- &ServerRefresh{
State: typedMsg.CurrentState(),
}
case *room.ApplyResponse:
l.Info("Got ApplyResponse")
if typedMsg.Success() {
wsw <- &ServerOK{
IDs: []uint32{typedMsg.ActionID()},
}
} else {
wsw <- &ServerFailed{
IDs: []uint32{typedMsg.ActionID()},
Error: typedMsg.Failure().Error(),
}
}
case *room.ActionBroadcast:
l.Info("Got ActionBroadcast")
wsw <- &ServerAct{
Actions: action.ServerSlice{typedMsg.Action()},
}
case *room.LeaveResponse:
l.Info("Got odd LeaveResponse")
// Oh. u_u I wasn't- okay.
return
case *room.ShutdownRequest:
// Oh. Oh! Okay! Sorry!
l.Info("Got ShutdownRequest")
leaveWith = rm.AcknowledgeShutdown()
return
}
}
}
}
// A Connection corresponds to a pair of actors.
type Connection struct {
r reader
w writer
}
func NewConnection(conn *websocket.Conn, logger *zap.Logger) *Connection {
readChan := make(chan time.Time)
out := &Connection{
r: reader{
conn: conn,
channel: make(chan ClientCommand),
readNotifications: readChan,
logger: logger.Named("reader"),
},
w: writer{
conn: conn,
channel: make(chan ServerCommand),
readNotifications: readChan,
timer: nil,
nextPingAt: time.Time{},
logger: logger.Named("writer"),
},
}
go out.r.act()
go out.w.act()
return out
}
// ReadChannel returns the channel that can be used to read client messages from the connection.
// After receiving SocketClosed, the reader will close its channel.
func (c *Connection) ReadChannel() <-chan ClientCommand {
return c.r.channel
}
// WriteChannel returns the channel that can be used to send server messages on the connection.
// After sending SocketClosed, the writer will close its channel; do not send any further messages on the channel.
func (c *Connection) WriteChannel() chan<- ServerCommand {
return c.w.channel
}

@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"fmt" "fmt"
@ -33,6 +33,11 @@ func (r *reader) act() {
defer r.shutdown() defer r.shutdown()
// Our first deadline starts immediately, so let the writer know. // Our first deadline starts immediately, so let the writer know.
r.updateDeadlines() r.updateDeadlines()
// The reader should not automatically respond with close messages.
// It should simply let the close message be returned as an error.
r.conn.SetCloseHandler(func(code int, text string) error {
return nil
})
r.conn.SetPongHandler(func(appData string) error { r.conn.SetPongHandler(func(appData string) error {
r.logger.Debug("Received pong, extending read deadline") r.logger.Debug("Received pong, extending read deadline")
r.updateDeadlines() r.updateDeadlines()
@ -44,18 +49,18 @@ func (r *reader) act() {
for { for {
messageType, messageData, err := r.conn.ReadMessage() messageType, messageData, err := r.conn.ReadMessage()
if err != nil { if err != nil {
var closure SocketClosed var closure *SocketClosed
if websocket.IsCloseError(err, StandardClientCloseTypes...) { if websocket.IsCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError) typedErr := err.(*websocket.CloseError)
r.logger.Debug("Received normal close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text)) r.logger.Debug("Received normal close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
closure = SocketClosed{Code: typedErr.Code, Text: typedErr.Text} closure = &SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else if websocket.IsUnexpectedCloseError(err, StandardClientCloseTypes...) { } else if websocket.IsUnexpectedCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError) typedErr := err.(*websocket.CloseError)
r.logger.Warn("Received unexpected close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text)) r.logger.Warn("Received unexpected close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
closure = SocketClosed{Code: typedErr.Code, Text: typedErr.Text} closure = &SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else { } else {
r.logger.Error("Error while reading message, shutting down", zap.Error(err)) r.logger.Error("Error while reading message, shutting down", zap.Error(err))
closure = SocketClosed{Error: err} closure = &SocketClosed{Error: err}
} }
r.logger.Debug("Sending close message to reader", zap.Object("closeMessage", closure)) r.logger.Debug("Sending close message to reader", zap.Object("closeMessage", closure))
r.channel <- closure r.channel <- closure
@ -75,7 +80,7 @@ func (r *reader) parseCommand(socketType int, data []byte) ClientCommand {
MessageType: socketType, MessageType: socketType,
} }
r.logger.Error("Received command with unknown WebSocket message type", zap.Error(err)) r.logger.Error("Received command with unknown WebSocket message type", zap.Error(err))
return ClientMalformed{ return &ClientMalformed{
Error: err, Error: err,
} }
} }
@ -83,13 +88,13 @@ func (r *reader) parseCommand(socketType int, data []byte) ClientCommand {
var cmdPb ClientCommandPB var cmdPb ClientCommandPB
err := proto.Unmarshal(data, &cmdPb) err := proto.Unmarshal(data, &cmdPb)
if err != nil { if err != nil {
return ClientMalformed{ return &ClientMalformed{
Error: err, Error: err,
} }
} }
cmd, err := (&cmdPb).ToGo() cmd, err := (&cmdPb).ToGo()
if err != nil { if err != nil {
return ClientMalformed{Error: err} return &ClientMalformed{Error: err}
} }
return cmd return cmd
} }

@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"git.reya.zone/reya/hexmap/server/action" "git.reya.zone/reya/hexmap/server/action"

@ -1,4 +1,4 @@
package websocket package ws
import "git.reya.zone/reya/hexmap/server/action" import "git.reya.zone/reya/hexmap/server/action"
@ -19,18 +19,18 @@ func (x *ServerCommandPB) ToGo() (ServerCommand, error) {
} }
} }
func (x *ServerHelloPB) ToGo() (ServerHello, error) { func (x *ServerHelloPB) ToGo() (*ServerHello, error) {
state, err := x.State.ToGo() state, err := x.State.ToGo()
if err != nil { if err != nil {
return ServerHello{}, err return nil, err
} }
return ServerHello{ return &ServerHello{
Version: x.Version, Version: x.Version,
State: &state, State: &state,
}, nil }, nil
} }
func (s ServerHello) ToServerPB() *ServerCommandPB { func (s *ServerHello) ToServerPB() *ServerCommandPB {
return &ServerCommandPB{ return &ServerCommandPB{
Command: &ServerCommandPB_Hello{ Command: &ServerCommandPB_Hello{
Hello: &ServerHelloPB{ Hello: &ServerHelloPB{
@ -41,17 +41,17 @@ func (s ServerHello) ToServerPB() *ServerCommandPB {
} }
} }
func (x *ServerRefreshPB) ToGo() (ServerRefresh, error) { func (x *ServerRefreshPB) ToGo() (*ServerRefresh, error) {
state, err := x.State.ToGo() state, err := x.State.ToGo()
if err != nil { if err != nil {
return ServerRefresh{}, err return nil, err
} }
return ServerRefresh{ return &ServerRefresh{
State: &state, State: &state,
}, nil }, nil
} }
func (s ServerRefresh) ToServerPB() *ServerCommandPB { func (s *ServerRefresh) ToServerPB() *ServerCommandPB {
return &ServerCommandPB{ return &ServerCommandPB{
Command: &ServerCommandPB_Refresh{ Command: &ServerCommandPB_Refresh{
Refresh: &ServerRefreshPB{ Refresh: &ServerRefreshPB{
@ -61,15 +61,15 @@ func (s ServerRefresh) ToServerPB() *ServerCommandPB {
} }
} }
func (x *ServerOKPB) ToGo() ServerOK { func (x *ServerOKPB) ToGo() *ServerOK {
ids := make(IDSlice, len(x.Ids)) ids := make(IDSlice, len(x.Ids))
copy(ids, x.Ids) copy(ids, x.Ids)
return ServerOK{ return &ServerOK{
IDs: ids, IDs: ids,
} }
} }
func (s ServerOK) ToServerPB() *ServerCommandPB { func (s *ServerOK) ToServerPB() *ServerCommandPB {
ids := make([]uint32, len(s.IDs)) ids := make([]uint32, len(s.IDs))
copy(ids, s.IDs) copy(ids, s.IDs)
return &ServerCommandPB{ return &ServerCommandPB{
@ -81,16 +81,16 @@ func (s ServerOK) ToServerPB() *ServerCommandPB {
} }
} }
func (x *ServerFailedPB) ToGo() ServerFailed { func (x *ServerFailedPB) ToGo() *ServerFailed {
ids := make(IDSlice, len(x.Ids)) ids := make(IDSlice, len(x.Ids))
copy(ids, x.Ids) copy(ids, x.Ids)
return ServerFailed{ return &ServerFailed{
IDs: ids, IDs: ids,
Error: x.Error, Error: x.Error,
} }
} }
func (s ServerFailed) ToServerPB() *ServerCommandPB { func (s *ServerFailed) ToServerPB() *ServerCommandPB {
ids := make([]uint32, len(s.IDs)) ids := make([]uint32, len(s.IDs))
copy(ids, s.IDs) copy(ids, s.IDs)
return &ServerCommandPB{ return &ServerCommandPB{
@ -103,21 +103,21 @@ func (s ServerFailed) ToServerPB() *ServerCommandPB {
} }
} }
func (x *ServerActPB) ToGo() (ServerAct, error) { func (x *ServerActPB) ToGo() (*ServerAct, error) {
actions := make(action.ServerSlice, len(x.Actions)) actions := make(action.ServerSlice, len(x.Actions))
for index, act := range x.Actions { for index, act := range x.Actions {
convertedAct, err := act.ToGo() convertedAct, err := act.ToGo()
if err != nil { if err != nil {
return ServerAct{}, err return nil, err
} }
actions[index] = convertedAct actions[index] = convertedAct
} }
return ServerAct{ return &ServerAct{
Actions: actions, Actions: actions,
}, nil }, nil
} }
func (s ServerAct) ToServerPB() *ServerCommandPB { func (s *ServerAct) ToServerPB() *ServerCommandPB {
actions := make([]*action.ServerActionPB, len(s.Actions)) actions := make([]*action.ServerActionPB, len(s.Actions))
for index, act := range s.Actions { for index, act := range s.Actions {
actions[index] = act.ToServerPB() actions[index] = act.ToServerPB()
@ -131,6 +131,6 @@ func (s ServerAct) ToServerPB() *ServerCommandPB {
} }
} }
func (SocketClosed) ToServerPB() *ServerCommandPB { func (*SocketClosed) ToServerPB() *ServerCommandPB {
return nil return nil
} }

@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -6,9 +6,8 @@ import (
) )
const ( const (
PingData string = "are you still there?" PingData string = "are you still there?"
ProtocolVersion uint32 = 1
GoodbyeType = "GOODBYE"
) )
var StandardClientCloseTypes = []int{websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure} var StandardClientCloseTypes = []int{websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure}
@ -28,15 +27,11 @@ type SocketClosed struct {
} }
func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error { func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", GoodbyeType) encoder.AddString("type", "GOODBYE")
encoder.AddInt16("code", int16(c.Code)) encoder.AddInt("code", c.Code)
encoder.AddString("text", c.Text) encoder.AddString("text", c.Text)
if c.Error != nil { if c.Error != nil {
encoder.AddString("error", c.Error.Error()) encoder.AddString("error", c.Error.Error())
} }
return nil return nil
} }
func (c SocketClosed) ServerType() ServerMessageType {
return GoodbyeType
}

@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -9,7 +9,7 @@ import (
) )
type writer struct { type writer struct {
// conn is the websocket connection that this writer is responsible for writing on. // conn is the ws connection that this writer is responsible for writing on.
conn *websocket.Conn conn *websocket.Conn
// channel is the channel used to receive server messages to be sent to the client. // channel is the channel used to receive server messages to be sent to the client.
// When it receives a SocketClosed, the process on the sending end promises not to send any further messages, as the writer will close it right after. // When it receives a SocketClosed, the process on the sending end promises not to send any further messages, as the writer will close it right after.
@ -66,7 +66,7 @@ func (w *writer) act() {
} }
case raw := <-w.channel: case raw := <-w.channel:
switch msg := raw.(type) { switch msg := raw.(type) {
case SocketClosed: case *SocketClosed:
w.logger.Debug("Received close message, forwarding and shutting down", zap.Object("msg", msg)) w.logger.Debug("Received close message, forwarding and shutting down", zap.Object("msg", msg))
w.sendClose(msg) w.sendClose(msg)
// bye bye, we'll graceful shutdown because we deferred it // bye bye, we'll graceful shutdown because we deferred it
@ -106,7 +106,7 @@ func (w *writer) send(msg ServerCommand) {
w.logger.Error("Error while setting write deadline", zap.Time("writeDeadline", writeDeadline), zap.Object("msg", msg), zap.Error(err)) w.logger.Error("Error while setting write deadline", zap.Time("writeDeadline", writeDeadline), zap.Object("msg", msg), zap.Error(err))
} }
w.logger.Debug("Opening message writer to send command", zap.Object("msg", msg)) w.logger.Debug("Opening message writer to send command", zap.Object("msg", msg))
writer, err := w.conn.NextWriter(websocket.TextMessage) writer, err := w.conn.NextWriter(websocket.BinaryMessage)
if err != nil { if err != nil {
w.logger.Error("Error while getting writer from connection", zap.Error(err)) w.logger.Error("Error while getting writer from connection", zap.Error(err))
return return
@ -127,9 +127,9 @@ func (w *writer) send(msg ServerCommand) {
// Deferred close happens now // Deferred close happens now
} }
// sendClose sends a close message on the websocket connection, but does not actually close the connection. // sendClose sends a close message on the ws connection, but does not actually close the connection.
// It does, however, close the incoming message channel to the writer. // It does, however, close the incoming message channel to the writer.
func (w *writer) sendClose(msg SocketClosed) { func (w *writer) sendClose(msg *SocketClosed) {
w.logger.Debug("Shutting down the writer channel") w.logger.Debug("Shutting down the writer channel")
close(w.channel) close(w.channel)
w.channel = nil w.channel = nil
@ -140,7 +140,7 @@ func (w *writer) sendClose(msg SocketClosed) {
} }
} }
// sendPing sends a ping message on the websocket connection. The content is arbitrary. // sendPing sends a ping message on the ws connection. The content is arbitrary.
func (w *writer) sendPing() { func (w *writer) sendPing() {
w.logger.Debug("Sending ping") w.logger.Debug("Sending ping")
err := w.conn.WriteControl(websocket.PingMessage, []byte(PingData), time.Now().Add(ControlTimeLimit)) err := w.conn.WriteControl(websocket.PingMessage, []byte(PingData), time.Now().Add(ControlTimeLimit))
@ -176,7 +176,7 @@ func (w *writer) gracefulShutdown() {
} }
case raw := <-w.channel: case raw := <-w.channel:
switch msg := raw.(type) { switch msg := raw.(type) {
case SocketClosed: case *SocketClosed:
w.logger.Debug("Received close message from channel while shutting down, forwarding", zap.Object("msg", msg)) w.logger.Debug("Received close message from channel while shutting down, forwarding", zap.Object("msg", msg))
w.sendClose(msg) w.sendClose(msg)
default: default:
Loading…
Cancel
Save