redux-socket-server
TypeScript icon, indicating that this package has built-in type declarations

0.1.37 • Public • Published

Redux Socket Server

Lightweight framework for building distributed redux stores using socket.io.

Features

  • Shared store and actions with a single middleware
  • Managers and clients for advanced scenarios
  • Permission management
  • Server-side broadcast
  • Synchronisation using locks

Installation

Redux Socket Server is in beta and NOT ready for production use.

To install the latest version, use NPM:

$ npm install redux-socket-server

Basics

The primary aim of this component is to provide a distributed redux store for real time web apps running on multiple devices at the same time. With a distributed store a group of clients and the server (which can be a cluster) can share a redux store.

This means that an action dispatched at a client, can cause a change at other clients, or can have an effect on the server. This concept can be useful for distributing changes real time across the devices of a single user, or for real time collaboration apps.

The distributed store has a predefined base structure and actions can be tagged to be distributed, otherwise the distributed store works just like any ordinary redux store, both on the client and the server.

Store Structure

The distributed store consists of two parts: shared and clients. The former provides a shared storage, which is available for every client, while the latter is the list of the client-specific stores. Every client has access to it's own store, while managers and the server has access to whole list of client stores. Also managers and the server, has write access to the shared part, while other clients only have read access.

The content of the shared part and each client part are specified by the application.

The store from a standard client:

{ 
    shared: { 
        // Read-only data, same for every client    
    }, 
    client: { 
        id: 'client-a',
        // Unique data of this client (also available on the server)   
    },
    // Anything else, only available locally for this client  
}

The store from a manager client or a server node:

{ 
    shared: { 
        // Shared data with write access
    }, 
    clients: { 
        items: [
            {
                id: 'client-a',
                //Unique data of Client A
            },
            {
                id: 'client-b',
                //Unique data of Client B
            }
        ],
        mappings: {
            'client-a': 0,
            'client-b': 1
        } 
    },
    // Anything else, only available locally
}

Actions and reducers

A standard redux action will only be executed locally, as always. To make it distributed, it have to be tagged as a client or a shared action.

Both tags will transfer the action to the server, but for permission management to function as expected, it is recommended to make sure client actions has no direct effect on the shared part of the store. (Of course shared actions can have an effect on the client part too.)

To tag an action, it's action creator must be tagged on declaration:

import { client, shared } from 'redux-socket-client';
 
export const addPrivateNote = client((value) => ({
    type: 'ADD_PRIVATE_NOTE',
    payload: {
        note: value
    }
}));
 
export const addPublicNote = shared((value) => ({
    type: 'ADD_PUBLIC_NOTE',
    payload: {
        note: value
    }
}));

Reducers work just like in a normal react app, except, here distributed reducers will be executed not just on every client, but also on every server node. Also there are some predefined actions, which must be handled correctly:

  • PRESENT: When a client (including managers) connects to the server, the current distributed state will be retrieved from the server node as a PRESENT action.
  • ADD_CLIENT: When a non-manager client connects to the server, the ADD_CLIENT action will be dispatched. The reducer must initialize the client part as the effect of this action.

You should provide at least one reducer for each part of the store. While the shared reducer is straightforward, the client reducer MUST be implemented to handle a single client and not the array of clients.

The minimal client reducer:

import { ADD_CLIENT, PRESENT } from 'redux-socket-client';
 
export const client = (state = {}, action) => {
    switch(action.type) {
        case ADD_CLIENT:
            return {
                id: payload.id,
                ...payload.details
                // Anything else you need for every client store...
            }
 
        case PRESENT:
            return payload.state.client
 
        default:
            return state
    }
}

The minimal shared reducer:

import { PRESENT } from 'redux-socket-client';
 
export const shared = (state = {}, action) => {
    switch(action.type) {
        case PRESENT:
            return payload.state.shared
 
        default:
            return state
    }
}

Setup

Setup in React apps

The store setup in react apps is mostly the same as for normal react apps, the only important exception is the sharedStoreMiddleware, which handles tagged actions.

Setup for standard clients:

import {createStore, applyMiddleware, combineReducers} from 'redux';
import {sharedStoreMiddleware} from 'redux-socket-client';
import {connect} from 'socket.io-client';
import {shared, client} from './reducers';
 
const socket = connect('wss://...');
 
const store = createStore(
    combineReducers({ shared, client }),
    applyMiddleware(sharedStoreMiddleware(socket, { clientFirst: true }))
)

If it is important to execute actions on client side as soon as possible, you should add { clientFirst: true }, otherwise actions will be sent to the server and only executed on the client, once the server processed and sent those back.

Setup for manager clients:

import {createStore, applyMiddleware, combineReducers} from 'redux';
import {sharedStoreMiddleware, combineClients} from 'redux-socket-client';
import {connect} from 'socket.io-client';
import {shared, client} from './reducers';
 
const socket = connect('wss://...');
 
this.store = createStore(
    combineReducers({ shared, clients: combineClients(client) }),
    applyMiddleware(sharedStoreMiddleware(socket))
)

Please notice clients: combineClients(client). This makes possible to handle the array of clients with the reducer built for single clients.

Setup for server nodes

On the server side, you have to use the SharedStore class as a wrapper around your redux store.

import {createStore, combineReducers} from 'redux';
import {SharedStore, combineClients} from 'redux-socket-server';
import {shared, client} from './reducers';
 
const store = new SharedStore(
    io, //Your socket.io instance for communication with the clients.
    createStore(
        combineReducers({ shared, clients: combineClients(client) }),
        {
            shared: {
               // The initial state of the shared part of the store.
            }
        }
    ),
    queue // [optional] Distributed queue
);

If you use multiple server nodes, you must use a distributed queue implementation for the store. A Redis based implementation is built into the library:

import {RedisQueue} from 'redux-socket-server';
 
const queue = new RedisQueue(
    redisClient1,
    redisClient2, 
    'prefix' // [optional] Prefix to be used for redis keys. 
);

Authentication

The server must authenticate every socket connection before the store can be used by a client.

store.on('authentication', (socket, authorize) => {
    if(isAuthenticated(socket)) {
        authorize(
            isManager(socket),     // Decide whether this user is a manager.
            getUserId(socket),     // Provide an optional user id.
            getUserDetails(socket) // [optional] User details.
        )
    }
    else {
        //Kick out the unauthorized socket.
        socket.disconnect(true)
    }
});

If no user id is provided the socket id will be used instead, but to handle multiple sockets for the same user it must be provided.

Server side reaction to actions

The SharedStore implementation supports almost every method specified by the Redux Store API:

  • dispatch(action)
  • getState()
  • subscribe(listener)

It also adds a custom method for dispatching actions to a specified client: dispatchToClient(clientId, action), plus one for stopping the store: stop().

Handling incoming actions on the server side can be implemented using subscribe, which provides some useful parameters for the listener:

  • action: The action which caused the call of the listener
  • clientId: The id of the client, if it is an action tagged as client.
  • prevPresent: The previous state and version number. ({ state: { shared, clients }, version })
  • present: The current state and version number. ({ state: { shared, clients }, version })

If it is required to handle actions, or execute specific tasks only on a single master node, the lock event of the RedisQueue class can be used. This will be fired every time a new master is selected:

queue.on('lock', () => {
    // Actions/tasks only on the master node...
})

Once a master gets selected, it won't loose it status, unless it crashes. In this case a new master will be assigned automatically within 1 second.

License

The MIT License. Free forever. :)

Package Sidebar

Install

npm i redux-socket-server

Weekly Downloads

8

Version

0.1.37

License

MIT

Unpacked Size

81.4 kB

Total Files

37

Last publish

Collaborators

  • ajuhos
  • lsc-deploy