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 {CellColorAction, isCellColorAction} from "./CellAction";
import {SyncedState} from "../state/SyncedState";
import {isUserActiveColorAction, UserActiveColorAction} from "./UserAction";
// TODO: Split into ServerAction and ClientAction files
export const SERVER_HELLO = "SERVER_HELLO"
/** Sent in response to the client's ClientHelloAction when the client has been accepted. */
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)
}
import {ClientAction, isClientAction} from "./ClientAction";
// Terminology: Messages are the data packets sent and received over the WebSocket connection.
// Commands are the protocol laid on top of messages.
// Actions are data objects that the client uses to communicate between parts of itself.
// All commands are both messages and actions.
// An action is a message if and only if it is a command.
// A message is an action if and only if it is a command.
export type NetworkAction = ServerAction | ClientAction;
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 {tileReducer} from "./TileReducer";
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 {exhaustivenessCheck} from "../util/TypeUtils";
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";
// TODO: Verify that only one special message exists at a time.

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

@ -1,29 +1,26 @@
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 {
CLIENT_HELLO,
SendableAction,
SERVER_ACT,
SERVER_FAILED,
SERVER_GOODBYE,
SERVER_HELLO,
SERVER_OK,
SERVER_REFRESH,
SERVER_ACT,
SERVER_SOCKET_STARTUP,
ServerAction,
ServerFailedAction,
ServerGoodbyeAction,
ServerHelloAction,
ServerOKAction,
ServerRefreshAction,
ServerActAction,
SERVER_SOCKET_STARTUP, ServerActCommand,
ServerAction, ServerFailedCommand,
ServerGoodbyeCommand,
ServerHelloCommand,
ServerOKCommand,
ServerRefreshCommand,
ServerSocketStartupAction,
SocketState,
SyncableAction,
} from "../actions/NetworkAction";
import {NetworkState, ServerConnectionState} from "../state/NetworkState";
import {SyncedState} from "../state/SyncedState";
import {applySyncableActions} from "./SyncedStateReducer";
import {clientReducer} from "./ClientReducer";
SyncableAction
} from "../actions/ServerAction";
import {CLIENT_HELLO, SendableAction} from "../actions/ClientAction";
interface StateRecalculationInputs {
/** 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 }
}
function serverHelloReducer(oldState: AppState, action: ServerHelloAction): AppState {
function serverHelloReducer(oldState: AppState, action: ServerHelloCommand): AppState {
const {
newServerState,
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 {
newServerState,
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.
return {
...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) {
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) {
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) {
return oldState
}
@ -262,6 +259,6 @@ export function serverReducer(oldState: AppState, action: ServerAction): AppStat
network: serverSocketStartupReducer(oldState.network, action)
}
case SERVER_ACT:
return serverSentReducer(oldState, action)
return serverActReducer(oldState, action)
}
}

@ -1,5 +1,5 @@
import {SyncedState} from "../state/SyncedState";
import {SyncableAction} from "../actions/NetworkAction";
import {SyncableAction} from "../actions/ClientAction";
import {isMapAction, MapAction} from "../actions/MapAction";
import {hexMapReducer} from "./HexMapReducer";
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";
export enum ServerConnectionState {
@ -35,7 +35,7 @@ export interface NetworkState {
/**
* 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.
*/
@ -57,7 +57,7 @@ export interface NetworkState {
* The error reason of the close message.
* 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. */
readonly autoReconnectAt: Date | null
/** The number of attempts at reconnecting. */

Loading…
Cancel
Save