Skip to content

Latest commit

 

History

History
460 lines (418 loc) · 13.9 KB

README.md

File metadata and controls

460 lines (418 loc) · 13.9 KB

Documentation

Table of Contents

Overview

Quickstart

Install

Install the package with npm

npm i @trixta/phoenix-to-redux or yarn - whichever you prefer

yarn add @trixta/phoenix-to-redux

1. Setup Reducer

/**
 * Combine all reducers in this file and export the combined reducers.
 */

import { combineReducers } from 'redux';
import { phoenixReducer } from '@trixta/phoenix-to-redux';

export default function createReducer() {
  const rootReducer = combineReducers({
    phoenix: phoenixReducer,
  });
  return rootReducer;
}

2. Setup Middleware

See example to setup middleware

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { createPhoenixChannelMiddleware } from '@trixta/phoenix-to-redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './rootSaga';
import createReducer from './reducers';


export default function configureStore(initialState = {}) {
  const reduxSagaMonitorOptions = {};
  // Makes redux connected to phoenix channels
  const phoenixChannelMiddleWare = createPhoenixChannelMiddleware();
  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
  const middlewares = [sagaMiddleware,phoenixChannelMiddleWare];

  const enhancers = [];

  const store = configureStore({
    reducer: createReducer(),
    middleware: [
      ...getDefaultMiddleware({
        thunk: false,
        immutableCheck: {
          ignore: ['socket', 'channel', 'trixta', 'phoenix', 'router'],
        },
        serializableCheck: false,
      }),
      ...middlewares,
    ],
    devTools:
      /* istanbul ignore next line */
      process.env.NODE_ENV !== 'production' ||
      process.env.PUBLIC_URL.length > 0,
    enhancers,
  });

  sagaMiddleware.run(rootSaga);

  return store;
}

3. Setup Socket Details

import { put } from 'redux-saga/effects';
import {  connectPhoenix } from '@trixta/phoenix-to-redux';
// update login details
yield put(connectPhoenix({ domainUrl: 'localhost:4000', params: { token:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmF6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',agentId: '[email protected]'} }));

Redux Saga Example

In our saga for a container we listen for the dispatched action REQUEST_LOGIN and respond with loginSaga function

import { put, select, takeLatest } from 'redux-saga/effects';
import { routePaths } from '../../route-paths';
import {
  REQUEST_LOGIN,
  REQUEST_LOGIN_FAILURE,
  REQUEST_LOGIN_SUCCESS,
  REQUEST_LOGIN_TIMEOUT,
} from './constants';
export default function* loginPageSaga() {
  // listen for REQUEST_LOGIN action
  yield takeLatest(REQUEST_LOGIN, loginSaga);
// listen for REQUEST_LOGIN_SUCCESS action
  yield takeLatest(REQUEST_LOGIN_SUCCESS, handleLoginSuccessSaga);
// listen for REQUEST_LOGIN_FAILURE action
  yield takeLatest(REQUEST_LOGIN_FAILURE, handleLoginFailureSaga);
}

On REQUEST_LOGIN the below loginSaga function is going to do the following

import _ from 'lodash';
import { routePaths } from '../../route-paths';
import { put, select, takeLatest } from 'redux-saga/effects';
import {
  connectPhoenix
  getPhoenixChannel,
  pushToPhoenixChannel,
} from '@trixta/phoenix-to-redux';
import {
  REQUEST_LOGIN,
  REQUEST_LOGIN_FAILURE,
  REQUEST_LOGIN_SUCCESS,
  REQUEST_LOGIN_TIMEOUT,
} from './constants';
/**
 *
 * @param data
 * @returns {IterableIterator<IterableIterator<*>|void|*>}
 */
export function* loginSaga({ data }) {
  try {
    // show loading indicator
    yield put(loggingIn());
    const channelTopic = 'authentication';
    // get the login domain data passed from the requestAuthentication action
    const domain = _.get(data, 'domain', '');
    // get the anonymous channel and socket
    yield put(connectPhoenix({ domainUrl: domain }))
    yield put(
      getPhoenixChannel({
        domainUrl: domain,
        channelTopic,
      })
    );
    // push the data to 'authentication' channel topic
    // domain will be available on the response because its passed as additionalData
    // on OK response from channel dispatch REQUEST_LOGIN_SUCCESS
    // on error response from channel dispatch REQUEST_LOGIN_FAILURE
     // on timeout response from channel dispatch REQUEST_LOGIN_TIMEOUT
    yield put(
    pushToPhoenixChannel({
      channelTopic,
      eventName: authenticationEvents.LOGIN,
      channelResponseEvent: REQUEST_LOGIN_SUCCESS,
      channelErrorResponseEvent: REQUEST_LOGIN_FAILURE,
      requestData: data,
      additionalData: { domainUrl },
      dispatchChannelError: true,
      customerTimeoutEvent: REQUEST_LOGIN_TIMEOUT,
    });
  } catch (error) {
    yield put(loginFailed(error));
    yield put(updateError({ error: error.toString() }));
  }
}

On REQUEST_LOGIN_SUCCESS the below handleLoginSuccessSaga function is going to do the following

import _ from 'lodash';
import { routePaths } from '../../route-paths';
import { makeSelectRouteLocation } from '../App/selectors';
import { put, select } from 'redux-saga/effects';
import {
  updateError,
  defaultLoad,
  loginFailed,
} from '../App/actions';
import {
  connectPhoenix,
} from '@trixta/phoenix-to-redux';
import {
  PHOENIX_TOKEN,
  PHOENIX_SOCKET_DOMAIN,
  PHOENIX_AGENT_ID,
} from '../../config';
/**
 *
 * @param data
 * @returns {IterableIterator<*>}
 */
export function* handleLoginSuccessSaga({ data }) {
  // on success of login take the response data
  if (data) {

    // eslint-disable-next-line camelcase
    // additionalData you passed
    const domainUrl = _.get(data, 'domainUrl');
    const agentId = _.get(data, 'agent_id', '');
    const identity = _.get(data, 'identity', '');
    const token = _.get(data, 'jwt', '');
    // eslint-disable-next-line camelcase
    // update phoenix storage keys for future phoenix socket channel calls
    setLocalStorageItem(PHOENIX_SOCKET_DOMAIN, domainUrl);
    setLocalStorageItem(PHOENIX_TOKEN, token);
    setLocalStorageItem(PHOENIX_AGENT_ID, agentId);
    // connect authenticated phoenix socket
     yield put(connectPhoenix({ domainUrl, params: { agentId, token } }));
    yield put(push('/home'));
  } else {
    yield put(loginFailed());
  }
}

On REQUEST_LOGIN_FAILURE the below handleLoginFailureSaga function is going to do the following

import _ from 'lodash';
import { put } from 'redux-saga/effects';
import {
  disconnectPhoenix,
  updatePhoenixLoginDetails,
  getAnonymousPhoenixChannel,
  pushToPhoenixChannel,
} from '@trixta/phoenix-to-redux';
import {
  updateError,
  defaultLoad,
} from '../App/actions';
export function* handleLoginFailureSaga(error) {
  yield put(
    updateError({
      error: _.get(error.data, 'reason', 'Authentication Failed'),
    }),
  );
  yield put(defaultLoad());
}

Responding to Channel Events

import {
  all,
  call,
  fork,
  put,
  select,
  takeEvery,
  takeLatest,
} from 'redux-saga/effects';
import {
  channelActionTypes,
} from '@trixta/phoenix-to-redux';



/**
 * Response after joining a phoenix channel with an error
 * @param {Object} params - parameters
 * @param {string} params.error - phoenix channel error
 * @param {string} params.channelTopic - phoenix channel topic
 */
export function* channelJoinErrorSaga({ error, channelTopic }) {
  console.error('channelJoinErrorSaga', error,, channelTopic);
}

/**
 * Invoked if the socket connection drops, or the channel crashes on the server. In either case, a channel rejoin is attempted automatically in an exponential backoff manner
 * @param {Object} params - parameters
 * @param {string} params.channel - phoenix channel
 * @param {string} params.channelTopic - phoenix channel topic
 */
export function* channelErrorSaga({ channel, channelTopic }) {
  console.error('channelErrorSaga', error, channel, channelTopic);
}

/**
 * Response after joining a phoenix channel with a timeout
 * @param {Object} params - parameters
 * @param {string} params.error - phoenix channel error
 * @param {string} params.channelTopic -  Name of channel/Topic
 */
export function* channelTimeOutErrorSaga({ error, channelTopic }) {
  console.error('channelTimeOutErrorSaga', error,, channelTopic);
}

/**
 * Response after pushing a request to phoenix channel with an error
 * @param {Object} params - parameters
 * @param {string} params.error - phoenix channel error
 * @param {string} params.channel - phoenix channel
 * @param {string} params.channelTopic - phoenix channel topic
 */
export function* channelPushErrorSaga({ error, channel, channelTopic }) {
  console.error('channelPushErrorSaga', error, channel, channelTopic);
}

/**
 * Response after joining a phoenix channel
 * @param {Object} params - parameters
 * @param {Object} params.response - response from joining channel
 * @param {Object} params.channel - phoenix channel
 */
export function* channelJoinSaga({ response, channel }) {
  console.info('channelJoinSaga', channel, channelTopic);
}

/**
 * Response after leaving a phoenix channel
 * @param {Object} params - parameters
 * @param {Object} params.channel - phoenix channel
 */
export function* channelLeaveSaga({ channel }) {
  console.info('channelLeaveSaga', channel, channelTopic);
}

/**
 * Invoked only in two cases. 1) the channel explicitly closed on the server, or 2). The client explicitly closed, by calling channel.leave()
 * @param {Object} params - parameters
 * @param {Object} params.channel - phoenix channel
 */
export function* channelCloseSaga({  channel }) {
  console.info('channelCloseSaga',  channel);
}


export default function* defaultSaga() {
  yield takeEvery(channelActionTypes.CHANNEL_JOIN_ERROR, channelJoinErrorSaga);
  yield takeEvery(channelActionTypes.CHANNEL_ERROR, channelErrorSaga);
  yield takeEvery(channelActionTypes.CHANNEL_TIMEOUT, channelTimeOutErrorSaga);
  yield takeEvery(channelActionTypes.CHANNEL_PUSH_ERROR, channelPushErrorSaga);
  yield takeEvery(channelActionTypes.CHANNEL_JOIN, channelJoinSaga);
  yield takeEvery(channelActionTypes.CHANNEL_LEAVE, channelJoinSaga);
  yield takeEvery(channelActionTypes.CHANNEL_CLOSE, channelCloseSaga);
}

Responding to Socket Events

import {
  all,
  call,
  fork,
  put,
  select,
  takeEvery,
  takeLatest,
} from 'redux-saga/effects';
import {
  socketActionTypes,
} from '@trixta/phoenix-to-redux';


/**
 * Should the socket attempt to open, this action is dispatched to the
 * phoenix reducer
 * @param {Object} params - parameters
 * @param {string} params.domainKey - domain for socket
 * @param {Object} params.socket = socket being opened
 * @param {boolean} params.isAnonymous - true if socket was anonymous
 */
export function* socketConnectedSaga({ isAnonymous, socket, domainKey }) {
  console.info('socketConnectedSaga',{ isAnonymous, socket, domainKey });
}

/** Should an error occur from the phoenix socket, this action will be dispatched
 * @param {Object} params - parameters
 * @param {string} params.error
 * @param {string} params.socketState
 * @param {string} params.domainKey - domain for socket
 */
export function* socketErrorSaga({ error, socketState, domainKey }) {
  // here you should check if your token is valid or not expired, if not
  // disconnect phoenix
  console.error('socketErrorSaga',{ error, socketState, domainKey });
}

/**
 * Should the socket attempt to close, this action is dispatched to the
 * phoenix reducer
 * @param {Object} params - parameters
 * @param {Object} params.socket = socket being closed
 * @param {string} params.domainKey - domain for socket
 * @param {object} params.params - socket.params()
 */
export function* socketCloseSaga({ params, socket, domainKey }) {
  console.info('socketCloseSaga',{ params, socket, domainKey });
}
/**
 * After  phoenix socket disconnects
 * @param {Object} params - parameters
 * @param {Object} params.socket = socket being disconnected
 * @param {string} params.domainKey - domain for socket
 * @param {Object} params.params - socket.params()
 */
export function* socketDisconnectionSaga({ params, socket, domainKey }) {
  console.info('socketDisconnectionSaga',{ params, socket, domainKey });
}

export default function* defaultSaga() {
yield takeEvery(
    socketActionTypes.SOCKET_DISCONNECT,
    socketDisconnectionSaga
  );
  yield takeEvery(socketActionTypes.SOCKET_OPEN, socketConnectedSaga);
  yield takeEvery(socketActionTypes.SOCKET_ERROR, socketErrorSaga);
  yield takeEvery(socketActionTypes.SOCKET_CLOSE, socketCloseSaga);
}

Responding to Channel Progress

import _ from 'lodash';
import produce from 'immer';
import {
  socketActionTypes,
  PHOENIX_CHANNEL_END_PROGRESS,
  PHOENIX_CHANNEL_LOADING_STATUS,
} from '@trixta/phoenix-to-redux';

export const initialState = {
  loading: false,
  loadingStatus: {},
}

const appReducer = (state = initialState, action) =>
  produce(state, (draft) => {
    switch (action.type) {
      case socketActionTypes.SOCKET_ERROR:
        // when a socket error has occured
        break;
      case socketActionTypes.SOCKET_CONNECT:
        // when the socket has connected
        break;
      case PHOENIX_CHANNEL_END_PROGRESS:
        // when the progress for loadingStatusKey for channel has completed
        {
          const loadingStatusKey = action.data.loadingStatusKey
          if (!loadingStatusKey) {
            draft.loading = false;
          } else {
            delete draft.loadingStatus[loadingStatusKey];
          }
        }
        break;
      case PHOENIX_CHANNEL_LOADING_STATUS:
        // when the progress for loadingStatusKey is being updated
        draft.loadingStatus[action.data.loadingStatusKey] = {
          status: true,
        };
        break;
          });
export default appReducer;

Communicate with Phoenix

To communicate with phoenix socket, you can make use of the following dispatch Methods