docs: function createClientType() {
      log.info("create client");
      // [ -Events- ]
      // error - when there's a problem with a received message
      // open - when the connection is open
      // close - when the connection is closed
      // message - when there's a message from the hub
      // [ -Constructor- ]
      function Client() {
          log.info("instantiate client");
          // get non-ambiguous this
          var self = this;
          // Call parent constructor
          Client.super_.call(self);
          // Internal structures
          self._transport = undefined;
          self._txRegistry = {};
          self._lastTxId = 0;
          self._txLimit = 1000;
          self._state = "NOT_OPENED";
          self._messageDispatch = {
              distribution: self._handleDistribution.bind(self),
              published: self._handlePublication.bind(self),
              subscribed: self._handleSubscription.bind(self),
              unsubscribed: self._handleUnsubscription.bind(self),
          };
          // state listeners
          self.once("open", function handleOpened() {
              self._state = "OPEN";
          }).once("close", function handleClosed() {
              self._state = "CLOSED";
          });
          // constructor - return undefined
          return undefined;
      }
      // [ -Inheritance- ]
      util.inherits(Client, events.EventEmitter);
      // [ -Private API- ]
      // check state for OPEN
      Client.prototype._checkState = function clientCheckState() {
          if (this._state !== "OPEN") {
              throw new VError("Nonsensical to call before 'open' or after 'close' events");
          }
          return this;
      };
      // get a transaction ID
      Client.prototype._getTxID = function clientGetTxID() {
          var txid;
          // try the last txid + 1 first
          var candidate = this._lastTxId + 1;
          // try to get a txid until we get one, or we've failed _txLimit times
          // (this means we have _txLimit txid's already assigned)
          var tries = 0;
          while (txid === undefined && tries < this._txLimit) {
              tries++;
              candidate %= this._txLimit;
              if (this._txRegistry[candidate] === undefined) {
                  txid = candidate;
              } else {
                  candidate++;
              }
          }
          // catch the error exit
          if (txid === undefined) {
              throw new VError("Cannot issue a transaction ID - too many outstanding transactions (%d)", this._txLimit);
          }
          // set the last txid and return it to the caller
          this._lastTxId = txid;
          return txid;
      };
      // register a transaction ID
      Client.prototype._registerTxID = function clientRegisterTxID(txid, cb) {
          this._txRegistry[txid] = cb;
          // allow chaining
          return this;
      };
      // retire a transaction ID
      Client.prototype._retireTxID = function clientRetireTxID(txid) {
          delete this._txRegistry[txid];
          // allow chaining
          return this;
      };
      // handle publication message
      Client.prototype._handlePublication = function clientHandlePublication(messageObj) {
          log.info("client handle publication");
          // validate message
          var error = validateProperties(
              messageObj,
             "published message from transport layer",
             ['type', 'message', 'channel', 'number', 'transactionID'],
             []
          );
          // FIXME - message should be JSON
          // FIXME - channel should be string
          // FIXME - number should be a number
          // FIXME - txid should be number
          // FIXME - need a test for no tx callback (on purpose)
          if (error) { this.emit('error', error); return null; }
          var transactionCallback = this._txRegistry[messageObj.transactionID];
          if (transactionCallback) {
              transactionCallback(null, messageObj.message, messageObj.channel, messageObj.number);
              this._retireTxID(messageObj.transactionID);
          } else {
              this.emit('error', new VError("received a message with an unrecognized transaction ID: %j", messageObj));
          }
          // allow chaining
          return this;
      };
      // handle subscription message
      Client.prototype._handleSubscription = function clientHandleSubscription(messageObj) {
          log.info("client on subscribed");
          var error = validateProperties(
              messageObj,
             "subscribed message from transport layer",
             ['type', 'pattern', 'number', 'transactionID'],
             []
          );
          // FIXME - pattern should be regex
          // FIXME - number should be a number
          if (error) { this.emit('error', error); return null; }
          var transactionCallback = this._txRegistry[messageObj.transactionID];
          if (transactionCallback) {
              log.debug("calling tx callback with", messageObj.pattern, messageObj.number);
              transactionCallback(null, messageObj.pattern, messageObj.number);
              this._retireTxID(messageObj.transactionID);
          } else {
              this.emit('error', new VError("received a message with an unrecognized transaction ID: %j", messageObj));
          }
          // allow chaining
          return this;
      };
      // handle unsubscription message
      Client.prototype._handleUnsubscription = function clientHandleUnsubscription(messageObj) {
          log.info("client on unsubscribed");
          var error = validateProperties(
              messageObj,
             "unsubscribed message from transport layer",
             ['type', 'pattern', 'number', 'transactionID'],
             []
          );
          // FIXME - pattern should be regex
          // FIXME - number should be a number
          if (error) { this.emit('error', error); return null; }
          var transactionCallback = this._txRegistry[messageObj.transactionID];
          if (transactionCallback) {
              transactionCallback(null, messageObj.pattern, messageObj.number);
              this._retireTxID(messageObj.transactionID);
          } else {
              this.emit('error', new VError("received a message with an unrecognized transaction ID: %j", messageObj));
          }
          // allow chaining
          return this;
      };
      // handle distributions
      Client.prototype._handleDistribution = function clientDistribute(messageObj) {
          log.info("client on distribution");
          var error = validateProperties(
              messageObj,
             "distribution message from transport layer",
             ['type', 'message', 'channel', 'match'],
             []
          );
          // FIXME - message should be JSON
          // FIXME - channel should be string
          // FIXME - match should be a regex
          if (error) { this.emit('error', error); return null; }
          this.emit('message', messageObj.message, messageObj.channel, messageObj.match);
          // allow chaining
          return this;
      };
      // handle messages from the transport layer
      Client.prototype._handleMessage = function clientHandleMessage(messageObj) {
          log.info("client handle message", this._id, messageObj);
          // parse the message type and respond accordingly
          var handler = this._messageDispatch[messageObj.type];
          if (handler === undefined) {
              this.emit('error', new VError("No dispatcher found for message type (%s) (%j)", messageObj.type, messageObj));
          } else {
              handler(messageObj);
          }
          // allow chaining
          return this;
      };
      // [ -Public API- ]
      // get/set the limit for the max number of client-initiated
      // transactions in flight simultaneously
      Client.prototype.txLimit = function clientTxLimit(limit) {
          if (limit) {
              this._txLimit = limit;
          }
          return this._txLimit;
      };
      // subscribe a subscriber
      Client.prototype.subscribe = function clientSubscribe(pattern, cb) {
          log.info("client subscribe", this._transport._id, pattern);
          // get a non-ambiguous "this"
          var self = this;
          // check state
          this._checkState();
          // get a transaction id
          var txid;
          try {
              txid = self._getTxID();
          } catch (e) {
              callback(cb)(new VError(e, "cannot subscribe - no transaction ID available."));
              // early return because we don't want to execute anything else,
              // and js has no try/catch/else
              return self;
          }
          self._registerTxID(txid, callback(cb));
          // send subscription request
          self._transport.send({
              type: 'subscription',
              'pattern': pattern,
              transactionID: txid,
          }, function handleCompletion(error) {
              // if there's an error, need to retire the TXID early.
              // assumes a send error means the message was not sent,
              // and there's not going to be a corresponding response.
              if (error) {
                  self._retireTxID(txid);
                  cb(error);
              }
              return null;
          });
          // allow chaining of calls
          return self;
      };
      // unsubscribe a pattern
      Client.prototype.unsubscribe = function clientUnsubscribe(pattern, cb) {
          log.info("client unsubscribe", this._transport._id, pattern);
          // get a non-ambiguous "this"
          var self = this;
          // check state
          this._checkState();
          // get a transaction id
          var txid;
          try {
              txid = self._getTxID();
          } catch (e) {
              callback(cb)(new VError(e, "cannot unsubscribe - no transaction ID available."));
              return self;
          }
          self._registerTxID(txid, callback(cb));
          // send unsubscription request
          self._transport.send({
              type: 'unsubscription',
              'pattern': pattern,
              transactionID: txid,
          }, function handleCompletion(error) {
              // if there's an error, need to retire the TXID early.
              // assumes a send error means the message was not sent,
              // and there's not going to be a corresponding response.
              if (error) {
                  self._retireTxID(txid);
                  callback(cb)(error);
              }
              return null;
          });
          // allow chaining
          return self;
      };
      // publish to subscribers
      Client.prototype.publish = function clientPublish(message, channel, cb) {
          log.info("client publish", this._transport._id, message);
          var self = this;
          // check state
          this._checkState();
          // get a transaction id
          var txid;
          try {
              txid = self._getTxID();
          } catch (e) {
              callback(cb)(new VError(e, "cannot publish - no transaction ID available."));
              return self;
          }
          self._registerTxID(txid, callback(cb));
          // send publication request
          self._transport.send({
              type: 'publication',
              'message': message,
              'channel': channel,
              transactionID: txid,
          }, function handleCompletion(error) {
              // if there's an error, need to retire the TXID early.
              // assumes a send error means the message was not sent,
              // and there's not going to be a corresponding response.
              if (error) {
                  self._retireTxID(txid);
                  callback(cb)(new VError(error, "Error sending to hub"));
              }
              return null;
          });
          // allow chaining
          return self;
      };
      // close a transport object
      Client.prototype.close = function clientClose() {
          log.info("client close");
          if (this._state !== "OPEN") {
              throw new VError("Cannot close a client which is not open");
          }
          this._transport.close();
          // it doesn't make sense to do anything with the object after this,
          // so return null
          return null;
      };
      // connect to listening client
      Client.prototype.connect = function clientConnect(url, cb) {
          log.info("client connect");
          // only allow this to be called once on the object
          if (this._transport) {
              // throw immediately - this is programmer error
              throw new VError("'connect' has already been called on this client.");
          }
          // add cb to listeners
          if (cb) {
              this.on('open', cb);
          }
          // open a new transport connection
          this._transport = new ActualTransportClient();
          this._transport.connect(url);
          // proxy some events
          this._transport.on('open', this.emit.bind(this, 'open'));
          this._transport.on('close', this.emit.bind(this, 'close'));
          // handle messages from transport
          this._transport.on('message', this._handleMessage.bind(this));
          // it doesn't make sense to do anything with the object until connect
          // calls back, so return null
          return null;
      };
      // return a new Client
      return Client;
  }