0.0.1-alpha.7 • Published 8 months ago

redux-mc v0.0.1-alpha.7

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

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.

NameHow many copies?Where is the state?Who can modify it?
L0Just oneServer onlyServer only
L1Just oneServer and all clientsServer only
L2One per clientServer and clientServer only
L3One per clientServer and clientServer and client
L4One per clientClient onlyClient 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 initial L1 - 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 via ClientStore class methods.
  • 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 initial L0 and L1 state for the server.
  • getInitialClientState(): returns initial L2 and L3 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 and handleL3 also receive a string clientId of the client the action is tareting.
  • 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 kinds. 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

0.0.1-alpha.7

8 months ago