redux-mc v0.0.1-alpha.7
Redux MC
Redux state management for Multi-Client applications.
What is this?
Redux is great for simplifying state management in client applications.
But for applications where multiple clients can interact via the server (such as games or chat rooms), an additional solution becomes necessary in order to synchronize shared state across clients, as well as to allow each client to make updates to state without opening the door for abuse.
Redux MC enables applications to create a Redux store that is synchronized across the server and multiple clients. State is segmented into five sections with different access levels. Each client can dispatch actions that affect only the parts of state that they are allowed to modify, and only the portions of state that are allowed to be shared with each client are sent from the server.
This allows applications to enable multi-client interactions while maintaining data safety and continuing to use Redux end-to-end.
State Format
The Redux store state is segmented into five portions, representing different levels of access.
Name | How many copies? | Where is the state? | Who can modify it? |
---|---|---|---|
L0 | Just one | Server only | Server only |
L1 | Just one | Server and all clients | Server only |
L2 | One per client | Server and client | Server only |
L3 | One per client | Server and client | Server and client |
L4 | One per client | Client only | Client only |
L0
and L1
are the server state. L0
is private to the server, and L1
is shared with all clients.
L2
, L3
, and L4
are the client state. L2
can only be modified by the server, L3
can be modified by either server or client, and L4
can only be modified by the client. L4
is private to the client.
Note
There is also a meta
section in the client state. This is controlled by Redux MC itself and contains data that may be useful for applications, such as the connection status, unique client ID, and calculated time offset from the server clock.
Example State
In a card game:
L0
might contain the order of the (face down) deck. It is known to the server, but no player can see it.L1
might contain the players in the room, how many cards are in each of their hands, the turn order, and the current player. Every player can see this, but they cannot change it directly.L2
might contain the cards in each player's hand. It is known only to each player and the server, but controlled by the server alone (a player cannot directly change their hand).L3
might contain each player's name or avatar. The player is free to modify this.L4
might contain UI state such as whether the menu is open, theme settings, etc... It is entirely controlled by the client and does not need to be synchronized to the server.
Action Format
In addition to the typical type
and payload
, each action is marked with a kind
. This can be one of L0
, L1
, L2
, L3
, L4
, or Req
.
L0
- L4
actions are associated with the respective portions of state -- L0
, L1
, and L2
actions can only be dispatched by the server, L3
can be dispatched by either server or client, and L4
can only be dispatched by the client.
Req
actions are "requests" sent to the server by the client. They represent intents to update the state, but are subject to validation / modification / rejection by the server. In the card game example, drawing a card might be a Req
action. The server would first validate whether the client is allowed to draw a card at this time, and only then dispatch an L2
action to update their hand (as well as L0
and L1
actions to update the server state appropriately).
Note
If inspecting the messages between client and server, you may notice an additional Meta
action type. These actions are used by Redux MC itself for things like sending initial state and action batching. Applications should not need to worry about these messages, nor should they attempt to send or listen for them directly.
Middlewares
Applications using Redux MC can provide a middleware for each type of action.
Middlewares are useful for performing business logic (especially in response to Req
actions in the server), or for synchronizing different parts of state as appropriate.
For example, in our card game, after a player draws a card and an L2
action is dispatched to update the player's hand, an L2
middleware can dispatch an L1
action to update the number of cards in that player's hand for all players to see.
How to use
npm install redux-mc
(TODO) See the samples
directory for example applications.
Redux MC is designed to allow code reuse across the client and server.
In a typical Redux MC application, shared code specifies the store logic. This includes all reducers, actions, and initial state. Middlewares differ across server and client.
In the client
Applications should extend the ClientStore
base class. The following fields must be overridden:
getInitialState()
: returns initialL1
-L4
state for the client.reduceL1
-reduceL4
: reducers for each seciton of the state.- Important: these should come from shared code to ensure the server reducers are exactly the same.
handleL1
-handleL4
,handleReq
: (optional) middlewares for each type of action. These aren't typically shared.- These receive an
action
argument. State can be retrieved viaClientStore
class methods.
- These receive an
version
: a string version identifier. The client and server versions are compared on connect to ensure that the server and client are compatible.- This usually only needs to be updated when the application actions, reducers, or Redux MC itself are updated. Redux MC does not provide a mechanism to version this automatically -- it is up to the application to do so.
onVersionMismatch()
: Callback when a version mismatch is detected. The client should usually refresh to resolve this.
The client can be connected to the server via the connect()
method. The method should be called with a single argument which is a connection to the server. Currently the only connection method is WebSocketConnection
.
The redux store can be accessed via the store
property for use in the client application.
import { ClientStore, WebSocketConnection } from "redux-mc/client";
class MyAppClient extends ClientStore {
// ...
}
const client = new MyAppClient();
client.connect(new WebSocketConnection("wss://some.url"));
// Redux store for use in the application
const store = client.store;
In the server
Applications should extend the ServerStore
base class. The following fields must be overridden:
getInitialServerState()
: returns initialL0
andL1
state for the server.getInitialClientState()
: returns initialL2
andL3
state for each client.reduceL0
-reduceL3
: reducers for each seciton of the state.- Important: these should come from shared code to ensure the server reducers are exactly the same.
handleL0
-handleL3
,handleReq
: (optional) middlewares for each type of action. These aren't typically shared.- In addition to the
action
argument,handleL2
andhandleL3
also receive a stringclientId
of the client the action is tareting.
- In addition to the
version
: a string version identifier. The client and server versions are compared on connect to ensure that the server and client are compatible.- This usually only needs to be updated when the application actions, reducers, or Redux MC itself are updated. Redux MC does not provide a mechanism to version this automatically -- it is up to the application to do so.
Note that, in the server, the store should not be accessed directly. Actions should be dispatched via the dispatch()
method on the class. Note that for L2
and L3
actions, a second parameter clientId
must be passed to indicate which client the action is targeting. State should be accessed via get*State()
methods on the class.
Clients can be connected to the server via the addClient
method (and removed via removeClient
). This is done automatically by the WebSocketClient
. But applications can also extend the BaseClient
class to implement their own clients. This is useful for implementing non-human clients such as chat bots or AI players.
import { ServerStore, WebSocketClient } from "redux-mc/server";
class MyAppServer extends ServerStore {
// ...
}
const server = new MyAppServer();
// Assume `ws` is a WebSocket to the client, and `id` is a unique string ID for the client.
new WebSocketClient(ws, server, id);
Action helpers
Redux MC provides helpers for creating actions with kind
s. These are available under redux-mc/util
.
create<Kind>Action(type)()
: constructs an action creator. For example (With TypeScript typings):
import { createL2Action } from "redux-mc/util";
const myAction = createL2Action("myAction")<MyPayloadType>();
const action = myAction(/* MyPayloadType contents here */);
The action type can be accessed via the type
property on the action creator:
switch (action.type) {
case myAction.type:
// ...
}
Custom clients
TODO
TypeScript
Redux MC comes with built-in TypeScript support.
TODO
8 months ago