'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.WRITE = exports.READ = exports.Driver = undefined;

var _maxSafeInteger = require('babel-runtime/core-js/number/max-safe-integer');

var _maxSafeInteger2 = _interopRequireDefault(_maxSafeInteger);

var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);

var _get2 = require('babel-runtime/helpers/get');

var _get3 = _interopRequireDefault(_get2);

var _inherits2 = require('babel-runtime/helpers/inherits');

var _inherits3 = _interopRequireDefault(_inherits2);

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _createClass2 = require('babel-runtime/helpers/createClass');

var _createClass3 = _interopRequireDefault(_createClass2);

var _session = require('./session');

var _session2 = _interopRequireDefault(_session);

var _pool = require('./internal/pool');

var _pool2 = _interopRequireDefault(_pool);

var _connection = require('./internal/connection');

var _connection2 = _interopRequireDefault(_connection);

var _streamObserver = require('./internal/stream-observer');

var _streamObserver2 = _interopRequireDefault(_streamObserver);

var _error = require('./error');

var _connectionProviders = require('./internal/connection-providers');

var _bookmark = require('./internal/bookmark');

var _bookmark2 = _interopRequireDefault(_bookmark);

var _connectivityVerifier = require('./internal/connectivity-verifier');

var _connectivityVerifier2 = _interopRequireDefault(_connectivityVerifier);

var _poolConfig = require('./internal/pool-config');

var _poolConfig2 = _interopRequireDefault(_poolConfig);

var _logger = require('./internal/logger');

var _logger2 = _interopRequireDefault(_logger);

var _connectionErrorHandler = require('./internal/connection-error-handler');

var _connectionErrorHandler2 = _interopRequireDefault(_connectionErrorHandler);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var DEFAULT_MAX_CONNECTION_LIFETIME = 60 * 60 * 1000; // 1 hour

/**
 * Constant that represents read session access mode.
 * Should be used like this: `driver.session(neo4j.session.READ)`.
 * @type {string}
 */
/**
 * Copyright (c) 2002-2018 "Neo4j,"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var READ = 'READ';

/**
 * Constant that represents write session access mode.
 * Should be used like this: `driver.session(neo4j.session.WRITE)`.
 * @type {string}
 */
var WRITE = 'WRITE';

var idGenerator = 0;

/**
 * A driver maintains one or more {@link Session}s with a remote
 * Neo4j instance. Through the {@link Session}s you can send statements
 * and retrieve results from the database.
 *
 * Drivers are reasonably expensive to create - you should strive to keep one
 * driver instance around per Neo4j Instance you connect to.
 *
 * @access public
 */

var Driver = function () {
  /**
   * You should not be calling this directly, instead use {@link driver}.
   * @constructor
   * @param {string} hostPort
   * @param {string} userAgent
   * @param {object} authToken
   * @param {object} config
   * @protected
   */
  function Driver(hostPort, userAgent) {
    var authToken = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
    var config = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
    (0, _classCallCheck3.default)(this, Driver);

    sanitizeConfig(config);

    this._id = idGenerator++;
    this._hostPort = hostPort;
    this._userAgent = userAgent;
    this._openConnections = {};
    this._authToken = authToken;
    this._config = config;
    this._log = _logger2.default.create(config);
    this._pool = new _pool2.default(this._createConnection.bind(this), this._destroyConnection.bind(this), this._validateConnection.bind(this), _poolConfig2.default.fromDriverConfig(config), this._log);

    /**
     * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}.
     * @type {ConnectionProvider}
     * @protected
     */
    this._connectionProvider = null;

    this._onCompleted = null;

    this._afterConstruction();
  }

  /**
   * @protected
   */


  (0, _createClass3.default)(Driver, [{
    key: '_afterConstruction',
    value: function _afterConstruction() {
      this._log.info('Direct driver ' + this._id + ' created for server address ' + this._hostPort);
    }

    /**
     * Get the installed connectivity verification callback.
     * @return {null|function}
     * @deprecated driver can be used directly once instantiated, use of this callback is not required.
     */

  }, {
    key: '_createConnection',


    /**
     * Create a new connection and initialize it.
     * @return {Promise<Connection>} promise resolved with a new connection or rejected when failed to connect.
     * @access private
     */
    value: function _createConnection(hostPort, release) {
      var _this = this;

      var connection = _connection2.default.create(hostPort, this._config, this._createConnectionErrorHandler(), this._log);
      connection._release = function () {
        return release(hostPort, connection);
      };
      this._openConnections[connection.id] = connection;

      return connection.connect(this._userAgent, this._authToken).catch(function (error) {
        if (_this.onError) {
          // notify Driver.onError callback about connection initialization errors
          _this.onError(error);
        }
        // propagate the error because connection failed to connect / initialize
        throw error;
      });
    }

    /**
     * Check that a connection is usable
     * @return {boolean} true if the connection is open
     * @access private
     **/

  }, {
    key: '_validateConnection',
    value: function _validateConnection(conn) {
      if (!conn.isOpen()) {
        return false;
      }

      var maxConnectionLifetime = this._config.maxConnectionLifetime;
      var lifetime = Date.now() - conn.creationTimestamp;
      return lifetime <= maxConnectionLifetime;
    }

    /**
     * Dispose of a connection.
     * @return {Connection} the connection to dispose.
     * @access private
     */

  }, {
    key: '_destroyConnection',
    value: function _destroyConnection(conn) {
      delete this._openConnections[conn.id];
      conn.close();
    }

    /**
     * Acquire a session to communicate with the database. The driver maintains
     * a pool of sessions, so calling this method is normally cheap because you
     * will be pulling a session out of the common pool.
     *
     * This comes with some responsibility - make sure you always call
     * {@link close} when you are done using a session, and likewise,
     * make sure you don't close your session before you are done using it. Once
     * it is returned to the pool, the session will be reset to a clean state and
     * made available for others to use.
     *
     * @param {string} [mode=WRITE] the access mode of this session, allowed values are {@link READ} and {@link WRITE}.
     * @param {string|string[]} [bookmarkOrBookmarks=null] the initial reference or references to some previous
     * transactions. Value is optional and absence indicates that that the bookmarks do not exist or are unknown.
     * @return {Session} new session.
     */

  }, {
    key: 'session',
    value: function session(mode, bookmarkOrBookmarks) {
      var sessionMode = Driver._validateSessionMode(mode);
      var connectionProvider = this._getOrCreateConnectionProvider();
      var bookmark = bookmarkOrBookmarks ? new _bookmark2.default(bookmarkOrBookmarks) : _bookmark2.default.empty();
      return new _session2.default(sessionMode, connectionProvider, bookmark, this._config);
    }
  }, {
    key: '_createConnectionProvider',


    // Extension point
    value: function _createConnectionProvider(hostPort, connectionPool, driverOnErrorCallback) {
      return new _connectionProviders.DirectConnectionProvider(hostPort, connectionPool, driverOnErrorCallback);
    }

    // Extension point

  }, {
    key: '_createConnectionErrorHandler',
    value: function _createConnectionErrorHandler() {
      return new _connectionErrorHandler2.default(_error.SERVICE_UNAVAILABLE);
    }
  }, {
    key: '_getOrCreateConnectionProvider',
    value: function _getOrCreateConnectionProvider() {
      if (!this._connectionProvider) {
        var driverOnErrorCallback = this._driverOnErrorCallback.bind(this);
        this._connectionProvider = this._createConnectionProvider(this._hostPort, this._pool, driverOnErrorCallback);
      }
      return this._connectionProvider;
    }
  }, {
    key: '_driverOnErrorCallback',
    value: function _driverOnErrorCallback(error) {
      var userDefinedOnErrorCallback = this.onError;
      if (userDefinedOnErrorCallback && error.code === _error.SERVICE_UNAVAILABLE) {
        userDefinedOnErrorCallback(error);
      } else {
        // we don't need to tell the driver about this error
      }
    }

    /**
     * Close all open sessions and other associated resources. You should
     * make sure to use this when you are done with this driver instance.
     * @return undefined
     */

  }, {
    key: 'close',
    value: function close() {
      this._log.info('Driver ' + this._id + ' closing');

      try {
        // purge all idle connections in the connection pool
        this._pool.purgeAll();
      } finally {
        // then close all connections driver has ever created
        // it is needed to close connections that are active right now and are acquired from the pool
        for (var connectionId in this._openConnections) {
          if (this._openConnections.hasOwnProperty(connectionId)) {
            this._openConnections[connectionId].close();
          }
        }
      }
    }
  }, {
    key: 'onCompleted',
    get: function get() {
      return this._onCompleted;
    }

    /**
     * Install a connectivity verification callback.
     * @param {null|function} callback the new function to be notified about successful connection.
     * @deprecated driver can be used directly once instantiated, use of this callback is not required.
     */
    ,
    set: function set(callback) {
      this._onCompleted = callback;
      if (this._onCompleted) {
        var connectionProvider = this._getOrCreateConnectionProvider();
        var connectivityVerifier = new _connectivityVerifier2.default(connectionProvider, this._onCompleted);
        connectivityVerifier.verify();
      }
    }
  }], [{
    key: '_validateSessionMode',
    value: function _validateSessionMode(rawMode) {
      var mode = rawMode || WRITE;
      if (mode !== READ && mode !== WRITE) {
        throw (0, _error.newError)('Illegal session mode ' + mode);
      }
      return mode;
    }
  }]);
  return Driver;
}();

/** Internal stream observer used for connection state */


var _ConnectionStreamObserver = function (_StreamObserver) {
  (0, _inherits3.default)(_ConnectionStreamObserver, _StreamObserver);

  function _ConnectionStreamObserver(driver, conn) {
    (0, _classCallCheck3.default)(this, _ConnectionStreamObserver);

    var _this2 = (0, _possibleConstructorReturn3.default)(this, (_ConnectionStreamObserver.__proto__ || (0, _getPrototypeOf2.default)(_ConnectionStreamObserver)).call(this));

    _this2._driver = driver;
    _this2._conn = conn;
    _this2._hasFailed = false;
    return _this2;
  }

  (0, _createClass3.default)(_ConnectionStreamObserver, [{
    key: 'onError',
    value: function onError(error) {
      if (!this._hasFailed) {
        (0, _get3.default)(_ConnectionStreamObserver.prototype.__proto__ || (0, _getPrototypeOf2.default)(_ConnectionStreamObserver.prototype), 'onError', this).call(this, error);
        if (this._driver.onError) {
          this._driver.onError(error);
        }
        this._hasFailed = true;
      }
    }
  }]);
  return _ConnectionStreamObserver;
}(_streamObserver2.default);

/**
 * @private
 */


function sanitizeConfig(config) {
  config.maxConnectionLifetime = sanitizeIntValue(config.maxConnectionLifetime, DEFAULT_MAX_CONNECTION_LIFETIME);
  config.maxConnectionPoolSize = sanitizeIntValue(config.maxConnectionPoolSize, _poolConfig.DEFAULT_MAX_SIZE);
  config.connectionAcquisitionTimeout = sanitizeIntValue(config.connectionAcquisitionTimeout, _poolConfig.DEFAULT_ACQUISITION_TIMEOUT);
}

function sanitizeIntValue(rawValue, defaultWhenAbsent) {
  var sanitizedValue = parseInt(rawValue, 10);
  if (sanitizedValue > 0 || sanitizedValue === 0) {
    return sanitizedValue;
  } else if (sanitizedValue < 0) {
    return _maxSafeInteger2.default;
  } else {
    return defaultWhenAbsent;
  }
}

exports.Driver = Driver;
exports.READ = READ;
exports.WRITE = WRITE;
exports.default = Driver;