"use strict";
/**
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
    return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable @typescript-eslint/promise-function-async */
var util_1 = require("./internal/util");
var connection_holder_1 = require("./internal/connection-holder");
var bookmarks_1 = require("./internal/bookmarks");
var tx_config_1 = require("./internal/tx-config");
var observers_1 = require("./internal/observers");
var error_1 = require("./error");
var result_1 = __importDefault(require("./result"));
/**
 * Represents a transaction in the Neo4j database.
 *
 * @access public
 */
var Transaction = /** @class */ (function () {
    /**
     * @constructor
     * @param {object} args
     * @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from.
     * @param {function()} args.onClose - Function to be called when transaction is committed or rolled back.
     * @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced.
     * @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the conneciton
     * is not yet released.
     * @param {boolean} args.reactive whether this transaction generates reactive streams
     * @param {number} args.fetchSize - the record fetch size in each pulling batch.
     * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session.
     * @param {number} args.highRecordWatermark - The high watermark for the record buffer.
     * @param {number} args.lowRecordWatermark - The low watermark for the record buffer.
     * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction.
     * @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry
     */
    function Transaction(_a) {
        var connectionHolder = _a.connectionHolder, onClose = _a.onClose, onBookmarks = _a.onBookmarks, onConnection = _a.onConnection, reactive = _a.reactive, fetchSize = _a.fetchSize, impersonatedUser = _a.impersonatedUser, highRecordWatermark = _a.highRecordWatermark, lowRecordWatermark = _a.lowRecordWatermark, notificationFilter = _a.notificationFilter, apiTelemetryConfig = _a.apiTelemetryConfig;
        var _this = this;
        this._connectionHolder = connectionHolder;
        this._reactive = reactive;
        this._state = _states.ACTIVE;
        this._onClose = onClose;
        this._onBookmarks = onBookmarks;
        this._onConnection = onConnection;
        this._onError = this._onErrorCallback.bind(this);
        this._fetchSize = fetchSize;
        this._onComplete = this._onCompleteCallback.bind(this);
        this._results = [];
        this._impersonatedUser = impersonatedUser;
        this._lowRecordWatermak = lowRecordWatermark;
        this._highRecordWatermark = highRecordWatermark;
        this._bookmarks = bookmarks_1.Bookmarks.empty();
        this._notificationFilter = notificationFilter;
        this._apiTelemetryConfig = apiTelemetryConfig;
        this._acceptActive = function () { }; // satisfy DenoJS
        this._activePromise = new Promise(function (resolve, reject) {
            _this._acceptActive = resolve;
        });
    }
    /**
     * @private
     * @param {Bookmarks | string |  string []} bookmarks
     * @param {TxConfig} txConfig
     * @param {Object} events List of observers to events
     * @returns {void}
     */
    Transaction.prototype._begin = function (getBookmarks, txConfig, events) {
        var _this = this;
        this._connectionHolder
            .getConnection()
            .then(function (connection) { return __awaiter(_this, void 0, void 0, function () {
            var _a;
            var _this = this;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        this._onConnection();
                        if (!(connection != null)) return [3 /*break*/, 2];
                        _a = this;
                        return [4 /*yield*/, getBookmarks()];
                    case 1:
                        _a._bookmarks = _b.sent();
                        return [2 /*return*/, connection.beginTransaction({
                                bookmarks: this._bookmarks,
                                txConfig: txConfig,
                                mode: this._connectionHolder.mode(),
                                database: this._connectionHolder.database(),
                                impersonatedUser: this._impersonatedUser,
                                notificationFilter: this._notificationFilter,
                                apiTelemetryConfig: this._apiTelemetryConfig,
                                beforeError: function (error) {
                                    if (events != null) {
                                        events.onError(error);
                                    }
                                    _this._onError(error).catch(function () { });
                                },
                                afterComplete: function (metadata) {
                                    if (events != null) {
                                        events.onComplete(metadata);
                                    }
                                    if (metadata.db !== undefined && ((events === null || events === void 0 ? void 0 : events.onDB) != null)) {
                                        events.onDB(metadata.db);
                                    }
                                    _this._onComplete(metadata);
                                }
                            })];
                    case 2: throw (0, error_1.newError)('No connection available');
                }
            });
        }); })
            .catch(function (error) {
            if (events != null) {
                events.onError(error);
            }
            _this._onError(error).catch(function () { });
        })
            // It should make the transaction active anyway
            // further errors will be treated by the existing
            // observers
            .finally(function () { return _this._acceptActive(); });
    };
    /**
     * Run Cypher query
     * Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}`
     * or with the query and parameters as separate arguments.
     * @param {mixed} query - Cypher query to execute
     * @param {Object} parameters - Map with parameters to use in query
     * @return {Result} New Result
     */
    Transaction.prototype.run = function (query, parameters) {
        var _a = (0, util_1.validateQueryAndParameters)(query, parameters), validatedQuery = _a.validatedQuery, params = _a.params;
        var result = this._state.run(validatedQuery, params, {
            connectionHolder: this._connectionHolder,
            onError: this._onError,
            onComplete: this._onComplete,
            onConnection: this._onConnection,
            reactive: this._reactive,
            fetchSize: this._fetchSize,
            highRecordWatermark: this._highRecordWatermark,
            lowRecordWatermark: this._lowRecordWatermak,
            preparationJob: this._activePromise
        });
        this._results.push(result);
        return result;
    };
    /**
     * Commits the transaction and returns the result.
     *
     * After committing the transaction can no longer be used.
     *
     * @returns {Promise<void>} An empty promise if committed successfully or error if any error happened during commit.
     */
    Transaction.prototype.commit = function () {
        var _this = this;
        var committed = this._state.commit({
            connectionHolder: this._connectionHolder,
            onError: this._onError,
            onComplete: function (meta) { return _this._onCompleteCallback(meta, _this._bookmarks); },
            onConnection: this._onConnection,
            pendingResults: this._results,
            preparationJob: this._activePromise
        });
        this._state = committed.state;
        // clean up
        this._onClose();
        return new Promise(function (resolve, reject) {
            committed.result.subscribe({
                onCompleted: function () { return resolve(); },
                onError: function (error) { return reject(error); }
            });
        });
    };
    /**
     * Rollbacks the transaction.
     *
     * After rolling back, the transaction can no longer be used.
     *
     * @returns {Promise<void>} An empty promise if rolled back successfully or error if any error happened during
     * rollback.
     */
    Transaction.prototype.rollback = function () {
        var rolledback = this._state.rollback({
            connectionHolder: this._connectionHolder,
            onError: this._onError,
            onComplete: this._onComplete,
            onConnection: this._onConnection,
            pendingResults: this._results,
            preparationJob: this._activePromise
        });
        this._state = rolledback.state;
        // clean up
        this._onClose();
        return new Promise(function (resolve, reject) {
            rolledback.result.subscribe({
                onCompleted: function () { return resolve(); },
                onError: function (error) { return reject(error); }
            });
        });
    };
    /**
     * Check if this transaction is active, which means commit and rollback did not happen.
     * @return {boolean} `true` when not committed and not rolled back, `false` otherwise.
     */
    Transaction.prototype.isOpen = function () {
        return this._state === _states.ACTIVE;
    };
    /**
     * Closes the transaction
     *
     * This method will roll back the transaction if it is not already committed or rolled back.
     *
     * @returns {Promise<void>} An empty promise if closed successfully or error if any error happened during
     */
    Transaction.prototype.close = function () {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!this.isOpen()) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.rollback()];
                    case 1:
                        _a.sent();
                        _a.label = 2;
                    case 2: return [2 /*return*/];
                }
            });
        });
    };
    // eslint-disable-next-line
    // @ts-ignore
    Transaction.prototype[Symbol.asyncDispose] = function () {
        return this.close();
    };
    Transaction.prototype._onErrorCallback = function (error) {
        // error will be "acknowledged" by sending a RESET message
        // database will then forget about this transaction and cleanup all corresponding resources
        // it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it
        if (this._state === _states.FAILED) {
            // already failed, nothing to do
            // if we call onError for each result again, we might run into an infinite loop, that causes an OOM eventually
            return Promise.resolve(null);
        }
        this._state = _states.FAILED;
        this._onClose();
        this._results.forEach(function (result) {
            if (result.isOpen()) {
                // @ts-expect-error
                result._streamObserverPromise
                    .then(function (resultStreamObserver) { return resultStreamObserver.onError(error); })
                    // Nothing to do since we don't have a observer to notify the error
                    // the result will be already broke in other ways.
                    .catch(function (_) { });
            }
        });
        // release connection back to the pool
        return this._connectionHolder.releaseConnection();
    };
    /**
     * @private
     * @param {object} meta The meta with bookmarks
     * @returns {void}
     */
    Transaction.prototype._onCompleteCallback = function (meta, previousBookmarks) {
        this._onBookmarks(new bookmarks_1.Bookmarks(meta === null || meta === void 0 ? void 0 : meta.bookmark), previousBookmarks !== null && previousBookmarks !== void 0 ? previousBookmarks : bookmarks_1.Bookmarks.empty(), meta === null || meta === void 0 ? void 0 : meta.db);
    };
    return Transaction;
}());
var _states = {
    // The transaction is running with no explicit success or failure marked
    ACTIVE: {
        commit: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, pendingResults = _a.pendingResults, preparationJob = _a.preparationJob;
            return {
                result: finishTransaction(true, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob),
                state: _states.SUCCEEDED
            };
        },
        rollback: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, pendingResults = _a.pendingResults, preparationJob = _a.preparationJob;
            return {
                result: finishTransaction(false, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob),
                state: _states.ROLLED_BACK
            };
        },
        run: function (query, parameters, _a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete, onConnection = _a.onConnection, reactive = _a.reactive, fetchSize = _a.fetchSize, highRecordWatermark = _a.highRecordWatermark, lowRecordWatermark = _a.lowRecordWatermark, preparationJob = _a.preparationJob;
            // RUN in explicit transaction can't contain bookmarks and transaction configuration
            // No need to include mode and database name as it shall be included in begin
            var requirements = preparationJob !== null && preparationJob !== void 0 ? preparationJob : Promise.resolve();
            var observerPromise = connectionHolder.getConnection()
                .then(function (conn) { return requirements.then(function () { return conn; }); })
                .then(function (conn) {
                onConnection();
                if (conn != null) {
                    return conn.run(query, parameters, {
                        bookmarks: bookmarks_1.Bookmarks.empty(),
                        txConfig: tx_config_1.TxConfig.empty(),
                        beforeError: onError,
                        afterComplete: onComplete,
                        reactive: reactive,
                        fetchSize: fetchSize,
                        highRecordWatermark: highRecordWatermark,
                        lowRecordWatermark: lowRecordWatermark
                    });
                }
                else {
                    throw (0, error_1.newError)('No connection available');
                }
            })
                .catch(function (error) { return new observers_1.FailedObserver({ error: error, onError: onError }); });
            return newCompletedResult(observerPromise, query, parameters, connectionHolder, highRecordWatermark, lowRecordWatermark);
        }
    },
    // An error has occurred, transaction can no longer be used and no more messages will
    // be sent for this transaction.
    FAILED: {
        commit: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.FailedObserver({
                    error: (0, error_1.newError)('Cannot commit this transaction, because it has been rolled back either because of an error or explicit termination.'),
                    onError: onError
                }), 'COMMIT', {}, connectionHolder, 0, // high watermark
                0 // low watermark
                ),
                state: _states.FAILED
            };
        },
        rollback: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.CompletedObserver(), 'ROLLBACK', {}, connectionHolder, 0, // high watermark
                0 // low watermark
                ),
                state: _states.FAILED
            };
        },
        run: function (query, parameters, _a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return newCompletedResult(new observers_1.FailedObserver({
                error: (0, error_1.newError)('Cannot run query in this transaction, because it has been rolled back either because of an error or explicit termination.'),
                onError: onError
            }), query, parameters, connectionHolder, 0, // high watermark
            0 // low watermark
            );
        }
    },
    // This transaction has successfully committed
    SUCCEEDED: {
        commit: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.FailedObserver({
                    error: (0, error_1.newError)('Cannot commit this transaction, because it has already been committed.'),
                    onError: onError
                }), 'COMMIT', {}, connection_holder_1.EMPTY_CONNECTION_HOLDER, 0, // high watermark
                0 // low watermark
                ),
                state: _states.SUCCEEDED,
                connectionHolder: connectionHolder
            };
        },
        rollback: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.FailedObserver({
                    error: (0, error_1.newError)('Cannot rollback this transaction, because it has already been committed.'),
                    onError: onError
                }), 'ROLLBACK', {}, connection_holder_1.EMPTY_CONNECTION_HOLDER, 0, // high watermark
                0 // low watermark
                ),
                state: _states.SUCCEEDED,
                connectionHolder: connectionHolder
            };
        },
        run: function (query, parameters, _a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return newCompletedResult(new observers_1.FailedObserver({
                error: (0, error_1.newError)('Cannot run query in this transaction, because it has already been committed.'),
                onError: onError
            }), query, parameters, connectionHolder, 0, // high watermark
            0 // low watermark
            );
        }
    },
    // This transaction has been rolled back
    ROLLED_BACK: {
        commit: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.FailedObserver({
                    error: (0, error_1.newError)('Cannot commit this transaction, because it has already been rolled back.'),
                    onError: onError
                }), 'COMMIT', {}, connectionHolder, 0, // high watermark
                0 // low watermark
                ),
                state: _states.ROLLED_BACK
            };
        },
        rollback: function (_a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return {
                result: newCompletedResult(new observers_1.FailedObserver({
                    error: (0, error_1.newError)('Cannot rollback this transaction, because it has already been rolled back.')
                }), 'ROLLBACK', {}, connectionHolder, 0, // high watermark
                0 // low watermark
                ),
                state: _states.ROLLED_BACK
            };
        },
        run: function (query, parameters, _a) {
            var connectionHolder = _a.connectionHolder, onError = _a.onError, onComplete = _a.onComplete;
            return newCompletedResult(new observers_1.FailedObserver({
                error: (0, error_1.newError)('Cannot run query in this transaction, because it has already been rolled back.'),
                onError: onError
            }), query, parameters, connectionHolder, 0, // high watermark
            0 // low watermark
            );
        }
    }
};
/**
 *
 * @param {boolean} commit
 * @param {ConnectionHolder} connectionHolder
 * @param {function(err:Error): any} onError
 * @param {function(metadata:object): any} onComplete
 * @param {function() : any} onConnection
 * @param {list<Result>>}pendingResults all run results in this transaction
 */
function finishTransaction(commit, connectionHolder, onError, onComplete, onConnection, pendingResults, preparationJob) {
    var requirements = preparationJob !== null && preparationJob !== void 0 ? preparationJob : Promise.resolve();
    var observerPromise = connectionHolder.getConnection()
        .then(function (conn) { return requirements.then(function () { return conn; }); })
        .then(function (connection) {
        onConnection();
        pendingResults.forEach(function (r) { return r._cancel(); });
        return Promise.all(pendingResults.map(function (result) { return result.summary(); })).then(function (results) {
            if (connection != null) {
                if (commit) {
                    return connection.commitTransaction({
                        beforeError: onError,
                        afterComplete: onComplete
                    });
                }
                else {
                    return connection.rollbackTransaction({
                        beforeError: onError,
                        afterComplete: onComplete
                    });
                }
            }
            else {
                throw (0, error_1.newError)('No connection available');
            }
        });
    })
        .catch(function (error) { return new observers_1.FailedObserver({ error: error, onError: onError }); });
    // for commit & rollback we need result that uses real connection holder and notifies it when
    // connection is not needed and can be safely released to the pool
    return new result_1.default(observerPromise, commit ? 'COMMIT' : 'ROLLBACK', {}, connectionHolder, {
        high: Number.MAX_VALUE,
        low: Number.MAX_VALUE
    });
}
/**
 * Creates a {@link Result} with empty connection holder.
 * For cases when result represents an intermediate or failed action, does not require any metadata and does not
 * need to influence real connection holder to release connections.
 * @param {ResultStreamObserver} observer - an observer for the created result.
 * @param {string} query - the cypher query that produced the result.
 * @param {Object} parameters - the parameters for cypher query that produced the result.
 * @param {ConnectionHolder} connectionHolder - the connection holder used to get the result
 * @return {Result} new result.
 * @private
 */
function newCompletedResult(observerPromise, query, parameters, connectionHolder, highRecordWatermark, lowRecordWatermark) {
    if (connectionHolder === void 0) { connectionHolder = connection_holder_1.EMPTY_CONNECTION_HOLDER; }
    return new result_1.default(Promise.resolve(observerPromise), query, parameters, new connection_holder_1.ReadOnlyConnectionHolder(connectionHolder !== null && connectionHolder !== void 0 ? connectionHolder : connection_holder_1.EMPTY_CONNECTION_HOLDER), {
        low: lowRecordWatermark,
        high: highRecordWatermark
    });
}
exports.default = Transaction;
