简体   繁体   中英

Modifying some features in a public npm package

I am trying to overload/replace functions in the ami-io npm package. That package is created to talk to asterisk AMI, a socket interface.

I need to talk to a service that has almost the exact same interface, but it presents a different greeting string upon login, and it requires an extra field in the logon. All the rest is the same. Instead of just plain copying the 600 LOC ami-io package, and modifying two or three lines, I want to override the function that detects the greeting string, and the login function, and keep using the ami-io package.

Inside the ami-io package there is a file index.js which contains the following function:

Client.prototype.auth = function (data) {
    this.logger.debug('First message:', data);
    if (data.match(/Asterisk Call Manager/)) {
        this._setVersion(data);
        this.socket.on('data', function (data) {
            this.splitMessages(data);
        }.bind(this));
        this.send(new Action.Login(this.config.login, this.config.password), function (error, response) {
            if (response && response.response === 'Success') this.emit('connected');
            else this.emit('incorrectLogin');
        }.bind(this));
    } else {
        this.emit('incorrectServer', data);
    }
};

Now I want to match not on Asterisk Call Manager , but on MyService , and I want to define and use Action.LoginExt(this.config.login, this.config.password) with another one with an extra parameter.

Is this possible? I tried this in my own module:

var AmiIo = require('ami-io');
var amiio = AmiIo.createClient({port:5038, host:'x.x.x.x', login:'system', password:'admin'});


amiio.prototype.auth = function (data) {
  this.logger.debug('First message:', data);
  if (data.match(/MyService Version/)) {
    this._setVersion(data);
    this.socket.on('data', function (data) {
      this.splitMessages(data);
    }.bind(this));
    this.send(new Action.LoginExt(this.config.login, this.config.password, this.config.extra), function (error, response) {
      if (response && response.response === 'Success') this.emit('connected');
      else this.emit('incorrectLogin');
    }.bind(this));
  } else {
    this.emit('incorrectServer', data);
  }
};

...but it resulted in TypeError: Cannot set property 'auth' of undefined , and now I am clueless. Also, can I define a new Action.LoginExt object in my own module? How?

The action.js module defines the Action objects as follows:

function Action(name) {
    Action.super_.bind(this)();
    this.id = this.getId();
    this.set('ActionID', this.id);
    this.set('Action', name);
}

(function(){
    var Message = require('./message.js');
    var util = require('util');
    util.inherits(Action, Message);
})();

Action.prototype.getId = (function() {
    var id = 0;
    return function() {
        return ++id;
    }
})();

function Login(username, secret) {
    Login.super_.bind(this, 'Login')();
    this.set('Username', username);
    this.set('Secret', secret );
}

... more functions ...

(function() {
    var actions = [
        Login,
        ... more functions ...
    ];
    var util = require('util');
    for (var i = 0; i < actions.length; i++) {
        util.inherits(actions[i], Action);
        exports[actions[i].name] = actions[i];
    }
    exports.Action = Action;
})();

What I think I understand is that Action is subclassed from Message. The Login function in its turn is subclassed from Action, and exported (in the last code block). So I think in my code I could try something similar:

// extend ami-io with LoginExt function
function LoginExt(username, secret, company) {
  Login.super_.bind(this, 'LoginExt')();
  this.set('Username', username);
  this.set('Secret', secret );
  this.set('Company', company);
}

var util = require('util');
util.inherits(LoginExt, amiio.Action);

But util.inherits fails with undefined. I've also opened a issue on ami-io.

You can use:

var AmiIo = require('ami-io');
AmiIo.Action.Login = function NewConstructor(){}; //to override Login action
//new constructor shold extend AmiIo.Action.Action(actionName)
//and also, you can use
AmiIo.Action.SomeNewAction = function SomeNewAction(){};//to create new actuion
//it also should extend AmiIo.Action.Action(actionName);

AmiIo.Action is just an Object. All constructors are fields of it.

To create new events you don't need to do anything, because it is just an object. If server send to you

Event: Armageddon
SomeField: 123

ami-io will create event with name 'Armageddon' .

To override Client#auth() method, you just should do

var AmiIo = require('ami-io');
AmiIo.Client.prototype.auth = function (){};//new function

amiio is an instance of a Client . The prototype property is only meaningful on constructor functions, such as Client . It is not meaningful on the result of a constructor function (except in the uncommon case that the instance happens to also be a function itself -- but even in that case, altering the instance's prototype does not influence its parent constructor).

Instead, you need to get the instance's prototype with Object.getPrototypeOf :

Object.getPrototypeOf(amiio).auth = function() { ... }

If you don't need to change this for every client, but only a single client, you don't need to change the prototype at all. Changing the instance's auth is sufficient:

amiio.auth = function() { ... }

Note that you code will not work if Action.LoginExt is local to the module scope. If the module exports it, you can probably do AmiIo.Action.LoginExt instead. If it does not export LoginExt , you will need to copy the code that implements it a re-implement it in your importing scope. It may be simpler to modify the module itself.

Here's the solution I applied that worked:

// Override the AmiIo auth procedure, because the other login is slightly different
// Write our own Login function (which adds a company)
function Login(username, secret, company) {
  Login.super_.bind(this, 'Login')();
  this.set('Username', username);
  this.set('Secret', secret );
  this.set('Company', company);
}

// This function should inherit from Action
var util = require('util');
util.inherits(Login, AmiIo.Action.Action);
AmiIo.Action.Login = Login;

// replace the auth with our own, to add the company. Also
// this sends a slightly different greeting: "Service Version 1.0"
AmiIo.Client.prototype.auth = function (data) {
  if (data.match(/Service Version/)) {
    this._setVersion(data);
    this.socket.on('data', function (data) {
      this.splitMessages(data);
    }.bind(this));
    this.send(new AmiIo.Action.Login(this.config.login, this.config.password, this.config.company), function (error, response) {
      if (response && response.response === 'Success') this.emit('connected');
      else this.emit('incorrectLogin');
    }.bind(this));
  } else {
    this.emit('incorrectServer', data);
  }
};

// our own function to grab the version number from the new greeting
AmiIo.Client.prototype._setVersion = function(version){
  var v = version.match(/Service Version ([\d\.]*[\-\w\d\.]*)/i);
  if (v){
    this.version = v[1];
  }
};

So it turns out this was as doable as I hoped it would be. Both answers by @NumminorihSF and @apsillers helped me here, but I could mark only one of them as the best answer.

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