Hurry
Maple
Lad

CommonJS Modules Make Brittle Singletons

Crossposted from bites.goodeggs.com

We occasionally rely on node's module caching to share a single instance throughout a full-stack javascript project. This strategy breaks more than we'd like.

If modules butternut and delicata both require('squash'), they'll usually get the same (think ===) squash instance. But not always.

Here are a couple times it hasn't worked out.

Olive & Sinclair Chocolate Co
Bourbon Nib Brittle

Let's say we're really into node-fibers with its concise coroutines and error handling. We're starting a new module, tests first, so we npm install mocha mocha-fibers and write some failing tests. Next we npm install fibrous to help implement our module. If we list installed fibers with npm ls fibers we get:

├─┬ fibrous@0.3.3
│ └── fibers@1.0.4
└─┬ mocha-fibers@1.1.0
  └── fibers@1.0.4

Uh oh, we've got two! This can cause some pretty weird behavior. Luckily, fibers was patched in 1.0.4 to mitigate this particular problem using global variables. If we care which version of fibers our project uses, it's best to install it explicitly as a top level dependency, before installing fibrous or node-fibers. For example, npm install fibers fibrous mocha-fibers yields:

├── fibers@1.0.5
├── fibrous@0.3.3
└── mocha-fibers@1.1.0

Only one fibers, much better.

Recap: multiple modules with a shared dependency can get different instances of that dependency. Nothing super new there, there's even a big caveat in the docs about it.

Several folks have noticed that growing node apps often develop uncomfortably long require paths like ../../../../widely_shared_code. At Good Eggs, we encountered these paths requiring the json manifest of versioned assets generated by grunt-assets-versioning. They require intense concentration to type accurately, and they sure don't help when we're moving files around.

Symlinks are a recommended workaround for these long requires. If we ln -s ../assets.json node_modules/assets.json we can just require('assets.json') throughout our project, no dots required! We can still use relative paths if we're in a file pretty close to the assets, perhaps in the same directory. require('./assets.json') isn't so bad, right?

Let's audit our client side bundle for duplicates with browserify <entrypoint>.js --list | grep assets.json . Depending on our version of Browserify we may or may not get them. Browserify can't seem to make up its mind if symlinks paths resolve to the same instance, but the authors are clear about using them for singletons:

Keep in mind that singletons are not guaranteed by either module loader (be it node or browserify). A lot of times you do get the same instance due to caching, but you shouldn't rely on that in order to enforce singleton semantics since it breaks in lots of cases.

Browserified duplicates have caused problems ranging from bloated bundle sizes to client-side app crashes due to missing configuration in one of the duplicate instances. We can avoid browserify duplication and still use symlinks for short require paths if we avoid relative requires to symlinked modules.

This is complicated

The module cache sure does not make a good service locator. I wonder what other patterns folks are using for distributing singleton instances throughout apps, especially dependencies shared between the browser and the server. Dependency injection comes to mind, but it often entails a complicated system of its own. Do you have a singleton strategy that's working well?