From 44b2813922a6f0a8c594b24be098c56e946ae538 Mon Sep 17 00:00:00 2001 From: Mari Date: Fri, 16 Jul 2021 13:13:52 -0400 Subject: [PATCH] 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). --- client/src/actions/ClientAction.ts | 64 ++++++++ client/src/actions/NetworkAction.ts | 173 ++-------------------- client/src/actions/ServerAction.ts | 111 ++++++++++++++ client/src/reducers/AppStateReducer.ts | 2 +- client/src/reducers/ClientReducer.ts | 2 +- client/src/reducers/NetworkReducer.ts | 8 +- client/src/reducers/ServerReducer.ts | 45 +++--- client/src/reducers/SyncedStateReducer.ts | 2 +- client/src/state/NetworkState.ts | 6 +- 9 files changed, 217 insertions(+), 196 deletions(-) create mode 100644 client/src/actions/ClientAction.ts create mode 100644 client/src/actions/ServerAction.ts diff --git a/client/src/actions/ClientAction.ts b/client/src/actions/ClientAction.ts new file mode 100644 index 0000000..907e21f --- /dev/null +++ b/client/src/actions/ClientAction.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) +} \ No newline at end of file diff --git a/client/src/actions/NetworkAction.ts b/client/src/actions/NetworkAction.ts index 2f041c9..7ebde07 100644 --- a/client/src/actions/NetworkAction.ts +++ b/client/src/actions/NetworkAction.ts @@ -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 { diff --git a/client/src/actions/ServerAction.ts b/client/src/actions/ServerAction.ts new file mode 100644 index 0000000..ea6c3a0 --- /dev/null +++ b/client/src/actions/ServerAction.ts @@ -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) +} \ No newline at end of file diff --git a/client/src/reducers/AppStateReducer.ts b/client/src/reducers/AppStateReducer.ts index 9196c73..1cbdf25 100644 --- a/client/src/reducers/AppStateReducer.ts +++ b/client/src/reducers/AppStateReducer.ts @@ -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"; diff --git a/client/src/reducers/ClientReducer.ts b/client/src/reducers/ClientReducer.ts index dec905d..002b631 100644 --- a/client/src/reducers/ClientReducer.ts +++ b/client/src/reducers/ClientReducer.ts @@ -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. diff --git a/client/src/reducers/NetworkReducer.ts b/client/src/reducers/NetworkReducer.ts index b471911..a6a5e39 100644 --- a/client/src/reducers/NetworkReducer.ts +++ b/client/src/reducers/NetworkReducer.ts @@ -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) } \ No newline at end of file diff --git a/client/src/reducers/ServerReducer.ts b/client/src/reducers/ServerReducer.ts index 3bade32..b1c90d5 100644 --- a/client/src/reducers/ServerReducer.ts +++ b/client/src/reducers/ServerReducer.ts @@ -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) } } \ No newline at end of file diff --git a/client/src/reducers/SyncedStateReducer.ts b/client/src/reducers/SyncedStateReducer.ts index 9076a12..0cc8d12 100644 --- a/client/src/reducers/SyncedStateReducer.ts +++ b/client/src/reducers/SyncedStateReducer.ts @@ -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"; diff --git a/client/src/state/NetworkState.ts b/client/src/state/NetworkState.ts index 7f97495..c6208b2 100644 --- a/client/src/state/NetworkState.ts +++ b/client/src/state/NetworkState.ts @@ -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. */