Split ClientAction and ServerAction, and differentiate between Commands (messages sent to/from the server) and Actions (data packets sent around the client's different parts).

main
Mari 3 years ago
parent a83c9688bb
commit 44b2813922
  1. 64
      client/src/actions/ClientAction.ts
  2. 173
      client/src/actions/NetworkAction.ts
  3. 111
      client/src/actions/ServerAction.ts
  4. 2
      client/src/reducers/AppStateReducer.ts
  5. 2
      client/src/reducers/ClientReducer.ts
  6. 8
      client/src/reducers/NetworkReducer.ts
  7. 45
      client/src/reducers/ServerReducer.ts
  8. 2
      client/src/reducers/SyncedStateReducer.ts
  9. 6
      client/src/state/NetworkState.ts

@ -0,0 +1,64 @@
import {BaseAction} from "./BaseAction";
import {AppAction} from "./AppAction";
import {CellColorAction, isCellColorAction} from "./CellAction";
import {isUserActiveColorAction, UserActiveColorAction} from "./UserAction";
export const CLIENT_HELLO = "CLIENT_HELLO"
/** Sent when the client connects. */
export interface ClientHelloCommand extends BaseAction {
readonly type: typeof CLIENT_HELLO
/** The protocol version the client is running on */
readonly version: number
}
export function isClientHelloCommand(action: AppAction): action is ClientHelloCommand {
return action.type === CLIENT_HELLO
}
export const CLIENT_REFRESH = "CLIENT_REFRESH"
/** Sent when the user requests a refresh. */
export interface ClientRefreshCommand extends BaseAction {
readonly type: typeof CLIENT_REFRESH
}
export function isClientRefreshCommand(action: AppAction): action is ClientRefreshCommand {
return action.type === CLIENT_REFRESH
}
export const CLIENT_PENDING = "CLIENT_PENDING"
/** Synthesized when a user action is ready to be sent to the server. */
export interface ClientPendingAction extends BaseAction {
readonly type: typeof CLIENT_PENDING
readonly pending: SendableAction
}
export function isClientPendingAction(action: AppAction): action is ClientPendingAction {
return action.type === CLIENT_PENDING
}
export interface SentAction {
readonly id: number
readonly action: SendableAction
}
export const CLIENT_ACT = "CLIENT_ACT"
/** Sent to the server when the user performs an action. */
export interface ClientActCommand extends BaseAction {
readonly type: typeof CLIENT_ACT
readonly actions: readonly SentAction[]
}
export function isClientActCommand(action: AppAction): action is ClientActCommand {
return action.type === CLIENT_ACT
}
export type SendableAction = CellColorAction | UserActiveColorAction
export function isSendableAction(action: AppAction): action is SendableAction {
return isCellColorAction(action) || isUserActiveColorAction(action)
}
export type ClientCommand = ClientHelloCommand | ClientRefreshCommand | ClientActCommand
export function isClientCommand(action: AppAction): action is ClientCommand {
return isClientHelloCommand(action) || isClientRefreshCommand(action) || isClientActCommand(action)
}
export type ClientAction = ClientHelloCommand | ClientRefreshCommand | ClientPendingAction | ClientActCommand
export function isClientAction(action: AppAction): action is ClientAction {
return isClientCommand(action) || isClientPendingAction(action)
}

@ -1,168 +1,13 @@
import {BaseAction} from "./BaseAction"; import {isServerAction, ServerAction} from "./ServerAction";
import {AppAction} from "./AppAction"; import {AppAction} from "./AppAction";
import {CellColorAction, isCellColorAction} from "./CellAction"; import {ClientAction, isClientAction} from "./ClientAction";
import {SyncedState} from "../state/SyncedState";
import {isUserActiveColorAction, UserActiveColorAction} from "./UserAction"; // Terminology: Messages are the data packets sent and received over the WebSocket connection.
// Commands are the protocol laid on top of messages.
// TODO: Split into ServerAction and ClientAction files // Actions are data objects that the client uses to communicate between parts of itself.
// All commands are both messages and actions.
export const SERVER_HELLO = "SERVER_HELLO" // An action is a message if and only if it is a command.
/** Sent in response to the client's ClientHelloAction when the client has been accepted. */ // A message is an action if and only if it is a command.
export interface ServerHelloAction extends BaseAction {
readonly type: typeof SERVER_HELLO
/** The protocol version the server is running on. */
readonly version: number
/** The current state of the server as of when the client connected. */
readonly state: SyncedState
}
export function isServerHelloAction(action: AppAction): action is ServerHelloAction {
return action.type === SERVER_HELLO
}
export const SERVER_GOODBYE = "SERVER_GOODBYE"
/** Synthesized when the server closes the connection. */
export interface ServerGoodbyeAction extends BaseAction {
readonly type: typeof SERVER_GOODBYE
/** The error code sent with the close message by the server, or -1 if our side crashed.*/
readonly code: number
/** The text of the server's goodbye message, or the exception message if our side crashed. */
readonly reason: string
/** The current time when this close message was received. */
readonly currentTime: Date
}
export function isServerGoodbyeAction(action: AppAction): action is ServerGoodbyeAction {
return action.type === SERVER_GOODBYE
}
export const SERVER_REFRESH = "SERVER_REFRESH"
/** Sent in response to the client's ClientRefreshAction. */
export interface ServerRefreshAction extends BaseAction {
readonly type: typeof SERVER_REFRESH
/** The current state of the server as of when the client's refresh request was processed. */
readonly state: SyncedState
}
export function isServerRefreshAction(action: AppAction): action is ServerRefreshAction {
return action.type === SERVER_REFRESH
}
export const SERVER_OK = "SERVER_OK"
/** Sent in response to the client's ClientNestedAction, if it succeeds and is applied to the server map. */
export interface ServerOKAction extends BaseAction {
readonly type: typeof SERVER_OK
/**
* The IDs of the successful actions.
* These will always be sent in sequential order - the order in which the server received and applied them.
* This allows the client to apply that set of actions to its server-state copy and then refresh its local copy.
*/
readonly ids: readonly number[]
}
export function isServerOKAction(action: AppAction): action is ServerOKAction {
return action.type === SERVER_OK
}
export const SERVER_FAILED = "SERVER_FAILED"
/** Sent in response to the client's ClientNestedAction, if it fails and has not been applied to the server map. */
export interface ServerFailedAction extends BaseAction {
readonly type: typeof SERVER_FAILED
/** The IDs of the failed actions, all of which failed with the same error. */
readonly ids: readonly number[]
/** The error the above actions failed with. */
readonly error: string
}
export function isServerFailedAction(action: AppAction): action is ServerFailedAction {
return action.type === SERVER_FAILED
}
export enum SocketState {
CONNECTING = "CONNECTING",
OPEN = "OPEN",
}
export const SERVER_SOCKET_STARTUP = "SERVER_SOCKET_STARTUP"
/** Synthesized when the websocket begins connecting, i.e., enters the Connecting or Open states.. */
export interface ServerSocketStartupAction extends BaseAction {
readonly type: typeof SERVER_SOCKET_STARTUP
readonly state: SocketState
}
export function isServerSocketStartupAction(action: AppAction): action is ServerSocketStartupAction {
return action.type === SERVER_SOCKET_STARTUP
}
export const SERVER_ACT = "SERVER_ACT"
/** Sent by the server when another client has performed an action. Never sent for the client's own actions. */
export interface ServerActAction extends BaseAction {
readonly type: typeof SERVER_ACT
readonly actions: readonly SyncableAction[]
}
export function isServerActAction(action: AppAction): action is ServerActAction {
return action.type === SERVER_ACT
}
export type SyncableAction = SendableAction
export type ServerAction =
ServerHelloAction | ServerGoodbyeAction | ServerRefreshAction |
ServerOKAction | ServerFailedAction |
ServerSocketStartupAction | ServerActAction
export function isServerAction(action: AppAction) {
return isServerHelloAction(action) || isServerGoodbyeAction(action) || isServerRefreshAction(action)
|| isServerOKAction(action) || isServerFailedAction(action)
|| isServerSocketStartupAction(action) || isServerActAction(action)
}
export const CLIENT_HELLO = "CLIENT_HELLO"
/** Sent when the client connects. */
export interface ClientHelloAction extends BaseAction {
readonly type: typeof CLIENT_HELLO
/** The protocol version the client is running on */
readonly version: number
}
export function isClientHelloAction(action: AppAction): action is ClientHelloAction {
return action.type === CLIENT_HELLO
}
export const CLIENT_REFRESH = "CLIENT_REFRESH"
/** Sent when the user requests a refresh, or if a malformed action comes through. */
export interface ClientRefreshAction extends BaseAction {
readonly type: typeof CLIENT_REFRESH
}
export function isClientRefreshAction(action: AppAction): action is ClientRefreshAction {
return action.type === CLIENT_REFRESH
}
export const CLIENT_PENDING = "CLIENT_PENDING"
/** Synthesized when a user action is ready to be sent to the server. */
export interface ClientPendingAction extends BaseAction {
readonly type: typeof CLIENT_PENDING
readonly pending: SendableAction
}
export function isClientPendingAction(action: AppAction): action is ClientPendingAction {
return action.type === CLIENT_PENDING
}
export interface SentAction {
readonly id: number
readonly action: SendableAction
}
export const CLIENT_ACT = "CLIENT_ACT"
/** Sent to the server when the user performs an action. */
export interface ClientActAction extends BaseAction {
readonly type: typeof CLIENT_ACT
readonly actions: readonly SentAction[]
}
export function isClientActAction(action: AppAction): action is ClientActAction {
return action.type === CLIENT_ACT
}
export type SendableAction = CellColorAction | UserActiveColorAction
export function isSendableAction(action: AppAction): action is SendableAction {
return isCellColorAction(action) || isUserActiveColorAction(action)
}
export type ClientAction = ClientHelloAction | ClientRefreshAction | ClientPendingAction | ClientActAction
export function isClientAction(action: AppAction): action is ClientAction {
return isClientHelloAction(action) || isClientRefreshAction(action) || isClientPendingAction(action) || isClientActAction(action)
}
export type NetworkAction = ServerAction | ClientAction; export type NetworkAction = ServerAction | ClientAction;
export function isNetworkAction(action: AppAction): action is NetworkAction { export function isNetworkAction(action: AppAction): action is NetworkAction {

@ -0,0 +1,111 @@
import {BaseAction} from "./BaseAction";
import {SyncedState} from "../state/SyncedState";
import {AppAction} from "./AppAction";
import {SendableAction} from "./ClientAction";
export const SERVER_HELLO = "SERVER_HELLO"
/** Sent in response to the client's ClientHelloAction when the client has been accepted. */
export interface ServerHelloCommand extends BaseAction {
readonly type: typeof SERVER_HELLO
/** The protocol version the server is running on. */
readonly version: number
/** The current state of the server as of when the client connected. */
readonly state: SyncedState
}
export function isServerHelloCommand(action: AppAction): action is ServerHelloCommand {
return action.type === SERVER_HELLO
}
export const SERVER_GOODBYE = "SERVER_GOODBYE"
/** Synthesized out of the close message when the server closes the connection. */
export interface ServerGoodbyeCommand extends BaseAction {
readonly type: typeof SERVER_GOODBYE
/** The error code sent with the close message by the server, or -1 if our side crashed.*/
readonly code: number
/** The text of the server's goodbye message, or the exception if our side crashed. */
readonly reason: string|Error
/** The current time when this close message was received. */
readonly currentTime: Date
}
export function isServerGoodbyeCommand(action: AppAction): action is ServerGoodbyeCommand {
return action.type === SERVER_GOODBYE
}
export const SERVER_REFRESH = "SERVER_REFRESH"
/** Sent in response to the client's ClientRefreshCommand. */
export interface ServerRefreshCommand extends BaseAction {
readonly type: typeof SERVER_REFRESH
/** The current state of the server as of when the client's refresh request was processed. */
readonly state: SyncedState
}
export function isServerRefreshCommand(action: AppAction): action is ServerRefreshCommand {
return action.type === SERVER_REFRESH
}
export const SERVER_OK = "SERVER_OK"
/** Sent in response to the client's ClientActCommand, if it succeeds and is applied to the server map. */
export interface ServerOKCommand extends BaseAction {
readonly type: typeof SERVER_OK
/**
* The IDs of the successful actions.
* These will always be sent in sequential order - the order in which the server received and applied them.
* This allows the client to apply that set of actions to its server-state copy and then refresh its local copy.
*/
readonly ids: readonly number[]
}
export function isServerOKCommand(action: AppAction): action is ServerOKCommand {
return action.type === SERVER_OK
}
export const SERVER_FAILED = "SERVER_FAILED"
/** Sent in response to the client's ClientActCommand, if it fails and has not been applied to the server map. */
export interface ServerFailedCommand extends BaseAction {
readonly type: typeof SERVER_FAILED
/** The IDs of the failed actions, all of which failed with the same error. */
readonly ids: readonly number[]
/** The error the above actions failed with. */
readonly error: string
}
export function isServerFailedCommand(action: AppAction): action is ServerFailedCommand {
return action.type === SERVER_FAILED
}
export enum SocketState {
CONNECTING = "CONNECTING",
OPEN = "OPEN",
}
export const SERVER_SOCKET_STARTUP = "SERVER_SOCKET_STARTUP"
/** Synthesized when the websocket begins connecting, i.e., enters the Connecting or Open states.. */
export interface ServerSocketStartupAction extends BaseAction {
readonly type: typeof SERVER_SOCKET_STARTUP
readonly state: SocketState
}
export function isServerSocketStartupAction(action: AppAction): action is ServerSocketStartupAction {
return action.type === SERVER_SOCKET_STARTUP
}
export const SERVER_ACT = "SERVER_ACT"
/** Sent by the server when another client has performed an action. Never sent for the client's own actions. */
export interface ServerActCommand extends BaseAction {
readonly type: typeof SERVER_ACT
readonly actions: readonly SyncableAction[]
}
export function isServerActCommand(action: AppAction): action is ServerActCommand {
return action.type === SERVER_ACT
}
export type SyncableAction = SendableAction
export type ServerCommand =
ServerHelloCommand | ServerGoodbyeCommand | ServerRefreshCommand |
ServerOKCommand | ServerFailedCommand | ServerActCommand
export function isServerCommand(action: AppAction): action is ServerCommand {
return isServerHelloCommand(action) || isServerGoodbyeCommand(action) || isServerRefreshCommand(action)
|| isServerOKCommand(action) || isServerFailedCommand(action) || isServerActCommand(action)
}
export type ServerAction = ServerCommand | ServerSocketStartupAction
export function isServerAction(action: AppAction): action is ServerAction {
return isServerCommand(action) || isServerSocketStartupAction(action)
}

@ -3,7 +3,7 @@ import {AppState} from "../state/AppState";
import {isTileAction} from "../actions/TileAction"; import {isTileAction} from "../actions/TileAction";
import {tileReducer} from "./TileReducer"; import {tileReducer} from "./TileReducer";
import {networkReducer} from "./NetworkReducer"; import {networkReducer} from "./NetworkReducer";
import {CLIENT_PENDING, isNetworkAction, isSendableAction} from "../actions/NetworkAction"; import {CLIENT_PENDING, isNetworkAction, isSendableAction} from "../actions/ClientAction";
import {syncedStateReducer} from "./SyncedStateReducer"; import {syncedStateReducer} from "./SyncedStateReducer";
import {exhaustivenessCheck} from "../util/TypeUtils"; import {exhaustivenessCheck} from "../util/TypeUtils";
import {isMapAction, MapAction} from "../actions/MapAction"; import {isMapAction, MapAction} from "../actions/MapAction";

@ -1,4 +1,4 @@
import {CLIENT_HELLO, CLIENT_PENDING, CLIENT_REFRESH, CLIENT_ACT, ClientAction} from "../actions/NetworkAction"; import {CLIENT_HELLO, CLIENT_PENDING, CLIENT_REFRESH, CLIENT_ACT, ClientAction} from "../actions/ClientAction";
import {NetworkState, ServerConnectionState} from "../state/NetworkState"; import {NetworkState, ServerConnectionState} from "../state/NetworkState";
// TODO: Verify that only one special message exists at a time. // TODO: Verify that only one special message exists at a time.

@ -1,7 +1,10 @@
import {AppState} from "../state/AppState"; import {AppState} from "../state/AppState";
import {isClientAction, NetworkAction} from "../actions/NetworkAction"; import {isClientAction} from "../actions/ClientAction";
import {clientReducer} from "./ClientReducer"; import {clientReducer} from "./ClientReducer";
import {serverReducer} from "./ServerReducer"; import {serverReducer} from "./ServerReducer";
import {NetworkAction} from "../actions/NetworkAction";
import {isServerAction} from "../actions/ServerAction";
import {exhaustivenessCheck} from "../util/TypeUtils";
export function networkReducer(oldState: AppState, action: NetworkAction): AppState { export function networkReducer(oldState: AppState, action: NetworkAction): AppState {
if (isClientAction(action)) { if (isClientAction(action)) {
@ -14,7 +17,8 @@ export function networkReducer(oldState: AppState, action: NetworkAction): AppSt
...oldState, ...oldState,
network: newState network: newState
} }
} else /* if (isServerAction(action)) */ { } else if (isServerAction(action)) {
return serverReducer(oldState, action) return serverReducer(oldState, action)
} }
exhaustivenessCheck(action)
} }

@ -1,29 +1,26 @@
import {AppState} from "../state/AppState"; import {AppState} from "../state/AppState";
import {NetworkState, ServerConnectionState} from "../state/NetworkState";
import {SyncedState} from "../state/SyncedState";
import {applySyncableActions} from "./SyncedStateReducer";
import {clientReducer} from "./ClientReducer";
import { import {
CLIENT_HELLO, SERVER_ACT,
SendableAction,
SERVER_FAILED, SERVER_FAILED,
SERVER_GOODBYE, SERVER_GOODBYE,
SERVER_HELLO, SERVER_HELLO,
SERVER_OK, SERVER_OK,
SERVER_REFRESH, SERVER_REFRESH,
SERVER_ACT, SERVER_SOCKET_STARTUP, ServerActCommand,
SERVER_SOCKET_STARTUP, ServerAction, ServerFailedCommand,
ServerAction, ServerGoodbyeCommand,
ServerFailedAction, ServerHelloCommand,
ServerGoodbyeAction, ServerOKCommand,
ServerHelloAction, ServerRefreshCommand,
ServerOKAction,
ServerRefreshAction,
ServerActAction,
ServerSocketStartupAction, ServerSocketStartupAction,
SocketState, SocketState,
SyncableAction, SyncableAction
} from "../actions/NetworkAction"; } from "../actions/ServerAction";
import {NetworkState, ServerConnectionState} from "../state/NetworkState"; import {CLIENT_HELLO, SendableAction} from "../actions/ClientAction";
import {SyncedState} from "../state/SyncedState";
import {applySyncableActions} from "./SyncedStateReducer";
import {clientReducer} from "./ClientReducer";
interface StateRecalculationInputs { interface StateRecalculationInputs {
/** The original server state before the actions changed. The base on which the actions are all applied. */ /** The original server state before the actions changed. The base on which the actions are all applied. */
@ -59,7 +56,7 @@ function recalculateStates(input: StateRecalculationInputs): StateRecalculationO
return { newServerState, newLocalState, appliedUnsentActions } return { newServerState, newLocalState, appliedUnsentActions }
} }
function serverHelloReducer(oldState: AppState, action: ServerHelloAction): AppState { function serverHelloReducer(oldState: AppState, action: ServerHelloCommand): AppState {
const { const {
newServerState, newServerState,
newLocalState, newLocalState,
@ -86,7 +83,7 @@ function serverHelloReducer(oldState: AppState, action: ServerHelloAction): AppS
} }
} }
function serverRefreshReducer(oldState: AppState, action: ServerRefreshAction): AppState { function serverRefreshReducer(oldState: AppState, action: ServerRefreshCommand): AppState {
const { const {
newServerState, newServerState,
newLocalState, newLocalState,
@ -112,7 +109,7 @@ function serverRefreshReducer(oldState: AppState, action: ServerRefreshAction):
}; };
} }
function serverGoodbyeReducer(oldState: NetworkState, action: ServerGoodbyeAction): NetworkState { function serverGoodbyeReducer(oldState: NetworkState, action: ServerGoodbyeCommand): NetworkState {
// TODO: Sort out the correct state and autoReconnectAt based on the time in the action. // TODO: Sort out the correct state and autoReconnectAt based on the time in the action.
return { return {
...oldState, ...oldState,
@ -123,7 +120,7 @@ function serverGoodbyeReducer(oldState: NetworkState, action: ServerGoodbyeActio
} }
} }
function serverOkReducer(oldState: AppState, action: ServerOKAction): AppState { function serverOkReducer(oldState: AppState, action: ServerOKCommand): AppState {
if (oldState.network.serverState === null) { if (oldState.network.serverState === null) {
return oldState return oldState
} }
@ -153,7 +150,7 @@ function serverOkReducer(oldState: AppState, action: ServerOKAction): AppState {
} }
} }
function serverFailedReducer(oldState: AppState, action: ServerFailedAction): AppState { function serverFailedReducer(oldState: AppState, action: ServerFailedCommand): AppState {
if (oldState.network.serverState === null) { if (oldState.network.serverState === null) {
return oldState return oldState
} }
@ -211,7 +208,7 @@ function serverSocketStartupReducer(oldState: NetworkState, action: ServerSocket
} }
} }
function serverSentReducer(oldState: AppState, action: ServerActAction): AppState { function serverActReducer(oldState: AppState, action: ServerActCommand): AppState {
if (oldState.network.serverState === null) { if (oldState.network.serverState === null) {
return oldState return oldState
} }
@ -262,6 +259,6 @@ export function serverReducer(oldState: AppState, action: ServerAction): AppStat
network: serverSocketStartupReducer(oldState.network, action) network: serverSocketStartupReducer(oldState.network, action)
} }
case SERVER_ACT: case SERVER_ACT:
return serverSentReducer(oldState, action) return serverActReducer(oldState, action)
} }
} }

@ -1,5 +1,5 @@
import {SyncedState} from "../state/SyncedState"; import {SyncedState} from "../state/SyncedState";
import {SyncableAction} from "../actions/NetworkAction"; import {SyncableAction} from "../actions/ClientAction";
import {isMapAction, MapAction} from "../actions/MapAction"; import {isMapAction, MapAction} from "../actions/MapAction";
import {hexMapReducer} from "./HexMapReducer"; import {hexMapReducer} from "./HexMapReducer";
import {isUserAction, UserAction} from "../actions/UserAction"; import {isUserAction, UserAction} from "../actions/UserAction";

@ -1,4 +1,4 @@
import {ClientHelloAction, ClientRefreshAction, SendableAction, SentAction} from "../actions/NetworkAction"; import {ClientHelloCommand, ClientRefreshCommand, SendableAction, SentAction} from "../actions/ClientAction";
import {SyncedState} from "./SyncedState"; import {SyncedState} from "./SyncedState";
export enum ServerConnectionState { export enum ServerConnectionState {
@ -35,7 +35,7 @@ export interface NetworkState {
/** /**
* A special action that should take precedence over sending more actions. * A special action that should take precedence over sending more actions.
*/ */
readonly specialMessage: ClientHelloAction|ClientRefreshAction|null readonly specialMessage: ClientHelloCommand|ClientRefreshCommand|null
/** /**
* The ID of the next ClientSentAction to be created. * The ID of the next ClientSentAction to be created.
*/ */
@ -57,7 +57,7 @@ export interface NetworkState {
* The error reason of the close message. * The error reason of the close message.
* Non-null if and only if the current state is OFFLINE or REJECTED. * Non-null if and only if the current state is OFFLINE or REJECTED.
*/ */
readonly goodbyeReason: string | null readonly goodbyeReason: string | Error | null
/** The time the client will attempt to reconnect, if at all. */ /** The time the client will attempt to reconnect, if at all. */
readonly autoReconnectAt: Date | null readonly autoReconnectAt: Date | null
/** The number of attempts at reconnecting. */ /** The number of attempts at reconnecting. */

Loading…
Cancel
Save