"use strict";
/**
 * @copyright (c) Microsoft Corporation. All rights reserved.
 */
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var sp_core_library_1 = require("@microsoft/sp-core-library");
var BatchedRequest_1 = tslib_1.__importDefault(require("./BatchedRequest"));
var SPHttpClientResponse_1 = tslib_1.__importDefault(require("./SPHttpClientResponse"));
var FetchProvider_1 = require("../httpClient/FetchProvider");
var DigestCache_1 = tslib_1.__importDefault(require("./DigestCache"));
var SPHttpClientHelper_1 = tslib_1.__importDefault(require("./SPHttpClientHelper"));
var ODataVersion_1 = tslib_1.__importDefault(require("./ODataVersion"));
var SPHttpClientBatchConfiguration_1 = require("./SPHttpClientBatchConfiguration");
/**
 * The SPHttpClientBatch class accumulates a number of REST service calls and
 * transmits them as a single ODATA batch.  This protocol is documented here:
 * http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html
 *
 * The usage is to call SPHttpClientBatch.fetch() to queue each individual request,
 * and then call SPHttpClientBatch.execute() to execute the batch operation.
 * The execute() method returns a promise that resolves when the real REST
 * call has completed.  Each call to fetch() also returns a promise that will
 * resolve with an SPHttpClientResponse object for that particular request.
 *
 * @privateRemarks
 * The type signature of SPHttpClientBatch class suggests that it should inherit from
 * the HttpClient base class.  However, the operational semantics are different
 * (e.g. nothing happens until execute() is called; further operations are
 * prohibited afterwards; fetch() calls cannot depend on each other).  In the
 * future we might introduce a base class for batches, but it would be separate
 * from the HttpClient hierarchy.  By contrast, the ISPHttpClientBatchOptions
 * does naturally inherit from IHttpClientOptions.
 *
 * @beta
 */
var SPHttpClientBatch = /** @class */ (function () {
    /**
     * Generally third parties should not use this constructor.
     * The recommended way to start a batch is by calling SPHttpClient.beginBatch().
     * @internal
     */
    function SPHttpClientBatch(serviceScope, batchCreationOptions) {
        var _this = this;
        this._batchedRequests = [];
        serviceScope.whenFinished(function () {
            _this._fetchProvider = serviceScope.consume(FetchProvider_1.fetchProviderServiceKey);
            _this._randomNumberGenerator = serviceScope.consume(sp_core_library_1.RandomNumberGenerator.serviceKey);
            _this._digestCache = serviceScope.consume(DigestCache_1.default.serviceKey);
        });
    }
    /**
     * Queues a new request, and returns a promise that can be used to access
     * the server response (after execute() has completed).
     *
     * @remarks
     * The parameters for this function are basically the same as the WHATWG API standard
     * documented here:
     *
     * {@link https://fetch.spec.whatwg.org/ }
     *
     * However, be aware that certain REST headers are ignored or not allowed inside
     * a batch.  See the ODATA documentation for details.
     *
     * When execute() is called, it will POST to a URL such as
     * "http://example.com/sites/sample/_api/$batch".  Typically SPHttpClientBatch can successfully
     * guess the appropriate SPWeb URL by looking for a reserved URL segment such as "_api"
     * in the first URL passed to fetch().  If not, use ISPHttpClientBatchCreationOptions.webUrl to specify it
     * explicitly.
     *
     * @param url - the URL to fetch
     * @param configuration - determines the default behavior of this request; normally this should
     *   be the latest version number from SPHttpClientBatchConfigurations
     * @param options - additional options that affect the request
     *
     * @returns A promise with behavior similar to WHATWG fetch().  This promise will resolve normally
     * (with {@link HttpClientResponse.ok} being false) for error status codes such as HTTP 404
     * or 500.  The promise will only reject for network failures or other errors that prevent communication
     * with the server.
     */
    SPHttpClientBatch.prototype.fetch = function (url, configuration, options) {
        if (!options) {
            options = {};
        }
        // Apply most of the same default headers as SPHttpClient
        var modifiedOptions = Object.assign({}, options);
        modifiedOptions.headers = SPHttpClientHelper_1.default.cloneHeaders(options);
        SPHttpClientHelper_1.default.applyDefaultHeaders(modifiedOptions.headers, configuration, options, ODataVersion_1.default.v4);
        var request = new Request(url, modifiedOptions);
        var requestUrl = request.url;
        if (!this._webUrl) {
            // If the webUrl wasn't provided to the constructor, then we infer it from
            // the first request.
            this._webUrl = SPHttpClientHelper_1.default.getWebUrlFromRequestUrl(requestUrl);
        }
        var batchedRequest = new BatchedRequest_1.default(request);
        this._batchedRequests.push(batchedRequest);
        return batchedRequest.promise;
    };
    /**
     * Calls fetch(), but sets the method to 'GET'.
     * @param url - the URL to fetch
     * @param configuration - determines the default behavior of this request; normally this should
     *   be the latest version number from SPHttpClientBatchConfigurations
     * @param options - additional options that affect the request
     * @returns A promise with behavior similar to WHATWG fetch().  This promise will resolve normally
     * (with {@link HttpClientResponse.ok} being false) for error status codes such as HTTP 404
     * or 500.  The promise will only reject for network failures or other errors that prevent communication
     * with the server.
     */
    SPHttpClientBatch.prototype.get = function (url, configuration, options) {
        var modifiedOptions = Object.assign({}, options);
        modifiedOptions.method = 'GET';
        return this.fetch(url, configuration, modifiedOptions);
    };
    /**
     * Calls fetch(), but sets the method to 'POST'.
     * @param url - the URL to fetch
     * @param configuration - determines the default behavior of this request; normally this should
     *   be the latest version number from SPHttpClientBatchConfigurations
     * @param options - additional options that affect the request
     * @returns A promise with behavior similar to WHATWG fetch().  This promise will resolve normally
     * (with {@link HttpClientResponse.ok} being false) for error status codes such as HTTP 404
     * or 500.  The promise will only reject for network failures or other errors that prevent communication
     * with the server.
     */
    SPHttpClientBatch.prototype.post = function (url, configuration, options) {
        var modifiedOptions = Object.assign({}, options);
        modifiedOptions.method = 'POST';
        return this.fetch(url, configuration, modifiedOptions);
    };
    /**
     * Executes the batched queries that were queued using SPHttpClientBatch.fetch().
     */
    SPHttpClientBatch.prototype.execute = function () {
        var _this = this;
        this._correlationId = undefined;
        this._batchResponseBody = undefined;
        var batchBody = '\n';
        var batchGuid = sp_core_library_1.Guid.newGuid(this._randomNumberGenerator);
        // In the loop below, we will build up a chain of promises that append
        // each request to the batch body, and then the "return" statement will
        // add the final processing to the chain.
        var chain = Promise.resolve();
        this._batchedRequests.forEach(function (batchedRequest) {
            chain = chain
                .then(function () {
                return batchedRequest.request.text();
            })
                .then(function (requestText) {
                var request = batchedRequest.request;
                batchBody += "--batch_" + batchGuid.toString() + "\n";
                batchBody += "Content-type: application/http\n" + "Content-Transfer-Encoding: binary\n\n";
                // Example:
                // "POST https://example.com/_api/web HTTP/1.1"
                batchBody += request.method + " " + request.url + " HTTP/1.1\n";
                request.headers.forEach(function (value, name) {
                    batchBody += name + ": " + value + "\n";
                });
                batchBody += '\n';
                batchBody += requestText;
                batchBody += '\n\n';
            });
        });
        return chain
            .then(function () {
            // Close the batch
            // This is a workaround to add an extra line to prevent batch.post from failure.
            // @see https://github.com/SharePoint/sp-dev-docs/issues/599
            batchBody += "--batch_" + batchGuid + "--\n\n";
            return _this._digestCache.fetchDigest(_this._webUrl);
        })
            .then(function (digest) {
            // Start the $batch request
            var headers = new Headers();
            headers.append('Accept', 'application/json');
            headers.append('OData-Version', '4.0');
            headers.append('Content-Type', "multipart/mixed; boundary=batch_" + batchGuid);
            headers.append('Content-Length', batchBody.length.toString());
            headers.append('X-RequestDigest', digest);
            var request = new Request(_this._webUrl + '/_api/$batch', {
                method: 'POST',
                headers: headers,
                body: batchBody,
                credentials: 'same-origin',
                cache: 'no-cache'
            });
            var promise = _this._fetchProvider.fetch(request);
            return SPHttpClientHelper_1.default.handleAuthenticationErrors(promise, _this._webUrl, _this._digestCache);
        })
            .then(function (response) {
            _this._correlationId = response.headers.get('sprequestguid') || undefined;
            if (!response.ok) {
                var useStatusKillSwitch = !sp_core_library_1._SPKillSwitch.isActivated('3a61beed-a088-4f18-a950-f41aa36b9486'
                /* '04/22/2021', 'Return the status instead of status text' */
                );
                throw new Error('SPHttpClientBatch: The batch request failed: ' +
                    (useStatusKillSwitch ? response.status.toString() : response.statusText));
            }
            return response.text();
        })
            .then(function (text) {
            _this._batchResponseBody = text;
            var responses = _this._parseResponsesFromBody(text);
            if (responses.length !== _this._batchedRequests.length) {
                throw new Error("SPHttpClientBatch: Expecting " + _this._batchedRequests.length + " responses");
            }
            for (var i = 0; i < _this._batchedRequests.length; ++i) {
                var request = _this._batchedRequests[i];
                request.resolvePromise(new SPHttpClientResponse_1.default(responses[i]));
            }
            return _this;
        })
            .catch(function (executeError) {
            // If execute() itself rejects, then we still need to settle the individual fetch() promises.
            // Even if the "_api/$batch" promise resolved with a Response object, we cannot use that to
            // resolve the fetch() promises, since it was a separate conversation with possibly incompatible
            // encodings.  We don't have a proper status code for the fetch() operations.
            for (var _i = 0, _a = _this._batchedRequests; _i < _a.length; _i++) {
                var request = _a[_i];
                request.rejectPromiseIfNotResolved(executeError);
            }
            // Pass the batch error along to the caller's catch block
            return Promise.reject(executeError);
        });
    };
    /**
     * Returns the correlation id of the most recent batch request if execute() was able to get a response
     * from SharePoint.
     * @internal
     */
    SPHttpClientBatch.prototype.correlationId = function () {
        return this._correlationId;
    };
    /**
     * Returns the original batch response body if execute() was able to get a response and the respones
     * status is success.
     * @internal
     */
    SPHttpClientBatch.prototype.batchResponseBody = function () {
        return this._batchResponseBody;
    };
    SPHttpClientBatch.prototype._parseResponsesFromBody = function (text) {
        var responses = [];
        var header = '--batchresponse_';
        // Ex. "HTTP/1.1 500 Internal Server Error"
        var statusRegExp = new RegExp('^HTTP/[0-9.]+ +([0-9]+) +(.*)', 'i');
        var lines = text.split('\n');
        var parserState = 'batch';
        var batchStatus;
        var batchStatusText;
        for (var i = 0; i < lines.length; ++i) {
            var line = lines[i];
            switch (parserState) {
                case 'batch':
                    if (line.substr(0, header.length) === header) {
                        parserState = 'batchHeaders';
                    }
                    else {
                        if (line.trim() !== '') {
                            throw new Error('SPHttpClientBatch: Invalid response, line ' + i);
                        }
                    }
                    break;
                case 'batchHeaders':
                    if (line.trim() === '') {
                        parserState = 'status';
                    }
                    break;
                case 'status':
                    // Ex. "HTTP/1.1 500 Internal Server Error"
                    var parts = statusRegExp.exec(line);
                    if (!parts || (parts && parts.length !== 3)) {
                        throw new Error('SPHttpClientBatch: Invalid status, line ' + i);
                    }
                    // Ex. "500"
                    batchStatus = parseInt(parts[1], 10);
                    // Ex. "Internal Server Error"
                    batchStatusText = parts[2];
                    parserState = 'statusHeaders';
                    break;
                case 'statusHeaders':
                    if (line.trim() === '') {
                        parserState = 'body';
                    }
                    break;
                case 'body':
                    var responseBody = void 0;
                    // Response with status 204, 205 and 304 which are null body status cannot have body.
                    // Otherwise, use parsed line as response body.
                    if (batchStatus !== 204 && // No content
                        batchStatus !== 205 && // Reset content
                        batchStatus !== 304 // Not modified
                    ) {
                        responseBody = line;
                    }
                    else {
                        responseBody = null; // tslint:disable-line:no-null-keyword
                    }
                    var headers = new Headers();
                    headers.append('request-id', this._correlationId || '');
                    var response = new Response(responseBody, {
                        status: batchStatus,
                        statusText: batchStatusText,
                        headers: headers
                    });
                    responses.push(response);
                    parserState = 'batch';
                    break;
            }
        }
        if (parserState !== 'status') {
            throw new Error('SPHttpClientBatch: Unexpected end of input');
        }
        return responses;
    };
    /**
     * The standard predefined SPHttpClientBatchConfigurations objects for use with
     * the SPHttpClientBatch class.
     */
    SPHttpClientBatch.configurations = SPHttpClientBatchConfiguration_1.predefinedConfigurations;
    return SPHttpClientBatch;
}());
exports.default = SPHttpClientBatch;
//# sourceMappingURL=SPHttpClientBatch.js.map