Skip to content

Instantly share code, notes, and snippets.

@jasisk
Last active August 29, 2015 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasisk/0df7fa8f21ce6bc4fa30 to your computer and use it in GitHub Desktop.
Save jasisk/0df7fa8f21ce6bc4fa30 to your computer and use it in GitHub Desktop.
Follow-up with Doug.

Hi Doug,

This is Jean-Charles from PayPal (jasisk).

Just following up on #2732, itself related to #2633. Really appreciate your time on this. This is a long one so grab a sandwich or something. 😀

So as I described in the express issue, we use the ephemeral app pattern (my term) all over the place. The pattern is quite simple:

  1. In our module, we create and export a new app instance.
  2. In the user’s app, they can mount the returned app instance from step 1 just like any other middleware / app.
  3. In our module, we listen for mount. On mount we: grab the parent app instance, grab the mount path, and pop the app instance off the router stack.

The benefit here is our module now has a mountpath specified by the user and the parent app instance, while the user gets to use the familiar middleware registration pattern (app.use(module())). You can see this pattern in use in pretty much all of our stuff (e.g., kraken-js, meddleware, swaggerize-express, express-enrouten, to name a few).

These problems come from meddleware. A quick intro on meddleware: it takes in a config, and then registers middleware. The short of it is that it enables configuration driven middleware registration. Useful when a thousand different departments write interdependent middleware since you can define arbitrary lifecycle ranges (e.g., auth should occur during priorities 60-70).

The first issue I wanted to attack is the one that 2633 was the result of: mountpath normalization. From the ephemeral app pattern, we get the mountpath the user supplied when adding our module (e.g., app.use('/this/path', kraken())).

The meddleware config can take a route property. In the simple case, this is essentially what it looks like:

config = {
  "auth": {
    "route": "/my-cool-path",
    "module": "cool-custom-auth"
  }
};

app.use(meddleware(config));

Which results in something like:

app.use('/my-cool-path', require('cool-custom-auth')());

Super simple.

The problem occurs with a slightly more complicated example:

config = {
  "auth": {
    "route": "/my-cool-path",
    "module": "cool-custom-auth"
  }
};

app.use('/secure', meddleware(config)); // addition of a mount path

… which would result in …

// mountpath is concatted with route property:
app.use('/secure/my-cool-path', require('cool-custom-auth')());

This works for strings, obviously, but breaks down if either mountpath or route are regexes or arrays containing regexes. You can see our naive approach implemented in the v3.x branch of meddleware.

Failed solution attempt

My solution to this problem was to create a Router, mount it to the parent app at mountpath, and mount all the configured middleware on the router instance. This is implemented in the v4.x branch of meddleware. This solves the complex case described above:

config = {
  "auth": {
    "route": ["/my-cool-path", /.*secure.*/],
    "module": "cool-custom-auth"
  }
};

app.use('/new-path', meddleware(config));

… becomes …

var router = new express.Router();
router.use(["/my-cool-path", /.*secure.*/], require('cool-custom-auth')());
app.use('/new-path', router);

Great. Except one thing. We've broken this case:

config = {
  "api": {
    "route": "/api",
    "module": "swaggerize-express" // uses the ephemeral app pattern
  }
};

app.use('/my-app', meddleware(config));

… becomes …

var router = new express.Router();
router.use('/api', require('swaggerize-express')());
app.use('/my-app', router);

However /my-app won't have anything on it because swaggerize-express uses the ephemeral app pattern which assumes a mount event. Since it's now being mounted on a Router instead of an app, there is no mount event so it never initializes.

Meddleware-level solution

I can think of a way to solve this from meddleware with something like this (replacing the mounting line in meddleware):

fn = wrapFn(fn);
fn.parent = router;
router.use(spec.route || '/', fn);
fn.emit('mount', router);

While wrapFn is a simple event emitter that replaces itself with fn when it sees mount, decides if it needs to refire on fn based on some hueristic (listener count plus is app instance, perhaps).

So after that epic—any other ideas? 😀

Thanks again, Doug!

@jasisk
Copy link
Author

jasisk commented Aug 12, 2015

So I've come up with a hack that seems to work for the most part but it feels 🔥 🚒 🔥 terrible:
https://github.com/krakenjs/meddleware/blob/66daa25d687aa1ed0ad55c3e6e03ebf2e817b31c/index.js#L168-L197

@dougwilson
Copy link

Ah, I see. I think I'm starting to understand what you're trying to accomplish :) Let me think on this a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment