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.
267 lines
10 KiB
267 lines
10 KiB
import {AppState} from "../state/AppState";
|
|
import {
|
|
CLIENT_HELLO,
|
|
SendableAction,
|
|
SERVER_FAILED,
|
|
SERVER_GOODBYE,
|
|
SERVER_HELLO,
|
|
SERVER_OK,
|
|
SERVER_REFRESH,
|
|
SERVER_ACT,
|
|
SERVER_SOCKET_STARTUP,
|
|
ServerAction,
|
|
ServerFailedAction,
|
|
ServerGoodbyeAction,
|
|
ServerHelloAction,
|
|
ServerOKAction,
|
|
ServerRefreshAction,
|
|
ServerActAction,
|
|
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";
|
|
|
|
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: ServerHelloAction): 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: ServerRefreshAction): 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: ServerOKAction): 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: ServerFailedAction): 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 serverSentReducer(oldState: AppState, action: ServerActAction): 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,
|
|
}
|
|
};
|
|
}
|
|
|
|
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 serverSentReducer(oldState, action)
|
|
}
|
|
} |