Private State
Up to this point, everything is pretty much in line with how Redux works, but the following concepts are specific to Movex, and they are needed in order to make the possible to store secret state fragments:
Private Actions
const myPrivateAction = {
type: 'incrementBy';
payload: 20;
isPrivate: true;
}
A Private Action is a special kind of action which can only be dispatched with a paired Public Action, and its purpose is to update a fragment of the state with secret information. This secret information is usually not meant to stay secret forever but to be revealed at a point in the future (when each player submits their moves for example), and thus to become part of the Shared (aka Public) State once again.
This is where the paired Public Action comes in, which purpose is to update the same or related fragment of the state, with a public value, meant as a temporary placeholder until the Revelation Point.
This means that the Shared State becomes out of sync for a while, until the Revelation Point, when the Reconciliation Check returns true
. In this temporary out-of-sync state, each author of a private action will only get access to its specific private/secret state (public state + the specific secret fragments applied over),
A Private Action can only be sent in pair with a Public Action. The public action is needed to create the changes in the Public State in order for the $canRevealPrivateState
handler to determine if it can reconcile the states or not yet. Think about it as a record of the private action being taken (could be a change of status for that player), without actually revealing the content of the action.
Let's see how will Movex process this in each environment:
- For the Client
The Sender Client, Movex dispatches the Private Action which will simply return the next state, BUT disregards the paired Public Action one as that is only relevant to its peers. The client can make use of the next private state right away, (aka optimistic update) even before the server acknwoledges it, making use of the mechanism of Deterministic Propagation once again, thus avoiding any lags.
- For Master (aka Server Authority)
The Master Movex will dispatch the private action, but knowing behind the scenes that it's a private action, it will do something different with the result. Instead of simply merging it into the publc state, it will compute a json patch from the prev to the next, and store that as a Private Patch along with the Action for each Client. This will allow the Sender Client to always get the Private State even after a resource refetch, while all the others to get the Public State or their own Private State (in case they have dispatched Private Actions as well and the state hasn't reconciled yet).
Reconciliatory Checks
A Reconciliatory Check is what determines when the secret fragments could reconcile back into the Public State.
It is a static method on the Reducer, and it is the only code that runs only on the Master (i.e. Server), but it's minimal and part of the same Reducer that runs on the Client, which makes movex almoooost serverless :).
This check is run on the Master (Server) after a Private Action gets dispatched. When it returns true
the Master knows that it's time to reconcile all the private fragments existents up to this point, and it simply merges them on top of the last Public State, resulting in the next Public State with all the secret fragments revealed, and computes a final checksum. It then broadcasts to all the clients the Private Actions dispatched prviously by their peers in the arrived order, thus allowing the client reducers to arrive at the exact state the master did. If there is any mismatch, the strategies described in the State Synchronization article happen.
Here it is in action:
// game.movex.ts
type State = {
playerA: {
submitted: boolean;
moves: Move[] | undefined;
};
playerB: {
submitted: boolean;
moves: Move[] | undefined;
};
}
const gameReducer = (state = initialCounterState, action: CounterActions) => {
if (action.type === 'submitMoves') {
const { payload } = action;
return {
...state,
// Apply the next submission to the submitting player
[payload.playerId]: {
submitted: true,
moves: payload.moves, // reveal the moves
}
};
}
if (action.type === 'setSubmitted') {
const { payload } = action;
return {
...state,
// Apply the next submission to the submitting player
[payload.playerId]: {
// Set the status to submitted so the other player can see I submitted
// and also for the canReveal Validation to determine when to reconcile
submitted: true,
moves: [], // hide the moves
}
};
}
return state;
};
// As a static method on the reducer function to check wether the
// It returns true when it's time to reveal all the private states by
// reconciling them all into the Public State
gameReducer.$canReconcile: (state: State): boolean => {
// If both players submitted than it's time to Reveal the moves!!!
return state.playerA.submitted && state.playerB.submitted;
}
// game-ui.ts
dispatchPrivate(
resourceIdentifier,
{
type: 'submitMoves',
payload: {
playerId: 'playerA',
moves: ['e2e4', 'f2f4'],
}
isPrivate: true,
},
{
type: 'setSubmitted',
payload: {
playerId: 'playerA',
}
})