You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
hexmap/client/src/reducers/ServerReducer.ts

282 lines
11 KiB

import {AppState} from "../state/AppState";
import {NetworkState, ServerConnectionState} from "../state/NetworkState";
import {SyncedState} from "../../../common/src/state/SyncedState";
import {applySyncableActions} from "../../../common/src/reducers/SyncedStateReducer";
import {clientReducer} from "./ClientReducer";
import {
SERVER_ACT,
SERVER_FAILED,
SERVER_GOODBYE,
SERVER_HELLO,
SERVER_MALFORMED,
SERVER_OK,
SERVER_REFRESH,
SERVER_SOCKET_STARTUP,
ServerActCommand,
ServerAction,
ServerFailedCommand,
ServerGoodbyeAction,
ServerHelloCommand,
ServerMalformedAction,
ServerOKCommand,
ServerRefreshCommand,
ServerSocketStartupAction,
SocketState,
SyncableAction
} from "../../../common/src/actions/ServerAction";
import {CLIENT_GOODBYE, CLIENT_HELLO, SendableAction} from "../../../common/src/actions/ClientAction";
interface StateRecalculationInputs {
/** The original server state before the actions changed. The base on which the actions are all applied. */
readonly serverState: SyncedState
/** The permanent actions which will be applied to form the new server state. */
readonly permanentActions: readonly SyncableAction[]
/**
* Actions which have already been sent to the server and therefore can't be deleted,
* but have not been approved and can't be applied to the server state.
* Applied to the local state first.
*/
readonly sentActions: readonly SendableAction[]
/**
* Actions which have not yet been sent to the server and therefore can be deleted.
* Applied to the local state second, and failed or no-effect actions are removed from the list returned.
*/
readonly unsentActions: readonly SendableAction[]
}
interface StateRecalculationOutputs {
/** The new server state after applying permanentActions to oldServerState. */
readonly newServerState: SyncedState,
/** The new local state after applying permanentActions, sentActions, and unsentActions (in that order) to oldServerState. */
readonly newLocalState: SyncedState,
/** The unsentActions list from the input, minus any actions which failed or had no effect. */
readonly appliedUnsentActions: readonly SendableAction[]
}
function recalculateStates(input: StateRecalculationInputs): StateRecalculationOutputs {
const newServerState = applySyncableActions(input.serverState, input.permanentActions).state
const {state: sentLocalState} = applySyncableActions(newServerState, input.sentActions)
const {state: newLocalState, appliedActions: appliedUnsentActions} = applySyncableActions(sentLocalState, input.unsentActions)
return { newServerState, newLocalState, appliedUnsentActions }
}
function serverHelloReducer(oldState: AppState, action: ServerHelloCommand): AppState {
const {
newServerState,
newLocalState,
appliedUnsentActions,
} = recalculateStates({
serverState: action.state,
permanentActions: [],
sentActions: [],
unsentActions: oldState.network.pendingActions
})
// TODO: The connection state should be AWAITING_HELLO and the special message should be our Hello
// TODO: Destroy all pending actions if the server's map has a different XID from ours.
return {
...oldState,
localState: newLocalState,
network: {
...oldState.network,
connectionState: ServerConnectionState.CONNECTED,
specialMessage: null,
serverState: newServerState,
pendingActions: appliedUnsentActions,
reconnectAttempts: 0,
}
}
}
function serverRefreshReducer(oldState: AppState, action: ServerRefreshCommand): AppState {
const {
newServerState,
newLocalState,
appliedUnsentActions,
} = recalculateStates({
serverState: action.state,
permanentActions: [],
sentActions: oldState.network.sentActions.map((sent) => sent.action),
unsentActions: oldState.network.pendingActions
})
// TODO: The connection state should be AWAITING_REFRESH and the special message should be our Refresh
// TODO: Destroy all pending actions if the server's map has a different XID from ours.
return {
...oldState,
localState: newLocalState,
network: {
...oldState.network,
connectionState: ServerConnectionState.CONNECTED,
specialMessage: null,
serverState: newServerState,
pendingActions: appliedUnsentActions,
}
};
}
function serverGoodbyeReducer(oldState: NetworkState, action: ServerGoodbyeAction): NetworkState {
// TODO: Sort out the correct state and autoReconnectAt based on the time in the action.
return {
...oldState,
connectionState: ServerConnectionState.OFFLINE,
specialMessage: null,
goodbyeCode: action.code,
goodbyeReason: action.reason,
}
}
function serverOkReducer(oldState: AppState, action: ServerOKCommand): AppState {
if (oldState.network.serverState === null) {
return oldState
}
const okIndexes: {[id: number]: boolean|undefined} = {}
for (let index = 0; index < action.ids.length; index += 1) {
okIndexes[action.ids[index]] = true
}
// TODO: Technically, the server is not obligated to apply the actions in order, so this should reconstruct the
// array the way the server put it. In practice, this is how the server behaves, so it does no harm.
const receivedActions = oldState.network.sentActions.filter((sent) => !!okIndexes[sent.id]).map((sent) => sent.action)
const stillWaitingActions = oldState.network.sentActions.filter((sent) => !okIndexes[sent.id])
const {newServerState, newLocalState, appliedUnsentActions} = recalculateStates({
serverState: oldState.network.serverState,
permanentActions: receivedActions,
sentActions: stillWaitingActions.map((sent) => sent.action),
unsentActions: oldState.network.pendingActions,
})
return {
...oldState,
localState: newLocalState,
network: {
...oldState.network,
serverState: newServerState,
sentActions: stillWaitingActions,
pendingActions: appliedUnsentActions,
}
}
}
function serverFailedReducer(oldState: AppState, action: ServerFailedCommand): AppState {
if (oldState.network.serverState === null) {
return oldState
}
const failedIndexes: {[id: number]: boolean|undefined} = {}
for (let index = 0; index < action.ids.length; index += 1) {
failedIndexes[action.ids[index]] = true
}
// TODO: Figure out somewhere to put the failure message for logging purposes, so the messages aren't wasted.
/* const failedActions = */
oldState.network.sentActions.filter((sent) => !!failedIndexes[sent.id]).map((sent) => sent.action)
const stillWaitingActions = oldState.network.sentActions.filter((sent) => !failedIndexes[sent.id])
const {newServerState, newLocalState, appliedUnsentActions} = recalculateStates({
serverState: oldState.network.serverState,
permanentActions: [],
sentActions: stillWaitingActions.map((sent) => sent.action),
unsentActions: oldState.network.pendingActions,
})
return {
...oldState,
localState: newLocalState,
network: {
...oldState.network,
serverState: newServerState,
sentActions: stillWaitingActions,
pendingActions: appliedUnsentActions,
}
}
}
function serverSocketStartupReducer(oldState: NetworkState, action: ServerSocketStartupAction): NetworkState {
switch (action.state) {
case SocketState.OPEN:
return clientReducer(oldState, {
type: CLIENT_HELLO,
version: 1,
})
case SocketState.CONNECTING:
return {
...oldState,
connectionState: ServerConnectionState.CONNECTING,
serverState: null,
specialMessage: null,
goodbyeCode: null,
goodbyeReason: null,
autoReconnectAt: null,
nextID: 0,
sentActions: [],
pendingActions: [
...oldState.sentActions.map((wrapper) => (wrapper.action)),
...oldState.pendingActions,
],
// Don't clear reconnectAttempts until SERVER_HELLO. Even if we fail _after_ establishing
// a connection, we still want to keep using the longer exponential backoff state, or even give up.
}
}
}
function serverActReducer(oldState: AppState, action: ServerActCommand): AppState {
if (oldState.network.serverState === null) {
return oldState
}
const {
newServerState,
newLocalState,
appliedUnsentActions,
} = recalculateStates({
serverState: oldState.network.serverState,
permanentActions: action.actions,
sentActions: oldState.network.sentActions.map((sent) => sent.action),
unsentActions: oldState.network.pendingActions
})
return {
...oldState,
localState: newLocalState,
network: {
...oldState.network,
connectionState: ServerConnectionState.CONNECTED,
serverState: newServerState,
pendingActions: appliedUnsentActions,
}
};
}
function serverMalformedReducer(oldState: NetworkState, action: ServerMalformedAction): NetworkState {
console.log("Got serverMalformed", action.error)
return clientReducer(oldState, {
type: CLIENT_GOODBYE,
code: 1002, // protocol error
reason: action.error?.message || "Unknown error"
});
}
export function serverReducer(oldState: AppState, action: ServerAction): AppState {
// TODO: Verify that these messages are only received at the proper times and in the proper states.
// e.g., BeginConnecting should only happen when the state is somewhere in the disconnected region.
// Goodbye, OK, Failed, and Sent, on the other hand, _can't_ happen then.
// Hello and Refresh should only happen when we are explicitly waiting for them.
switch (action.type) {
case SERVER_HELLO:
return serverHelloReducer(oldState, action);
case SERVER_REFRESH:
return serverRefreshReducer(oldState, action);
case SERVER_GOODBYE:
return {
...oldState,
network: serverGoodbyeReducer(oldState.network, action)
};
case SERVER_OK:
return serverOkReducer(oldState, action);
case SERVER_FAILED:
return serverFailedReducer(oldState, action);
case SERVER_SOCKET_STARTUP:
return {
...oldState,
network: serverSocketStartupReducer(oldState.network, action)
}
case SERVER_ACT:
return serverActReducer(oldState, action)
case SERVER_MALFORMED:
return {
...oldState,
network: serverMalformedReducer(oldState.network, action)
}
}
}