Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't find the way to integrate socket.io and redux-socket.io #1482

Closed
alvaroqt opened this issue Dec 21, 2017 · 17 comments
Closed

Can't find the way to integrate socket.io and redux-socket.io #1482

alvaroqt opened this issue Dec 21, 2017 · 17 comments

Comments

@alvaroqt
Copy link

Hello, I can't find the way to establish a communication with a socket.io port... it works on the build but i have some kind of conflict with Browser-sync, i tried different configurations, but it doesn't establish the connection. In some cases i receive a 404 error page from express.

Here is the code, please help me on how to revolve this issue or how te remove or deactivate browser-sync, is not critic for my project. But when i tried to remove the dev version stop to works, I don't know how to replace Browser-sync, to keep the dev environment with HMR operational.

Please I'll appreciate any orientation, I've invested a lot of time in this issue and it surpasses me.

CLIENT SIDE
// /src/store/configureStore.js

import createSocketIoMiddleware from 'redux-socket.io';
import { createSocket, registerListenersSocket } from '../lib/functions';

...

let socketIoMiddleware, socket;
if (process.env.BROWSER) {
  socket = createSocket();
  console.log('socket', socket);
  const socketDebug = (action, emit, next, dispatch ) => {
    console.log('socket middleware: ', { action, emit, next, dispatch });
    next();
  };
  socketIoMiddleware = createSocketIoMiddleware(socket, 'server/', socketDebug);
}

export default function configureStore(initialState, helpersConfig) {
...
  const middleware = [];
  const sagaMiddleware = createSagaMiddleware();
  middleware.push(sagaMiddleware);

  if (process.env.BROWSER) {
    console.log('adding socket middleware');
    middleware.push(socketIoMiddleware);
  }


  let enhancer;
  if (__DEV__) {
    middleware.push(createLogger());
    // https://github.com/zalmoxisus/redux-devtools-extension#redux-devtools-extension
    let devToolsExtension = f => f;
    if (process.env.BROWSER && window.devToolsExtension) {
      devToolsExtension = window.devToolsExtension();
    }

    enhancer = compose(applyMiddleware(...middleware), devToolsExtension);
  } else {
    enhancer = applyMiddleware(...middleware);
  }

  // See https://github.com/rackt/redux/releases/tag/v3.1.0
  const store = createStore(rootReducer, initialState, enhancer);
  if (process.env.BROWSER) {
    registerListenersSocket(socket, store);
  }
  // Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
  if (__DEV__ && module.hot) {
    module.hot.accept('../reducers', () =>
      // eslint-disable-next-line global-require
      store.replaceReducer(require('../reducers').default),
    );
  }
  store.runSaga = sagaMiddleware.run;
  store.close = () => store.dispatch(END);

  return store;
}

/src/lib/functions.js

import io from 'socket.io-client';

import {
  socketConnect,
  socketReconnect,
  socketDisconnect,
  socketConnectionError,
} from '../actions/server';

export const calcProgress = (dateStr, limit) => {
  const date = new Date(dateStr);
  const now = new Date(Date.now());
  const days = Math.floor((Date.now() - date) / (3600000 * 24));
  return {
    days,
    percent: Math.round(Math.min(100, days / limit * 100)),
  };
};

export function createSocket() {

  // todo: access configuration by conf.
  const serverURL = process.env.SOCKET_SERVER;
  console.log('connecting socket on: ');
  return io();
}

export function registerListenersSocket(socket, store) {
  socket.on('connect', () => store.dispatch(socketConnect()));
  socket.on('connect_timeout', () => store.dispatch(socketConnectionError()));
  socket.on('connect_error', () =>
    store.dispatch(
      socketConnectionError(new Error('Error al conectar el socket.')),
    ),
  );
  socket.on('reconnect_error', () =>
    store.dispatch(
      socketConnectionError(new Error('Error al reconectar el socket.')),
    ),
  );
  socket.on('disconnect', () => store.dispatch(socketDisconnect()));
  socket.on('reconnect', () => store.dispatch(socketReconnect()));
}

image

SERVER SIDE
/src/server.js

...
import socket from './lib/socket';

const app = express();
app.server = http.createServer(app);
socket(app, process);
...

/src/lib/socket

import SocketIO from 'socket.io';
...
export default function socketConnection (app, process) {
...

  /** SOCKET CONNECTION */
  console.log('** socket connection');
  const { server } = app;
  const sockets = {};
  const io = new SocketIO(server);
  io.on('connection', socket => {
    console.log('socket connected'); /** <-- I never get this **/
    sockets[socket.id] = socket; // socket table.
    console.log('sending hello acknowledge');
    socket.emit('action', {type: 'server/HELLO', payload: {message: 'socket connected', code: 1} });

    socket.on('action', (action) => {
      console.log('socket action:', action);
      switch (action.type) {
        case 'server/CALL': {
          const {source, target, leadId, agentId} = action.payload;
          console.log('call');
          console.log('source', source);
          console.log('target', target);
          crmami.call(source, target, {
            socketId: socket.id,
            leadId,
            agentId
          },
            (error) => {
              socket.emit('action', {
                type: 'server/CALL_FAIL',
                payload: { error }
              })
            });
        }
      }
    });


    socket.on('disconnect', () => {
      delete sockets[socket.id];
    });

  });

  process.on('exit', () => {
    crmami.disconnect();
    crmami.destroy();
  })
}

/tools/start.js

...
  await new Promise((resolve, reject) =>
    browserSync.create().init(
      {
        socket: {
          path: '/bs/socket.io',
          clientPath: '/browser-sync',
          namespace: '/bs',
          domain: undefined,
          port: undefined,
          'clients.heartbeatTimeout': 5000
        },
  //       // socket: {
  //       //   namespace: `http://localhost:3000/bs`
  //       // },
        server: 'src/server.js',
        port: config.port,
        middleware: [server],
        open: !process.argv.includes('--silent'),
        ...(isDebug ? {} : { notify: false, ui: false }),
      },
      (error, bs) => (error ? reject(error) : resolve(server)),
    )
  );
@alvaroqt
Copy link
Author

I've created a new server with a new port and it works.... I'm not sure if it's the best way, but it works, any comment will be appreciated.

CLIENT SIDE
/src/lib/functions

export function createSocket() {
  const serverURL = `http://localhost:4000`;
  console.log('connecting socket on: ', serverURL);
  return io(serverURL);
}

SERVER SIDE
/src/lib/socket

  /** SOCKET CONNECTION */
  const server = http.createServer();
  server.listen(4000);
  const sockets = {};
  const io = new SocketIO(server);
  io.on('connection', socket => {
    sockets[socket.id] = socket; // socket table.
    socket.emit('action', {type: 'server/HELLO', payload: {message: 'socket connected', code: 1} });
    socket.on('action', (action) => {
      switch (action.type) {
        case 'server/CALL': {
          const {source, target, leadId, agentId} = action.payload;
          crmami.call(source, target, {
            socketId: socket.id,
            leadId,
            agentId
          },
            (error) => {
              socket.emit('action', {
                type: 'server/CALL_FAIL',
                payload: { error }
              })
            });
        }
      }
    });

@tim-soft
Copy link

That's more or less how you'd need to do it in development as you obviously can't occupy browser sync's websocket port.

@leviwheatcroft
Copy link

leviwheatcroft commented Dec 24, 2017

I've been playing around with this..

The biggest problem I can see is dealing with auth. you could use something like socket-jwt-auth but that relies on passing the jwt as a query string. Alternatively you can implement some sort of authentication exchange after a socket connects like this

I also had trouble with the server reloading, like at first glance I can't see how react-starter-kit decides what to kill when you save a file, so on every reload my src/server tries to spawn another socket server but collides with the previous one still listening on that port.

@alvaroqt
Copy link
Author

@leviwheatcroft is right, my solution fails on reload, please HELP.

@alvaroqt
Copy link
Author

Hello I made a little change on start and It seams to work... can anybody verify if this is OK? ... I did it from my ignorance...

/tools/start.js

  function checkForUpdate(fromUpdate) {
    const hmrPrefix = '[\x1b[35mHMR\x1b[0m] ';
    if (!app.hot) {
      throw new Error(`${hmrPrefix}Hot Module Replacement is disabled.`);
    }
    if (app.hot.status() !== 'idle') {
      return Promise.resolve();
    }
    return app.hot
      .check(true)
      .then(updatedModules => {
        if (!updatedModules) {
          if (fromUpdate) {
            console.info(`${hmrPrefix}Update applied.`);
          }
          return;
        }
        if (updatedModules.length === 0) {
          console.info(`${hmrPrefix}Nothing hot updated.`);
        } else {
          console.info(`${hmrPrefix}Updated modules:`);
          updatedModules.forEach(moduleId =>
            console.info(`${hmrPrefix} - ${moduleId}`),
          );
          checkForUpdate(true);
        }
      })
      .catch(error => {
        if (['abort', 'fail'].includes(app.hot.status())) {
          console.warn(`${hmrPrefix}Cannot apply update.`);
          delete require.cache[require.resolve('../build/server')];
          // eslint-disable-next-line global-require, import/no-unresolved
          app.server.removeListener('./router');                  /* <--- this is the line to FIX de problem */
          app = require('../build/server').default;
          console.warn(`${hmrPrefix}App has been reloaded.`);
        } else {
          console.warn(
            `${hmrPrefix}Update failed: ${error.stack || error.message}`,
          );
        }
      });
  }

@leviwheatcroft
Copy link

@alvaroqt in case you missed it, or for anyone else looking at this, the apollo branch recently updated from apollo-client 1.x to 2.x. I haven't really dug into it but I'm pretty sure this provides access to some fancy new features which might be a convenient substitute for socket.io functionality.

@tim-soft
Copy link

tim-soft commented Jan 9, 2018

Is this socket server intended for graphql subscriptions or something else?

@langpavel
Copy link
Collaborator

@leviwheatcroft Yes, I updated apollo branch, but it still uses plain old original GraphQL server and not introduces subscriptions nor websockets. It can be easy to do but I doubt that it can decay quickly, so I don't want to dig into in near future.. This is task for current—future—open—source—CMS which I can start, but nobody will give single $ for my living needs 🙁
Integration of real-time features can be blazingly easy in GraphQL, but sometimes you want simples working solution, so @alvaroqt look at faye-websocket

@tim-soft
Copy link

Are subscriptions/redis something you'd want the Apollo branch to have?

@langpavel
Copy link
Collaborator

@tim-soft Hmm.. I think that this is out of scope of Apollo branch, but can be in scope of feature/apollo-realtime branch which isn't exist yet 🙂

@tim-soft
Copy link

tim-soft commented Jan 11, 2018

Hypothetically speaking, I'd try to make a socket.js file under src, add a socket section in the webpack config and then mimic the server creation steps in /tools/start.js for the socket server. The socket should be it's own entity just like the current server and in a production build, be separable so you could slap it on it's own machine. Then adding in the ws apollo link is pretty self explanatory

@alvaroqt
Copy link
Author

My app uses socket.io to map events from a callcenter server and realtime live call events, untill now my solution is working OK, i don't understand if GraphQL or Apollo can help with this in any way. My app is connected with the callcenter server throught some kind of terminal connection using a node-express module, then receive and send events to the front end using socket.io.

@tim-soft
Copy link

tim-soft commented Jan 11, 2018

@alvaroqt Apollo implements the GraphQL spec including the experimental Subscriptions type. Unlike queries and mutations, subscriptions need a websocket server and some sort of pub-sub system like Redis in order to work.

In your scenario, you'd need to emit a subscription to a channel containing a payload object describing what has changed. You could think of this like sending a message to a group in Slack.
react-apollo lets you subscribe to a channel, allowing your react components to receive these messages(object payloads). When your UI receives a payload, you typically update the Apollo Cache, which will automagically update your UI that is using that data from the cache.

You could make it work but in order to use GraphQL subscriptions via Apollo, you'll need to incorporate redis and ws somewhere, they don't necessarily need to exist inside rsk, but they can.

@langpavel
Copy link
Collaborator

@tim-soft I don't think that GraphQL subscriptions will be most fit for @alvaroqt's use case — signaling VoIP or similar.. May be it can, but plain WebSocket + Redis can be simpler, more flexible and overall better fit I think..

@tim-soft
Copy link

tim-soft commented Jan 11, 2018

To each their own, I've drank the subscriptions kool-aid at this point. @alvaroqt Good luck!

edit: you can try it this way https://github.com/tgdn/react-starter-kit-subscription

@Shubh1692
Copy link

Shubh1692 commented Jan 28, 2019

@alvaroqt, Hey you can integrate Socket IO server for prod build by replacing this code in src/server.js

if (!module.hot) {
  promise.then(() => {
    app.listen(config.port, () => {
      console.info(`The server is running at http://localhost:${config.port}/`);
    });
  });
}

with this

if (!module.hot) {
  promise.then(() => {
      const mainServer = app.listen(config.port, () => {
        console.info(`The server is running at http://localhost:${config.port}/`);
      });
      const io = require('socket.io').listen(mainServer);
      io.on('connection', function(socket){
             socket.on('chat message', function(msg){
             io.emit('chat message', msg);
      });
   });
 });
} 

@ulani
Copy link
Member

ulani commented May 27, 2021

@alvaroqt thank you very much for crating this issue! Unfortunately, we have close it due to inactivity. Feel free to re-open it or join our Discord channel for discussion.

NOTE: The main branch has been updated with React Starter Kit v2, using JAM-style architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants