question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Websockets with new Isomorphic Hot Module Replacement

See original GitHub issue

Hello,

I’m looking for a way to integrate websockets into an app with current version of HMR, after changes being made to the start script.

In previous version I could easily access http server to attach WS capability (exactly Apollo Subscriptions server for GraphQL Subscriptions)

// server.js
//...
import { createServer } from 'http';
import { SubscriptionServer } from 'subscriptions-transport-ws';

// ...
const server = createServer(app);
server.listen(config.port, () => {
    console.info(`The server is running at http://localhost:${config.port}/`);
    SubscriptionServer.create({
      subscriptionManager,
    }, {
      server,
      path: config.subscriptions.path,
    });
  });

With current way the HMR is done Browsersync’s server is used. I was thinking to attach own websocket server directly to Browsersync’s by changing a little start script like:

// start.js
// ...
// Launch the development server with Browsersync and HMR
const b = await new Promise((resolve, reject) => browserSync.create().init({
    // https://www.browsersync.io/docs/options
    server: 'src/server.js',
    middleware: [server],
    open: !process.argv.includes('--silent'),
    ...isDebug ? {} : { notify: false, ui: false },
}, (error, bs) => (error ? reject(error) : resolve(bs))));

SubscriptionServer.create({
      subscriptionManager,
}, {
      b.server,
      path: config.subscriptions.path,
});

this enables GraphQL subscriptions but sadly breaks socket.io in Browsersync, so no HMR then (probably overrides socket.io from BS server).

I’m looking for a hint how could it be implemented with current setup, without Browsersync’s proxy. Is it at all possible to be done this way?

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:17 (2 by maintainers)

github_iconTop GitHub Comments

2reactions
pdeszynskicommented, Feb 14, 2018

@ricardomoura I don’t know if this will be still useful for you, but I had the same problem after reloads as it was not always calling dispose from hot reload (depending which file I was exactly editing). Sadly I still didn’t have luck to make subscriptions working on the same port as the server (maybe this could help solve this problem https://github.com/websockets/ws/pull/885), but for now I’ve solved EADDRESS, by closing previous subscription server not using webpack’s hot module accept/dispose but inside of start.js script. Code looks like:

// server.js
import createSubscriptionsServer from './core/createSubscriptionsServer';
import { createServer } from 'http';
// other RSK imports

//...

//
// Launch the server
// -----------------------------------------------------------------------------
if (!module.hot) {
  const server = createServer(app);
  createSubscriptionsServer({ server });
  server.listen(config.port, () => { // on production it will be working under the same port as main app
    console.info(`The server is running at http://localhost:${config.port}/`);
  });
}

//
// Hot Module Replacement
// -----------------------------------------------------------------------------
if (module.hot) {
  app.hot = module.hot;
  const server = createServer();
  createSubscriptionsServer({ server });
  server.listen(config.subscriptionsPort, () => {
    console.info(
      `The subscription server is running at http://localhost:${
        config.subscriptionsPort
      }`,
    );
  });

  app.subsciptionsServer = server;
  module.hot.accept('./router');
}

export default app;
// createSubscriptionsServer
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import config from '../config';
import schema from '../data/schema';

export default function createSubscriptionServer(socketOpts = {}) {
  return SubscriptionServer.create(
    {
      schema,
      execute,
      subscribe,
      async onConnect(connectionParams, webSocket) {
       // stuff to create a context for subscriptions like logged in user
        return context;
      },
    },
    {
      ...socketOpts,
      path: '/subscriptions',
    },
  );
}

And the last part is an update in tools/start.js to close previously opened websocket server:

// start.js
//...

function checkForUpdate(fromUpdate) {
  const hmrPrefix = '[\x1b[35mHMR\x1b[0m] ';
 // ...

return app.hot
  .check(true)
  //...
  .catch(error => {
        if (['abort', 'fail'].includes(app.hot.status())) {
          console.warn(`${hmrPrefix}Cannot apply update.`);
          app.subsciptionsServer.close(); // close the previous subscription server, getting here it means that server side code needs to be reloaded
         // ... rest of function body
  });
}

It’s important that on production it will be working under the same port as your app, so for e.g. if you have set a port 3000 then it will also listen for subscriptions under the port 3000. Locally you will have to setup port inside of a config and use other one for subscriptions.

It would be great if somebody could share a better solution.

1reaction
tim-softcommented, Sep 15, 2017

@Asthor I solved this problem by giving the socket server it’s own port and then managing it’s creation/destruction via HMR’s dispose/accept functions like what @lobnico proposed.

Doing it this way accomplishes two things:

  • Browser-Sync and your own socket server can coexist
  • The socket server won’t break HMR/crash the app when it goes to reload

Assuming you have the latest version of RSK (after start.js got redone)

Add a value for your socket port in /src/config.js (I call it subPort)

module.exports = {
  // Node.js app
  port: process.env.PORT || 3000,

  // GraphQL subscriptions websocket port
  subPort: process.env.SUB_PORT || 4040,

{Stuff...}

Create /src/core/server/subscriptions.js

import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import schema from '../../data/schema';
import config from '../../config';

let subscriptionServer;

const addSubscriptions = httpServer => {
  subscriptionServer = SubscriptionServer.create({
    execute,
    subscribe,
      schema,
      onConnect: (connectionParams, webSocket) => {
        return ({ connectionParams,  })
      },
    },
    {
      server: httpServer,
      path: '/subscriptions'
    },
  );
};

const addGraphQLSubscriptions = httpServer => {
  if (module.hot && module.hot.data) {
    const prevServer = module.hot.data.subscriptionServer;
    if (prevServer && prevServer.wsServer) {
      console.log('Reloading the subscription server.');
      prevServer.wsServer.close(() => {
        addSubscriptions(httpServer);
      });
    }
  } else {
    addSubscriptions(httpServer);
  }
};

if (module.hot) {
  module.hot.dispose(data => {
    try {
      data.subscriptionServer = subscriptionServer;
    } catch (error) {
      console.log(error.stack);
    }
  });
}

export default addGraphQLSubscriptions;

Then modify /src/server.js

import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { createServer } from 'http';
import addGraphQLSubscriptions from './core/server/subscriptions';
import config from './config';

{Stuff...}

// Add in your graphql endpoint
app.use('/graphql', bodyParser.json(), graphqlMiddleware);

// Enable graphiql with subscriptions
app.use('/graphiql', graphiqlExpress({
  endpointURL: '/graphql',
  subscriptionsEndpoint: `ws://supercoolsite.com:${config.subPort}/subscriptions`
}));

// Create websocket server
const websocketServer = createServer((_request, response) => {
  response.writeHead(404);
  response.end();
});

// Try to spin up the websocket server with diff port
websocketServer.listen(config.subPort, () => {
  console.log(`Websocket server listening on port ${config.subPort}`);

  addGraphQLSubscriptions(websocketServer);
});

//
// Launch the server
// -----------------------------------------------------------------------------
const promise = models.sync(
  //{ force: true },
).catch(err => console.error(err.stack));

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

//
// Hot Module Replacement
// -----------------------------------------------------------------------------
if (module.hot) {
  app.hot = module.hot;
  module.hot.dispose(() => {
    try {
      if (websocketServer) {
        websocketServer.close();
      }
    } catch (error) {
      console.error(error.stack);
    }
  });

  module.hot.accept(['./core/server/subscriptions', './router'], () => {
    try {
      addGraphQLSubscriptions(websocketServer);
      console.log("attached addGraphQLSubscriptions to module.hot")
    } catch (error) {
      console.error(error.stack);
    }
  });
}

export default app;

You can verify that it’s working by going to graphiql and looking through the dev console. If you don’t see some variation of handshake failed, then it’s probably working. In chrome, if you go in dev console -> network and find the subscription request, look in the frames tab to further verify your subscriptions are working.

capture

This one was a bit of a headscratcher, there are probably better ways to solve this problem but here’s where I said “It’s good enough”. Many thanks to @lobnico for getting me 80% of the way to working subscriptions 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Isomorphic Implementation of React | by Yudhajit Adhikary
Isomorphic process is a process of rendering web-application on browser in ... Hot Module Replacement is the process by application gets updated without ......
Read more >
Differences between socket.io and websockets - Stack Overflow
I wrote an npm module to demonstrate the difference between WebSocket and Socket.IO: ... appendChild(i); } log('opening websocket connection'); var s = new...
Read more >
react-isomorphic-render - npm
Start using react-isomorphic-render in your project by running `npm i ... Supports Webpack "hot reload" (aka "Hot Module Replacement") ...
Read more >
Webpack's Hot Module Replacement Feature Explained
The Webpack compiler will build your application with the changes, creating a new manifest file and comparing it with the old one. This...
Read more >
The future of web software is HTML over WebSockets
It seems like you could get most, if not all the way, there with an "isomorphic" Redux store on client and server side,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found