0

I have an express server which needs to fetch some data from multiple external sources for each request. This logic is seperated into multiple routers (some are not managed by me).

These routers are completely independent, so there is no need for one to wait on the other.


As an example I have the following code:

const router1 = express.Router();
const router2 = express.Router();
const router3 = express.Router();
const finalRouter = express.Router();

router1.use((req, res, next) => setTimeout(next, 2000));
router2.use((req, res, next) => setTimeout(next, 2000));
router3.use((req, res, next) => setTimeout(next, 2000));

finalRouter.use((req, res, next) => console.log('All done!'));

When I would normally use all these routers in my application, it will execute sequentially and print All done! in 6 seconds.

But to improve the speed of my page I want to execute them in parallel, so they are all finished in 2 seconds. How can I do this?

Duncan Lukkenaer
  • 12,050
  • 13
  • 64
  • 97

2 Answers2

2

Individual routes definitely aren't needed here, you could just have a single route and use Promise.all to wait on all the requests. If they don't naturally support Promises, you could just wrap them in one e.g.

router.use(async (req, res, next) => {
    await Promise.all([
       new Promise((resolve, reject) => {
           setTimeout(() => resolve(), 2000);
       }),
       ...
    ]);
    next();
});
router.use((req, res, next) => {
    console.log('All done!');
});

As per the discussion in the comments, if you don't have the ability to merge the routes into one - that's fine, and doesn't really matter. Thinking more about this, all you would really need is a wrapper to capture the "real" middleware, add it to a queue, and then move onto to the next. For this to work, you would at least need access to the code that set's up the routes (which it appears to have) e.g.

queue.js

module.exports = route => {
    return (req, res, next) => {
        // ensure we've got a queue
        if (!(req.queue instanceof Array)) {
            req.queue = [];
        }
        // queue the blocking route
        req.queue.push(new Promise((resolve, reject) => {
            // run real route, signal success or failure when done
            route(req, res, err => err ? reject(err) : resolve()); 
        });
        next(); // move on, don't wait
    };
}

Then in your routes

const queue = require('./queue');
...
router.use(queue((req, res, next) => setTimeout(() => next(), 2000))));
router.use(queue((req, res, next) => setTimeout(() => next(), 2000))));
router.use(queue((req, res, next) => setTimeout(() => next(), 2000))));
router.use(async (req, res, next) => {
    await Promise.all(req.queue);
    console.log('All done!');
}
James
  • 80,725
  • 18
  • 167
  • 237
  • Thanks for the answer. That's what I was thinking, but as I mentioned in the question, some routers are not managed by me. So I'm hoping to find a way to do this using the existing routers. – Duncan Lukkenaer Aug 21 '17 at 14:33
  • @DuncanLuk sorry can only go on the example given, do you have _any_ sort of control over the routes? Quite tricky to do anything with 3rd party code without knowing the intricacies. – James Aug 21 '17 at 14:36
  • It's not 3rd party, but rather legacy code. It uses and alters the request and response object, so that's why it's a router in the first place. It's also used in multiple places, so I'd prefer not having to refactor everything if possible. – Duncan Lukkenaer Aug 21 '17 at 14:42
  • If it's not 3rd party then you do have access to the code? Unfortunately, I can't see how you can fix this without making _some_ changes to the existing routes as it's all dependant on how they interact with `next`. – James Aug 21 '17 at 14:45
  • I have some access to the code. Can you explain what you're thinking? – Duncan Lukkenaer Aug 21 '17 at 14:51
  • 2
    @DuncanLuk I'll update my answer, Jonas not sure I'd be happy for code like that going out to production - looks like it could become maintenance problem real fast. – James Aug 21 '17 at 14:58
  • Seems like a perfect solution. I'll try it out, thanks a lot! – Duncan Lukkenaer Aug 21 '17 at 15:22
  • @DuncanLuk code is untested, it's really just a braindump - but I'm confident this should do what you need, let me know how you get on. – James Aug 21 '17 at 15:31
  • I [worked it out in TypeScript](https://gist.github.com/d-luk/1c00fa1fbb5a3728a3d7d80a3eba6598) and it works perfectly. Thanks again! – Duncan Lukkenaer Aug 21 '17 at 17:21
0

You could do fake requests to all subrouters ( by abusing the internal handle method as found out by this answer):

const mainroute = Express.Router();

mainroute.get("/",function(req,res,next){     
 Promise.all([
  new Promise(resolve => 
    router1.handle(req,res, resolve)),
  new Promise(resolve => 
    router2.handle(req,res,resolve))
  ]).then(_=>res.end("all done"));
});

that could also be achieved manually by passing a promise array with the request, then you can await them all at the end:

const router1 = express.Router();
const router2 = express.Router();

const timer = ms => new Promise( res => setTimeout(res,ms));

router1.use((req, res, next) =>{
  req.promises = [timer(2000)];
});      
router1.use( req => req.promises[0] = req.promises[0].then(_=>"first done"));

router2.use((req, res, next)=>req.promises.push(timer(2000)));

router1.use("/test",router2);
router1.use((req,res)=> Promise.all(req.promises).then(_=>res.end("all done")));
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • Looks like this could work, but I'm afraid that this will break my code some day as this "hack" is most likely not supported by the framework. – Duncan Lukkenaer Aug 21 '17 at 15:04