kriasoft / react-starter-kit

The web's most popular Jamstack front-end template (boilerplate) for building web applications with React
https://reactstarter.com
MIT License
22.77k stars 4.16k forks source link

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

Closed alvaroqt closed 3 years ago

alvaroqt commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

alvaroqt commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

langpavel commented 6 years ago

@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 :slightly_frowning_face: 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 commented 6 years ago

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

langpavel commented 6 years ago

@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 :slightly_smiling_face:

tim-soft commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 5 years ago

@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 commented 3 years ago

@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.