简体   繁体   中英

Inject functionality into other Node.js module

I have a Node.js application (using sequelize as orm) which i try to separate into different modules which can easily be reused. This means I need to remove some dependencies between them, but I'm struggling at one point:

There's a module called "Account", which should have (almost) zero dependencies on other modules. It just provides basic account functionality.

Then there are other modules, which have a dependency on the account module (which is ok).

At the moment, the Account module has a dependency on the other modules, which I'd like to resolve. Now, when an account gets created, other modules also must create some objects in the database. This should happen in the same transaction and without the account module knowing something about the other modules.

Currently, this looks like the following:

AccountController.prototype.createAccount = function (data) {
   // validation checks etc. omited

   return db.sequelize.transaction(function (t) {
      return Q.Promise(function (resolve, reject, notify) {
         _createHash(pw, 10)
            .then(function (hash) {
               data.passwordHash = hash;

               return _createAccount(data, t);
            })
            .then(function (account) {
               // create user
               return [account, _createUser(account, t)];
            })
            .spread(function (account, user) {
               // create another object
               return [account, user, _createXXX(account, t)]
            })
            .spread(function(account, user, xxx) {
                // create 1 more object
               return [account, user, xxx, _createYYY(account, t)];
            })
            .spread(function (account, user, xxx, yyy) {
               resolve([account, user, xxx, yyy]);
            })
            .catch(reject);
      });

   });
};

Now, I only want to create the Account object within this module, and let the other modules create their objects independent, but in the same transaction.

First I thought about let the AccountController emit an "createAccount" Event within the promise chain, hand over the transaction object and let the modules register some listeners. But then I noticed that EventEmitter works asynchronously.

What's the best practice in Node.js to do something like this?

Ok let me answer the question myself:

  • Despite I read something different, I realized EventEmitter is NOT asynchronously
  • So using events for this task basically works
  • But I encountered the problem, that my event listeners where asynchronously, due to database calls. Therefore, I needed a way to wait until all event listeners have "really" finished.
  • I introduced a promisedEmit() function to a subclass of EventEmitter, which allows a listener to return a promise and returns a promise itself, which will only finish when all listener promises finished.

Here's the code I came up with:

'use strict';

var EventEmitter = require('events');
var util = require('util');
var Q = require('q');

function PromisedEventEmitter() {
}

util.inherits(PromisedEventEmitter, EventEmitter);

PromisedEventEmitter.prototype.promisedEmit = function promisedEmit(type) {
   var er, handler, len, args, i, listeners;
   var promises = [];

   if (!this._events)
      this._events = {};

   // If there is no 'error' event listener then throw.
   if (type === 'error' && !this._events.error) {
      er = arguments[1];
      if (this.domain) {
         if (!er)
            er = new Error('Uncaught, unspecified "error" event.');
         er.domainEmitter = this;
         er.domain = this.domain;
         er.domainThrown = false;
         this.domain.emit('error', er);
      } else if (er instanceof Error) {
         throw er; // Unhandled 'error' event
      } else {
         throw Error('Uncaught, unspecified "error" event.');
      }
      return Q.all(promises);
   }

   handler = this._events[type];

   if (util.isUndefined(handler))
      return Q.all(promises);

   if (this.domain && this !== process)
      this.domain.enter();

   var promise;
   if (util.isFunction(handler)) {
      switch (arguments.length) {
         // fast cases
         case 1:
            promise = handler.call(this);
            break;
         case 2:
            promise = handler.call(this, arguments[1]);
            break;
         case 3:
            promise = handler.call(this, arguments[1], arguments[2]);
            break;
         // slower
         default:
            len = arguments.length;
            args = new Array(len - 1);
            for (i = 1; i < len; i++)
               args[i - 1] = arguments[i];
            promise = handler.apply(this, args);
      }
      promises.push(promise);
   } else if (util.isObject(handler)) {
      len = arguments.length;
      args = new Array(len - 1);
      for (i = 1; i < len; i++)
         args[i - 1] = arguments[i];

      listeners = handler.slice();
      len = listeners.length;
      for (i = 0; i < len; i++) {
         promise = listeners[i].apply(this, args);
         promises.push(promise);
      }
   }

   if (this.domain && this !== process)
      this.domain.exit();

   return Q.all(promises);
};


module.exports = PromisedEventEmitter;

This is basically just the original .emit code, I only added some promise handling to it. I now can use this the same way as the original EventEmitter, and just call

XXXController
    .promisedEmit("eventName", arg1, arg2, argN)]
    .then(function(){
        //...
    });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM