|                                                                                                                                                                                                                                    |  | const Base = require('sdk-base');const util = require('util');const ready = require('get-ready');const copy = require('copy-to');const currentIP = require('address').ip();
const RR = 'roundRobin';const MS = 'masterSlave';
module.exports = function (OssClient) {  function Client(options) {    if (!(this instanceof Client)) {      return new Client(options);    }
    if (!options || !Array.isArray(options.cluster)) {      throw new Error('require options.cluster to be an array');    }
    Base.call(this);
    this.clients = [];    this.availables = {};
    for (let i = 0; i < options.cluster.length; i++) {      const opt = options.cluster[i];      copy(options).pick('timeout', 'agent', 'urllib').to(opt);      this.clients.push(new OssClient(opt));      this.availables[i] = true;    }
    this.schedule = options.schedule || RR;    // only read from master, default is false
    this.masterOnly = !!options.masterOnly;    this.index = 0;
    const heartbeatInterval = options.heartbeatInterval || 10000;    this._checkAvailableLock = false;    this._timerId = this._deferInterval(this._checkAvailable.bind(this, true), heartbeatInterval);    this._ignoreStatusFile = options.ignoreStatusFile || false;    this._init();  }
  util.inherits(Client, Base);  const proto = Client.prototype;  ready.mixin(proto);
  const GET_METHODS = ['head', 'get', 'getStream', 'list', 'getACL'];
  const PUT_METHODS = ['put', 'putStream', 'delete', 'deleteMulti', 'copy', 'putMeta', 'putACL'];
  GET_METHODS.forEach(method => {    proto[method] = async function (...args) {      const client = this.chooseAvailable();      let lastError;      try {        return await client[method](...args);      } catch (err) {        if (err.status && err.status >= 200 && err.status < 500) {          // 200 ~ 499 belong to normal response, don't try again
          throw err;        }        // < 200 || >= 500 need to retry from other cluser node
        lastError = err;      }
      for (let i = 0; i < this.clients.length; i++) {        const c = this.clients[i];        if (c !== client) {          try {            return await c[method].apply(client, args);          } catch (err) {            if (err.status && err.status >= 200 && err.status < 500) {              // 200 ~ 499 belong to normal response, don't try again
              throw err;            }            // < 200 || >= 500 need to retry from other cluser node
            lastError = err;          }        }      }
      lastError.message += ' (all clients are down)';      throw lastError;    };  });
  // must cluster node write success
  PUT_METHODS.forEach(method => {    proto[method] = async function (...args) {      const res = await Promise.all(this.clients.map(client => client[method](...args)));      return res[0];    };  });
  proto.signatureUrl = function signatureUrl(/* name */ ...args) {    const client = this.chooseAvailable();    return client.signatureUrl(...args);  };
  proto.getObjectUrl = function getObjectUrl(/* name, baseUrl */ ...args) {    const client = this.chooseAvailable();    return client.getObjectUrl(...args);  };
  proto._init = function _init() {    const that = this;    (async () => {      await that._checkAvailable(that._ignoreStatusFile);      that.ready(true);    })().catch(err => {      that.emit('error', err);    });  };
  proto._checkAvailable = async function _checkAvailable(ignoreStatusFile) {    const name = `._ali-oss/check.status.${currentIP}.txt`;    if (!ignoreStatusFile) {      // only start will try to write the file
      await this.put(name, Buffer.from(`check available started at ${Date()}`));    }
    if (this._checkAvailableLock) {      return;    }    this._checkAvailableLock = true;    const downStatusFiles = [];    for (let i = 0; i < this.clients.length; i++) {      const client = this.clients[i];      // check 3 times
      let available = await this._checkStatus(client, name);      if (!available) {        // check again
        available = await this._checkStatus(client, name);      }      if (!available) {        // check again
        /* eslint no-await-in-loop: [0] */        available = await this._checkStatus(client, name);        if (!available) {          downStatusFiles.push(client._objectUrl(name));        }      }      this.availables[i] = available;    }    this._checkAvailableLock = false;
    if (downStatusFiles.length > 0) {      const err = new Error(        `${downStatusFiles.length} data node down, please check status file: ${downStatusFiles.join(', ')}`      );      err.name = 'CheckAvailableError';      this.emit('error', err);    }  };
  proto._checkStatus = async function _checkStatus(client, name) {    let available = true;    try {      await client.head(name);    } catch (err) {      // 404 will be available too
      if (!err.status || err.status >= 500 || err.status < 200) {        available = false;      }    }    return available;  };
  proto.chooseAvailable = function chooseAvailable() {    if (this.schedule === MS) {      // only read from master
      if (this.masterOnly) {        return this.clients[0];      }      for (let i = 0; i < this.clients.length; i++) {        if (this.availables[i]) {          return this.clients[i];        }      }      // all down, try to use this first one
      return this.clients[0];    }
    // RR
    let n = this.clients.length;    while (n > 0) {      const i = this._nextRRIndex();      if (this.availables[i]) {        return this.clients[i];      }      n--;    }    // all down, try to use this first one
    return this.clients[0];  };
  proto._nextRRIndex = function _nextRRIndex() {    const index = this.index++;    if (this.index >= this.clients.length) {      this.index = 0;    }    return index;  };
  proto._error = function error(err) {    if (err) throw err;  };
  proto._createCallback = function _createCallback(ctx, gen, cb) {    return () => {      cb = cb || this._error;      gen.call(ctx).then(() => {        cb();      }, cb);    };  };  proto._deferInterval = function _deferInterval(gen, timeout, cb) {    return setInterval(this._createCallback(this, gen, cb), timeout);  };
  proto.close = function close() {    clearInterval(this._timerId);    this._timerId = null;  };
  return Client;};
 |