docs: function(service, id, securityOptions) {
    /** The current service being used */
    var _service;
  
    /** The current state */
    var _state;
  
    /** The client identifier */
    var _id;
  
    /*
     * Internal helper function for public methods, to return the current value of
     * _service, regardless of state.
     */
    this._getService = function() {
      return _service;
    };
  
    /*
     * Internal helper function for public methods, to be able to change the state
     * TODO - Ideally we should not have this (i.e. methods that change state
     *        should be defined in the constructor.
     */
    this._setState = function(value) {
      if (_state !== value) {
        logger.log('data', _id, 'Client.state', value);
        _state = value;
      }
    };
  
    /**
     * @return {String} The URL of the service to which the client is currently
     *         connected (when the client is in 'started' state) - otherwise (for
     *         all other client states) undefined is returned.
     */
    Object.defineProperty(this, 'service', {
      get: function() {
        return _state === STATE_STARTED ?
            _service : undefined;
      },
      set: function(value) {
        if (process.env.NODE_ENV === 'unittest') {
          _service = value;
        }
      }
    });
  
    /**
     * @return {String} The identifier associated with the client. This will
     *         either be: a) the identifier supplied as the id property of the
     *         options object supplied to the mqlight.createClient() method, or b)
     *         an automatically generated identifier if the id property was not
     *         specified when the client was created.
     */
    Object.defineProperty(this, 'id', {
      get: function() {
        return _id;
      }
    });
  
    /**
     * @return {String} The current state of the client - can will be one of the
     *         following string values: 'started', 'starting', 'stopped',
     *         'stopping', or 'retrying'.
     */
    Object.defineProperty(this, 'state', {
      get: function() {
        return _state;
      }
    });
  
    logger.entry('Client.constructor', logger.NO_CLIENT_ID);
    logger.log('parms', logger.NO_CLIENT_ID, 'service:',
               String(service).replace(/:[^/:]+@/g, ':********@'));
    logger.log('parms', logger.NO_CLIENT_ID, 'id:', id);
    logger.log('parms', logger.NO_CLIENT_ID, 'securityOptions:',
               securityOptions.toString());
  
    EventEmitter.call(this);
  
    var msg;
    var err;
  
    // Ensure the service is an Array or Function
    var serviceFunction;
    if (service instanceof Function) {
      serviceFunction = service;
    } else if (typeof service === 'string') {
      var serviceUrl = url.parse(service);
      if (serviceUrl.protocol === 'http:' || serviceUrl.protocol === 'https:') {
        serviceFunction = getHttpServiceFunction(serviceUrl);
      } else if (serviceUrl.protocol === 'file:') {
        if (serviceUrl.host.length > 0 && serviceUrl.host !== 'localhost') {
          msg = 'service contains unsupported file URI of ' + service +
                ', only file:///path or file://localhost/path are supported.';
          err = new InvalidArgumentError(msg);
          logger.throw('Client.constructor', logger.NO_CLIENT_ID, err);
          throw err;
        }
        serviceFunction = getFileServiceFunction(serviceUrl.path);
      }
    }
  
    // Add generateServiceList function to client with embedded securityOptions
    this._generateServiceList = function(service) {
      logger.entry('_generateServiceList', _id);
      logger.log('parms', _id, 'service:',
          String(service).replace(/:[^/:]+@/g, ':********@'));
      logger.log('parms', _id, 'securityOptions:',
                 securityOptions.toString());
  
      var err;
  
      // Ensure the service is an Array
      var inputServiceList = [];
      if (!service) {
        err = new TypeError('service is undefined');
        logger.throw('_generateServiceList', _id, err);
        throw err;
      } else if (service instanceof Function) {
        err = new TypeError('service cannot be a function');
        logger.throw('_generateServiceList', _id, err);
        throw err;
      } else if (service instanceof Array) {
        if (service.length === 0) {
          err = new TypeError('service array is empty');
          logger.throw('_generateServiceList', _id, err);
          throw err;
        }
        inputServiceList = service;
      } else if (typeof service === 'string') {
        inputServiceList[0] = service;
      } else {
        err = new TypeError('service must be a string or array type');
        logger.throw('_generateServiceList', _id, err);
        throw err;
      }
  
      /*
       * Validate the list of URLs for the service, inserting default values as
       * necessary Expected format for each URL is: amqp://host:port or
       * amqps://host:port (port is optional, defaulting to 5672 or 5671 as
       * appropriate)
      */
      var serviceList = [];
      var authUser;
      var authPassword;
      var msg;
  
      for (var i = 0; i < inputServiceList.length; i++) {
        var serviceUrl = url.parse(inputServiceList[i]);
        var protocol = serviceUrl.protocol;
  
        // check for auth details
        var auth = serviceUrl.auth;
        authUser = undefined;
        authPassword = undefined;
        if (auth) {
          if (auth.indexOf(':') >= 0) {
            authUser = String(auth).slice(0, auth.indexOf(':'));
            authPassword = String(auth).slice(auth.indexOf(':') + 1);
          } else {
            msg = 'URLs supplied via the \'service\' property must specify ' +
                  'both a user name and a password value, or omit both values';
            err = new InvalidArgumentError(msg);
            logger.throw('_generateServiceList', _id, err);
            throw err;
          }
          if (securityOptions.propertyUser && authUser &&
              (securityOptions.propertyUser !== authUser)) {
            msg = 'User name supplied as \'user\' property (' +
                  securityOptions.propertyUser + ') does not match user name ' +
                  'supplied via a URL passed via the \'service\' property (' +
                  authUser + ')';
            err = new InvalidArgumentError(msg);
            logger.throw('_generateServiceList', _id, err);
            throw err;
          }
          if (securityOptions.propertyPassword && authPassword &&
              (securityOptions.propertyPassword !== authPassword)) {
            msg = 'Password supplied as \'password\' property does not match a ' +
                  'password supplied via a URL passed via the \'service\' ' +
                  'property';
            err = new InvalidArgumentError(msg);
            logger.throw('_generateServiceList', _id, err);
            throw err;
          }
          if (i === 0) {
            securityOptions.urlUser = authUser;
            securityOptions.urlPassword = authPassword;
          }
        }
  
        // Check whatever URL user names / passwords are present this time
        // through the loop - match the ones set on securityOptions by the first
        // pass through the loop.
        if (i > 0) {
          if (securityOptions.urlUser !== authUser) {
            msg = 'URLs supplied via the \'service\' property contain ' +
                  'inconsistent user names';
            err = new InvalidArgumentError(msg);
            logger.throw('_generateServiceList', _id, err);
            throw err;
          } else if (securityOptions.urlPassword !== authPassword) {
            msg = 'URLs supplied via the \'service\' property contain ' +
                  'inconsistent password values';
            err = new InvalidArgumentError(msg);
            logger.throw('_generateServiceList', _id, err);
            throw err;
          }
        }
  
        // Check we are trying to use the amqp protocol
        if (!protocol || (protocol !== 'amqp:' && protocol !== 'amqps:')) {
          msg = 'Unsupported URL \'' + inputServiceList[i] +
                '\' specified for service. Only the amqp or amqps protocol are ' +
                'supported.';
          err = new InvalidArgumentError(msg);
          logger.throw('_generateServiceList', _id, err);
          throw err;
        }
        // Check we have a hostname
        var host = serviceUrl.host;
        if (!host || !serviceUrl.hostname) {
          msg = 'Unsupported URL \' ' + inputServiceList[i] + '\' specified ' +
                'for service. Must supply a hostname.';
          err = new InvalidArgumentError(msg);
          logger.throw('_generateServiceList', _id, err);
          throw err;
        }
        // Set default port if not supplied
        var port = serviceUrl.port;
        if (!port) {
          port = (protocol === 'amqp:') ? '5672' : '5671';
        }
        // Check for no path
        var path = serviceUrl.path;
        if (path && path !== '/') {
          msg = 'Unsupported URL \'' + inputServiceList[i] + '\' paths (' + path +
                ' ) can\'t be part of a service URL.';
          err = new InvalidArgumentError(msg);
          logger.throw('_generateServiceList', _id, err);
          throw err;
        }
  
        serviceList[i] = protocol + '//' + host;
        if (!serviceUrl.port) {
          serviceList[i] += ':' + port;
        }
      }
  
      logger.exit('_generateServiceList', _id,
        [
          'serviceList:',
          String(serviceList).replace(/:[^/:]+@/g, ':********@'),
          'securityOptions:', securityOptions.toString()
        ]);
      return serviceList;
    };
  
    /**
    * Function to invoke all callbacks waiting on a started event.
    *
    * @param {Error} err Set if an error occurred while starting.
    */
    this._invokeStartedCallbacks = function(err) {
      var client = this;
      logger.entry('Client._invokeStartedCallbacks', _id);
  
      var callbacks = client._queuedStartCallbacks.length;
      logger.log('debug', _id, 'callbacks:', callbacks);
  
      for (var i = 0; i < callbacks; i++) {
        var invocation = client._queuedStartCallbacks.shift();
        if (invocation.callback) {
          logger.entry('Client._invokeStartedCallbacks.callback',
                       client.id, invocation.create);
          if (invocation.create) {
            invocation.callback.apply(client, [err ? err : null, client]);
          } else {
            invocation.callback.apply(client, [err]);
          }
          logger.exit('Client._invokeStartedCallbacks.callback', client.id, null);
        } else {
          logger.ffdc('Client._invokeStartedCallbacks', 'ffdc001', client,
                      'No callback provided');
        }
      }
  
      logger.exit('Client._invokeStartedCallbacks', _id, null);
    };
  
    // performs the connect
    this._performConnect = function(newClient, retrying) {
      var client = this;
      logger.entry('Client._performConnect', _id, newClient);
  
      // If there is no active client (i.e. we've been stopped) then add
      // ourselves back to the active list. Otherwise if there is another
      // active client (that's replaced us) then exit function now
      var activeClient = activeClientList.get(_id);
      if (activeClient === undefined) {
        logger.log('debug', _id, 'Adding client to active list, as there' +
            ' is no currently active client');
        activeClientList.add(_id);
      } else if (client !== activeClient) {
        logger.log('debug', _id,
            'Not connecting because client has been replaced');
        if (!client.isStopped()) {
          logger.ffdc('Client._performConnect', 'ffdc005', client,
                      'Replaced client not in stopped state');
        }
        client._invokeStartedCallbacks(new LocalReplacedError(_id));
        logger.exit('Client._performConnect', _id, null);
        return;
      }
  
      if (newClient) {
        client._setState(STATE_STARTING);
      } else {
        var currentState = _state;
        logger.log('debug', _id, 'currentState:', currentState);
        logger.log('debug', _id, 'retrying:', retrying);
  
        // if we are not stopped or stopping state return with the client object
        if (currentState !== STATE_STOPPED && !retrying) {
          if (currentState === STATE_STOPPING) {
            var stillDisconnecting = function(client) {
              logger.entry('stillDisconnecting', _id);
  
              if (_state === STATE_STOPPING) {
                setImmediate(function() {
                  stillDisconnecting(client);
                });
              } else {
                process.nextTick(function() {
                  client._performConnect(newClient, retrying);
                });
              }
  
              logger.exit('stillDisconnecting', _id, null);
            };
  
            setImmediate(function() {
              stillDisconnecting(client);
            });
            logger.exit('Client._performConnect', _id, null);
            return;
          }
          if (currentState === STATE_STARTED) {
            // if the client is already started, then drive any callbacks
            // waiting to be notified
            process.nextTick(function() {
              client._invokeStartedCallbacks(null);
            });
          }
          logger.exit('Client._performConnect', _id, client);
          return client;
        }
  
        if (_state === STATE_STOPPED) {
          client._setState(STATE_STARTING);
        }
      }
  
      // Obtain the list of services for connect and connect to one of the
      // services, retrying until a connection can be established
      var serviceList;
      if (client._serviceFunction instanceof Function) {
        logger.entry('_serviceFunction', client.id);
        var serviceFunctionCallback = function(err, service) {
          if (err) {
            // The service 'lookup' function returned an error. Wait a few
            // seconds and then try again (unless we're in a unit test)
            if (process.env.NODE_ENV === 'unittest') {
              client._setState(STATE_STOPPED);
              client._invokeStartedCallbacks(err);
            } else {
              client._serviceFunctionTimeout = setTimeout(function() {
                client._serviceFunction(serviceFunctionCallback);
              }, 5000);
            }
          } else {
            try {
              client._setState(STATE_STARTING);
              serviceList =
                  client._generateServiceList(service);
              client._connectToService(serviceList);
            } catch (err) {
              logger.caught('_serviceFunction', client.id, err);
              client._setState(STATE_STOPPED);
              client._invokeStartedCallbacks(err);
            }
          }
        };
        client._serviceFunction(serviceFunctionCallback);
        logger.exit('_serviceFunction', client.id, null);
      } else {
        try {
          serviceList = client._generateServiceList(service);
          client._connectToService(serviceList);
        } catch (err) {
          logger.caught('Client._performConnect', client.id, err);
          client._setState(STATE_STOPPED);
          client._invokeStartedCallbacks(err);
        }
      }
  
      logger.exit('Client._performConnect', _id, null);
      return;
    };
  
    /**
    * Function to connect to a service. Callback happens once a successful
    * connect/reconnect occurs.
    *
    * @param {Array} serviceList list of services to connect to.
    */
    this._tryService = function(serviceList) {
      var client = this;
      logger.entry('Client._tryService', _id);
  
      if (serviceList.length === 0) {
        logger.ffdc('Client._tryService', 'ffdc004', client);
      } else {
        try {
          var service = serviceList[0];
          logger.log('data', _id, 'service:', service);
  
          // check if we will be providing authentication information
          var auth;
          if (securityOptions.urlUser) {
            auth = encodeURIComponent(String(securityOptions.urlUser));
            auth += ':';
            auth += encodeURIComponent(String(securityOptions.urlPassword));
            auth += '@';
          } else if (securityOptions.propertyUser) {
            auth = encodeURIComponent(String(securityOptions.propertyUser));
            auth += ':';
            auth +=
                encodeURIComponent(String(securityOptions.propertyPassword));
            auth += '@';
          } else {
            auth = null;
          }
          var logUrl;
          var serviceUrl = url.parse(service);
          // reparse the service url to prepend authentication information
          // back on as required
          if (auth) {
            service = serviceUrl.protocol + '//' + auth + serviceUrl.host;
            logUrl = serviceUrl.protocol + '//' +
                     auth.replace(/:[^/:]+@/g, ':********@') + serviceUrl.host;
          } else {
            logUrl = service;
          }
          logger.log('data', _id, 'attempting to connect to: ' + logUrl);
  
          var connectUrl = url.parse(service);
          // remove any path elements from the URL (for ipv6 which appends /)
          if (connectUrl.path) {
            var hrefLength = connectUrl.href.length - connectUrl.path.length;
            connectUrl.href = connectUrl.href.substr(0, hrefLength);
            connectUrl.pathname = connectUrl.path = null;
          }
  
          // Try and connect to the next service in the list, or retry from the
          // beginning if we've run out of services.
          var tryNextService = function(error) {
            logger.entry('Client._tryService.tryNextService', _id);
  
            if (serviceList.length === 1) {
              // We've tried all services without success. Pause for a while
              // before trying again
              logger.log('data', _id, 'End of service list');
  
              client._setState(STATE_RETRYING);
              var retry = function() {
                logger.entryLevel('entry_often', 'Client._tryService.retry', _id);
                if (!client.isStopped()) {
                  process.nextTick(function() {
                    client._performConnect(false, true);
                  });
                }
                logger.exitLevel('exit_often', 'Client._tryService.retry',
                                 _id, null);
              };
  
              client._retryCount++;
              var retryCap = 60000;
              // Limit to the power of 8 as anything above this will put the
              // interval higher than the cap straight away.
              var exponent = (client._retryCount <= 8) ? client._retryCount : 8;
              var upperBound = Math.pow(2, exponent);
              var lowerBound = 0.75 * upperBound;
              var jitter = Math.random() * (0.25 * upperBound);
              var interval = Math.min(retryCap, (lowerBound + jitter) * 1000);
              // times by CONNECT_RETRY_INTERVAL for unittest purposes
              interval = Math.round(interval) * CONNECT_RETRY_INTERVAL;
              logger.log('data', _id, 'trying to connect again ' +
                         'after ' + (interval / 1000) + ' seconds');
              setTimeout(retry, interval);
              if (error) {
                setImmediate(function() {
                  logger.log('emit', _id, 'error', error);
                  client.emit('error', error);
                });
              }
            } else {
              // Try the next service in the list
              logger.log('data', _id, 'Trying next service');
              client._tryService(serviceList.slice(1));
            }
  
            logger.exit('Client._tryService.tryNextService', _id, null);
          };
  
          // Define an error handler for connection errors. Log the failure and
          // then try the next service.
          var connError = function(err) {
            logger.entry('Client._tryService.connError', _id);
  
            err = lookupError(err);
            logger.log('data', _id, 'failed to connect to: ' + logUrl +
                       ' due to error: ' + util.inspect(err));
  
            // This service failed to connect. Try the next one.
            // XXX: wrap in shouldReconnect ?
            tryNextService(err);
  
            logger.exit('Client._tryService.connError', _id, null);
          };
  
          var connected = function() {
            logger.log('data', _id, 'successfully connected to:', logUrl);
            _service = serviceList[0];
  
            // Indicate that we're started
            client._setState(STATE_STARTED);
            var eventToEmit;
            if (client._firstStart) {
              eventToEmit = STATE_STARTED;
              client._firstStart = false;
              client._retryCount = 0;
              // could be queued actions so need to process those here. On
              // reconnect this would be done via the callback we set, first
              // connect its the users callback so won't process anything.
              logger.log('data', _id, 'first start since being stopped');
              setImmediate(function() {
                processQueuedActions.apply(client);
              });
            } else {
              client._retryCount = 0;
              eventToEmit = STATE_RESTARTED;
            }
            ++client._connectionId;
  
            process.nextTick(function() {
              logger.log('emit', _id, eventToEmit);
              client.emit(eventToEmit);
              client._invokeStartedCallbacks(null);
            });
            return null;
          };
  
          // Otherwise do a standard net connect.
          // Get messenger to connect if it hasn't already.
          var promise;
          if (process.env.NODE_ENV === 'unittest') {
            logger.log('debug', _id, 'connecting via stub');
            // XXX: hack for unittesting purposes
            var connOpts = {
              host: serviceUrl.hostname,
              port: serviceUrl.port,
              rejectUnauthorized: false,
              sslTrustCertificate: securityOptions.sslTrustCertificate,
              sslVerifyName: securityOptions.sslVerifyName
            };
            promise = client._messenger.connect(connectUrl, connOpts);
          } else {
            logger.log('debug', _id, 'connecting via net');
            promise = client._messenger.connect(connectUrl, {
              saslMechanism: (auth) ? 'PLAIN' : 'ANONYMOUS'
            });
          }
          promise.then(connected)
            .catch(function(err) {
              logger.caught('Client._tryService', _id, err);
              connError(err);
            })
            .error(function(err) {
              logger.caught('Client._tryService', _id, err);
              connError(err);
            });
        } catch (err) {
          // should never get here, as it means that messenger.connect has been
          // called in an invalid way, so FFDC
          logger.caught('Client._tryService', _id, err);
          logger.ffdc('Client._tryService', 'ffdc002', client, err);
          client._setState(STATE_STOPPED);
          logger.throw('Client._tryService', _id, err);
          throw err;
        }
      }
  
      logger.exit('Client._tryService', _id, null);
    };
  
    /**
    * Function to connect to the service, trys each available service
    * in turn. If none can connect it emits an error, waits and
    * attempts to connect again. Callback happens once a successful
    * connect/reconnect occurs.
    *
    * @param {Array} serviceList list of services to connect to.
    */
    this._connectToService = function(serviceList) {
      var client = this;
      logger.entry('Client._connectToService', _id);
  
      if (client.isStopped()) {
        client._invokeStartedCallbacks(
            new StoppedError('connect aborted due to stop'));
        logger.exit('Client._connectToService', _id, null);
        return;
      }
  
      this._tryService(serviceList);
  
      logger.exit('Client._connectToService', _id, null);
      return;
    };
  
    // If client id has not been specified then generate an id
    if (!id) id = 'AUTO_' + uuid.v4().substring(0, 7);
  
    // If the client id is too long then throw an error
    if (id.length > 256) {
      msg = 'Client identifier \'' + id + '\' is longer than the maximum ' +
            'ID length of 256.';
      err = new InvalidArgumentError(msg);
      logger.throw('Client.constructor', logger.NO_CLIENT_ID, err);
      throw err;
    }
  
    id = String(id);
  
    // currently client ids are restricted, reject any invalid ones
    var matches = invalidClientIdRegex.exec(id);
    if (matches) {
      msg =
          'Client Identifier \'' + id + '\' contains invalid char: ' + matches[0];
      err = new InvalidArgumentError(msg);
      logger.throw('Client.constructor', logger.NO_CLIENT_ID, err);
      throw err;
    }
  
    // User/password must either both be present, or both be absent.
    if ((securityOptions.propertyUser && !securityOptions.propertyPassword) ||
        (!securityOptions.propertyUser && securityOptions.propertyPassword)) {
      err = new InvalidArgumentError('both user and password properties ' +
                                     'must be specified together');
      logger.throw('Client.constructor', id, err);
      throw err;
    }
  
    // Validate the ssl security options
    var keystoreOption = '';
    var keystoreOptionCount = 0;
    if (typeof securityOptions.sslKeystorePassphrase !== 'undefined') {
      keystoreOption = 'sslKeystorePassphrase';
      keystoreOptionCount++;
    }
    if (typeof securityOptions.sslKeystore !== 'undefined') {
      if (keystoreOptionCount > 0) keystoreOption += ', ';
      keystoreOption += 'sslKeystore';
      keystoreOptionCount++;
    }
  
    var clientCertOption = '';
    var clientCertOptionCount = 0;
    if (typeof securityOptions.sslClientCertificate !== 'undefined') {
      clientCertOption = 'sslClientCertificate';
      clientCertOptionCount++;
    }
    if (typeof securityOptions.sslClientKey !== 'undefined') {
      if (clientCertOptionCount > 0) clientCertOption += ', ';
      clientCertOption += 'sslClientKey';
      clientCertOptionCount++;
    }
    if (typeof securityOptions.sslClientKeyPassphrase !== 'undefined') {
      if (clientCertOptionCount > 0) clientCertOption += ', ';
      clientCertOption += 'sslClientKeyPassphrase';
      clientCertOptionCount++;
    }
  
    var certificateOption = '';
    if (typeof securityOptions.sslTrustCertificate === 'undefined') {
      certificateOption = clientCertOption;
    } else {
      if (clientCertOptionCount > 0) certificateOption += ', ';
      certificateOption += 'sslTrustCertificate';
    }
  
    if ((keystoreOption.length > 0) && (certificateOption.length > 0)) {
      err = new TypeError(keystoreOption + ' and ' + certificateOption +
                          ' options cannot be specified together');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
    if ((keystoreOption.length > 0) && (keystoreOptionCount !== 2)) {
      err = new TypeError('sslKeystore and sslKeystorePassphrase options' +
                          ' must both be specified');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
    if ((clientCertOption.length > 0) && (clientCertOptionCount !== 3)) {
      err = new TypeError('sslClientCertificate, sslClientKey and' +
                          ' sslClientKeyPassphrase options must all be' +
                          ' specified');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
  
    var sslOptions = [
      {name: 'sslKeystore', value: securityOptions.sslKeystore},
      {name: 'sslTrustCertificate', value: securityOptions.sslTrustCertificate},
      {name: 'sslClientCertificate',
        value: securityOptions.sslClientCertificate},
      {name: 'sslClientKey', value: securityOptions.sslClientKey}
    ];
    for (var i = 0; i < sslOptions.length; i++) {
      var sslOption = sslOptions[i];
      if (typeof sslOption.value !== 'undefined') {
        if (typeof sslOption.value !== 'string') {
          err = new TypeError(sslOption.name + ' value \'' +
                              sslOption.value +
                              '\' is invalid. Must be of type String');
          logger.throw('Client.constructor', _id, err);
          throw err;
        }
        if (!fs.existsSync(sslOption.value)) {
          err = new TypeError('The file specified for ' + sslOption.name + ' \'' +
                              sslOption.value +
                              '\' does not exist');
          logger.throw('Client.constructor', _id, err);
          throw err;
        }
        if (!fs.statSync(sslOption.value).isFile()) {
          err = new TypeError('The file specified for ' + sslOption.name + ' \'' +
                              sslOption.value +
                              '\' is not a regular file');
          logger.throw('Client.constructor', _id, err);
          throw err;
        }
      }
    }
    if ((typeof securityOptions.sslKeystorePassphrase !== 'undefined') &&
        (typeof securityOptions.sslKeystorePassphrase !== 'string')) {
      err = new TypeError('sslKeystorePassphrase value \'' +
                          securityOptions.sslKeystorePassphrase +
                          '\' is invalid. Must be of type String');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
    if ((typeof securityOptions.sslClientKeyPassphrase !== 'undefined') &&
        (typeof securityOptions.sslClientKeyPassphrase !== 'string')) {
      err = new TypeError('sslClientKeyPassphrase value \'' +
                          securityOptions.sslClientKeyPassphrase +
                          '\' is invalid. Must be of type String');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
    if (typeof securityOptions.sslVerifyName !== 'boolean') {
      err = new TypeError('sslVerifyName value \'' +
          securityOptions.sslVerifyName +
          '\' is invalid. Must be of type Boolean');
      logger.throw('Client.constructor', _id, err);
      throw err;
    }
  
    // Save the required data as client fields
    this._serviceFunction = serviceFunction;
    _id = id;
  
    logger.entry('proton.createMessenger', _id);
    if (process.env.NODE_ENV === 'unittest') {
      this._messenger =
          require('./test/stubs/stubproton.js').createProtonStub().messenger;
    } else {
      var policy = AMQP.Policy.merge({
        defaultSubjects: false,
        reconnect: {
          retries: 0,
          strategy: 'exponential',
          forever: false
        },
        connect: {
          options: {
            containerId: _id,
            idleTimeout: 0
          }
        }
      });
  
      sslOptions = {
        keyFile: null,
        certFile: null,
        caFile: null,
        rejectUnauthorized: true
      };
  
      if (securityOptions.sslVerifyName === false) {
        sslOptions.checkServerIdentity = function() {
          return null;
        };
      }
  
      // Replace the default AMQP policy for parsing addresses (because it is too
      // simplistic and looks for the first colon (':') character in the decoded
      // URI, breaking if either the username or password contain this character).
      if (policy.parseAddress) {
        var originalParseAddress = policy.parseAddress;
        policy.parseAddress = function(amqpAddress) {
          var result = originalParseAddress(amqpAddress);
          if (amqpAddress.auth) {
            // Capture the part of the encoded URI between '//' and '@'
            var matchAuth = /.+\/\/([^@]+).+/g;
            var auth = matchAuth.exec(amqpAddress.href)[1];
            var authSplit = auth.split(':');
            result.user = decodeURIComponent(authSplit[0]);
            if (authSplit[1]) {
              result.pass = decodeURIComponent(authSplit[1]);
            }
          }
          return result;
        };
      }
  
      // Read the client keystore or pem files as appropriate, setting the
      // required security related connection options
      if (typeof securityOptions.sslKeystore === 'undefined') {
        if (typeof securityOptions.sslClientCertificate !== 'undefined') {
          sslOptions.cert =
            fs.readFileSync(securityOptions.sslClientCertificate, 'utf-8');
        }
        if (typeof securityOptions.sslClientKey !== 'undefined') {
          sslOptions.key = fs.readFileSync(securityOptions.sslClientKey,
            'utf-8');
          sslOptions.passphrase = securityOptions.sslClientKeyPassphrase;
        }
        // Read the pem file and load multiple certs up into a separate entry
        if (typeof securityOptions.sslTrustCertificate !== 'undefined') {
          sslOptions.ca = fs.readFileSync(securityOptions.sslTrustCertificate,
            'utf-8').match(pemCertRegex);
        }
      } else {
        sslOptions.pfx = fs.readFileSync(securityOptions.sslKeystore);
        sslOptions.passphrase = securityOptions.sslKeystorePassphrase;
      }
  
      policy.connect.options.sslOptions = sslOptions;
      logger.log('debug', _id, 'sslOptions', sslOptions);
  
      this._messenger = new AMQP.Client(policy);
    }
    logger.exit('proton.createMessenger', _id, null);
  
    var client = this;
    this._messenger.on(AMQP.Client.ErrorReceived, function(err) {
      logger.entry('Client._messenger.on.ErrorReceived', _id);
      if (err) {
        err = lookupError(err);
        logger.log('parms', _id, 'err:', err);
        logger.log('data', _id, _state);
        if (_state === STATE_STARTED || err instanceof SecurityError ||
            err instanceof NotPermittedError) {
          if (!(err instanceof NotPermittedError) || _state !== STATE_STARTED) {
            logger.log('debug', _id, '_state', _state);
            logger.log('emit', _id, 'error', err);
            client.emit('error', err);
          }
          if (shouldReconnect(err)) {
            setImmediate(function() {
              logger.log('data', _id, 'calling reconnect', err);
              reconnect(client);
            });
          } else {
            stopMessenger(client);
          }
        }
      }
      logger.exit('Client._messenger.on.ErrorReceived', _id, null);
    });
  
    this._messenger.on(AMQP.Client.ConnectionClosed, function() {
      logger.entry('Client._messenger.on.ConnectionClosed', _id);
      if (_state === STATE_STARTED) {
        logger.log('data', client.id, 'calling reconnect');
        reconnect(client);
      } else if (_state === STATE_RETRYING || _state === STATE_STARTING) {
        //
      }
      logger.exit('Client._messenger.on.ConnectionClosed', _id, null);
    });
  
    // Set the initial state to starting
    _state = STATE_STARTING;
    _service = null;
    // The stream associated with the client
    this._stream = null;
    // The first start, set to false after start and back to true on stop
    this._firstStart = true;
    // List of callbacks to notify when a start operation completes
    this._queuedStartCallbacks = [];
  
    // List of message subscriptions
    this._subscriptions = [];
    // List of queued subscriptions
    this._queuedSubscriptions = [];
    // List of queued unsubscribe requests
    this._queuedUnsubscribes = [];
  
    // List of outstanding send operations waiting to be accepted, settled, etc
    // by the listener.
    this._outstandingSends = [];
    // List of queuedSends for resending on a reconnect
    this._queuedSends = [];
    // List of callbacks to notify when a send operation completes
    this._queuedSendCallbacks = [];
  
    // No drain event initially required
    this._drainEventRequired = false;
  
    // Number of attempts the client has tried to reconnect
    this._retryCount = 0;
  
    // An identifier for the connection
    this._connectionId = 0;
  
    // List of chunks ready to push into proton
    this._queuedChunks = [];
    this._queuedChunksSize = 0;
  
    if (!serviceFunction) {
      this._serviceList = this._generateServiceList(service);
    }
    logger.exit('Client.constructor', _id, this);
  }