'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

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 _streamObserver = require('./internal/stream-observer');

var _streamObserver2 = _interopRequireDefault(_streamObserver);

var _result = require('./result');

var _result2 = _interopRequireDefault(_result);

var _transaction = require('./transaction');

var _transaction2 = _interopRequireDefault(_transaction);

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

var _util = require('./internal/util');

var _connectionHolder = require('./internal/connection-holder');

var _connectionHolder2 = _interopRequireDefault(_connectionHolder);

var _driver = require('./driver');

var _driver2 = _interopRequireDefault(_driver);

var _transactionExecutor = require('./internal/transaction-executor');

var _transactionExecutor2 = _interopRequireDefault(_transactionExecutor);

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

var _bookmark2 = _interopRequireDefault(_bookmark);

var _txConfig = require('./internal/tx-config');

var _txConfig2 = _interopRequireDefault(_txConfig);

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

// Typedef for JSDoc. Declares TransactionConfig type and makes it possible to use in in method-level docs.
/**
 * Configuration object containing settings for explicit and auto-commit transactions.
 * <p>
 * Configuration is supported for:
 * <ul>
 *   <li>queries executed in auto-commit transactions using {@link Session#run}</li>
 *   <li>transactions started by transaction functions using {@link Session#readTransaction} and {@link Session#writeTransaction}</li>
 *   <li>explicit transactions using {@link Session#beginTransaction}</li>
 * </ul>
 * @typedef {object} TransactionConfig
 * @property {number} timeout - the transaction timeout in **milliseconds**. Transactions that execute longer than the configured timeout will
 * be terminated by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default timeout
 * configured in the database using `dbms.transaction.timeout` setting. Value should not represent a duration of zero or negative duration.
 * @property {object} metadata - the transaction metadata. Specified metadata will be attached to the executing transaction and visible in the output of
 * `dbms.listQueries` and `dbms.listTransactions` procedures. It will also get logged to the `query.log`. This functionality makes it easier to tag
 * transactions and is equivalent to `dbms.setTXMetaData` procedure.
 */

/**
 * A Session instance is used for handling the connection and
 * sending statements through the connection.
 * @access public
 */
/**
 * 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 Session = function () {

  /**
   * @constructor
   * @param {string} mode the default access mode for this session.
   * @param {ConnectionProvider} connectionProvider - the connection provider to acquire connections from.
   * @param {Bookmark} bookmark - the initial bookmark for this session.
   * @param {Object} [config={}] - this driver configuration.
   */
  function Session(mode, connectionProvider, bookmark, config) {
    (0, _classCallCheck3.default)(this, Session);

    this._mode = mode;
    this._readConnectionHolder = new _connectionHolder2.default(_driver.READ, connectionProvider);
    this._writeConnectionHolder = new _connectionHolder2.default(_driver.WRITE, connectionProvider);
    this._open = true;
    this._hasTx = false;
    this._lastBookmark = bookmark;
    this._transactionExecutor = _createTransactionExecutor(config);
  }

  /**
   * Run Cypher statement
   * Could be called with a statement object i.e.: `{text: "MATCH ...", parameters: {param: 1}}`
   * or with the statement and parameters as separate arguments.
   * @param {mixed} statement - Cypher statement to execute
   * @param {Object} parameters - Map with parameters to use in statement
   * @param {TransactionConfig} [transactionConfig] - configuration for the new auto-commit transaction.
   * @return {Result} - New Result
   */


  (0, _createClass3.default)(Session, [{
    key: 'run',
    value: function run(statement, parameters, transactionConfig) {
      var _this = this;

      var _validateStatementAnd = (0, _util.validateStatementAndParameters)(statement, parameters),
          query = _validateStatementAnd.query,
          params = _validateStatementAnd.params;

      var autoCommitTxConfig = transactionConfig ? new _txConfig2.default(transactionConfig) : _txConfig2.default.empty();

      return this._run(query, params, function (connection, streamObserver) {
        return connection.protocol().run(query, params, _this._lastBookmark, autoCommitTxConfig, streamObserver);
      });
    }
  }, {
    key: '_run',
    value: function _run(statement, parameters, statementRunner) {
      var streamObserver = new SessionStreamObserver(this);
      var connectionHolder = this._connectionHolderWithMode(this._mode);
      if (!this._hasTx) {
        connectionHolder.initializeConnection();
        connectionHolder.getConnection(streamObserver).then(function (connection) {
          return statementRunner(connection, streamObserver);
        }).catch(function (error) {
          return streamObserver.onError(error);
        });
      } else {
        streamObserver.onError((0, _error.newError)('Statements cannot be run directly on a ' + 'session with an open transaction; either run from within the ' + 'transaction or use a different session.'));
      }
      return new _result2.default(streamObserver, statement, parameters, function () {
        return streamObserver.serverMetadata();
      }, connectionHolder);
    }

    /**
     * Begin a new transaction in this session. A session can have at most one transaction running at a time, if you
     * want to run multiple concurrent transactions, you should use multiple concurrent sessions.
     *
     * While a transaction is open the session cannot be used to run statements outside the transaction.
     *
     * @param {TransactionConfig} [transactionConfig] - configuration for the new auto-commit transaction.
     * @returns {Transaction} - New Transaction
     */

  }, {
    key: 'beginTransaction',
    value: function beginTransaction(transactionConfig) {
      // this function needs to support bookmarks parameter for backwards compatibility
      // parameter was of type {string|string[]} and represented either a single or multiple bookmarks
      // that's why we need to check parameter type and decide how to interpret the value
      var arg = transactionConfig;

      var txConfig = _txConfig2.default.empty();
      if (typeof arg === 'string' || arg instanceof String || Array.isArray(arg)) {
        // argument looks like a single or multiple bookmarks
        // bookmarks in this function are deprecated but need to be supported for backwards compatibility
        this._updateBookmark(new _bookmark2.default(arg));
      } else if (arg) {
        // argument is probably a transaction configuration
        txConfig = new _txConfig2.default(arg);
      }

      return this._beginTransaction(this._mode, txConfig);
    }
  }, {
    key: '_beginTransaction',
    value: function _beginTransaction(accessMode, txConfig) {
      if (this._hasTx) {
        throw (0, _error.newError)('You cannot begin a transaction on a session with an open transaction; ' + 'either run from within the transaction or use a different session.');
      }

      var mode = _driver2.default._validateSessionMode(accessMode);
      var connectionHolder = this._connectionHolderWithMode(mode);
      connectionHolder.initializeConnection();
      this._hasTx = true;

      var tx = new _transaction2.default(connectionHolder, this._transactionClosed.bind(this), this._updateBookmark.bind(this));
      tx._begin(this._lastBookmark, txConfig);
      return tx;
    }
  }, {
    key: '_transactionClosed',
    value: function _transactionClosed() {
      this._hasTx = false;
    }

    /**
     * Return the bookmark received following the last completed {@link Transaction}.
     *
     * @return {string|null} a reference to a previous transaction
     */

  }, {
    key: 'lastBookmark',
    value: function lastBookmark() {
      return this._lastBookmark.maxBookmarkAsString();
    }

    /**
     * Execute given unit of work in a {@link READ} transaction.
     *
     * Transaction will automatically be committed unless the given function throws or returns a rejected promise.
     * Some failures of the given function or the commit itself will be retried with exponential backoff with initial
     * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
     * `maxTransactionRetryTime` property in milliseconds.
     *
     * @param {function(tx: Transaction): Promise} transactionWork - callback that executes operations against
     * a given {@link Transaction}.
     * @param {TransactionConfig} [transactionConfig] - configuration for all transactions started to execute the unit of work.
     * @return {Promise} resolved promise as returned by the given function or rejected promise when given
     * function or commit fails.
     */

  }, {
    key: 'readTransaction',
    value: function readTransaction(transactionWork, transactionConfig) {
      var config = new _txConfig2.default(transactionConfig);
      return this._runTransaction(_driver.READ, config, transactionWork);
    }

    /**
     * Execute given unit of work in a {@link WRITE} transaction.
     *
     * Transaction will automatically be committed unless the given function throws or returns a rejected promise.
     * Some failures of the given function or the commit itself will be retried with exponential backoff with initial
     * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's
     * `maxTransactionRetryTime` property in milliseconds.
     *
     * @param {function(tx: Transaction): Promise} transactionWork - callback that executes operations against
     * a given {@link Transaction}.
     * @param {TransactionConfig} [transactionConfig] - configuration for all transactions started to execute the unit of work.
     * @return {Promise} resolved promise as returned by the given function or rejected promise when given
     * function or commit fails.
     */

  }, {
    key: 'writeTransaction',
    value: function writeTransaction(transactionWork, transactionConfig) {
      var config = new _txConfig2.default(transactionConfig);
      return this._runTransaction(_driver.WRITE, config, transactionWork);
    }
  }, {
    key: '_runTransaction',
    value: function _runTransaction(accessMode, transactionConfig, transactionWork) {
      var _this2 = this;

      return this._transactionExecutor.execute(function () {
        return _this2._beginTransaction(accessMode, transactionConfig);
      }, transactionWork);
    }

    /**
     * Update value of the last bookmark.
     * @param {Bookmark} newBookmark the new bookmark.
     */

  }, {
    key: '_updateBookmark',
    value: function _updateBookmark(newBookmark) {
      if (newBookmark && !newBookmark.isEmpty()) {
        this._lastBookmark = newBookmark;
      }
    }

    /**
     * Close this session.
     * @param {function()} callback - Function to be called after the session has been closed
     * @return
     */

  }, {
    key: 'close',
    value: function close() {
      var _this3 = this;

      var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {
        return null;
      };

      if (this._open) {
        this._open = false;
        this._transactionExecutor.close();
        this._readConnectionHolder.close().then(function () {
          _this3._writeConnectionHolder.close().then(function () {
            callback();
          });
        });
      } else {
        callback();
      }
    }
  }, {
    key: '_connectionHolderWithMode',
    value: function _connectionHolderWithMode(mode) {
      if (mode === _driver.READ) {
        return this._readConnectionHolder;
      } else if (mode === _driver.WRITE) {
        return this._writeConnectionHolder;
      } else {
        throw (0, _error.newError)('Unknown access mode: ' + mode);
      }
    }
  }]);
  return Session;
}();

/**
 * @private
 */


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

  function SessionStreamObserver(session) {
    (0, _classCallCheck3.default)(this, SessionStreamObserver);

    var _this4 = (0, _possibleConstructorReturn3.default)(this, (SessionStreamObserver.__proto__ || (0, _getPrototypeOf2.default)(SessionStreamObserver)).call(this));

    _this4._session = session;
    return _this4;
  }

  (0, _createClass3.default)(SessionStreamObserver, [{
    key: 'onCompleted',
    value: function onCompleted(meta) {
      (0, _get3.default)(SessionStreamObserver.prototype.__proto__ || (0, _getPrototypeOf2.default)(SessionStreamObserver.prototype), 'onCompleted', this).call(this, meta);
      var bookmark = new _bookmark2.default(meta.bookmark);
      this._session._updateBookmark(bookmark);
    }
  }]);
  return SessionStreamObserver;
}(_streamObserver2.default);

function _createTransactionExecutor(config) {
  var maxRetryTimeMs = config && config.maxTransactionRetryTime ? config.maxTransactionRetryTime : null;
  return new _transactionExecutor2.default(maxRetryTimeMs);
}

exports.default = Session;