"use strict";
/**
 * @license
 * Copyright 2018 Google LLC. All Rights Reserved.
 * 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) {
    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) : new P(function (resolve) { resolve(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;
    return g = { next: verb(0), "throw": verb(1), "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 (_) 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 };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * IOHandler implementations based on HTTP requests in the web browser.
 *
 * Uses [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
 */
var util_1 = require("../util");
var io_utils_1 = require("./io_utils");
var router_registry_1 = require("./router_registry");
var weights_loader_1 = require("./weights_loader");
var OCTET_STREAM_MIME_TYPE = 'application/octet-stream';
var JSON_TYPE = 'application/json';
var BrowserHTTPRequest = /** @class */ (function () {
    function BrowserHTTPRequest(path, requestInit, weightPathPrefix, fetchFunc, onProgress) {
        this.weightPathPrefix = weightPathPrefix;
        this.onProgress = onProgress;
        this.DEFAULT_METHOD = 'POST';
        if (fetchFunc == null) {
            if (typeof fetch === 'undefined') {
                throw new Error('browserHTTPRequest is not supported outside the web browser ' +
                    'without a fetch polyfill.');
            }
            // Make sure fetch is always bound to window (the
            // original object) when available.
            fetchFunc = fetch.bind(typeof window === 'undefined' ? null : window);
        }
        else {
            util_1.assert(typeof fetchFunc === 'function', function () { return 'Must pass a function that matches the signature of ' +
                '`fetch` (see ' +
                'https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)'; });
        }
        this.fetchFunc = function (path, requestInits) {
            // tslint:disable-next-line:no-any
            return fetchFunc(path, requestInits).catch(function (error) {
                throw new Error("Request for " + path + " failed due to error: " + error);
            });
        };
        util_1.assert(path != null && path.length > 0, function () {
            return 'URL path for browserHTTPRequest must not be null, undefined or ' +
                'empty.';
        });
        if (Array.isArray(path)) {
            util_1.assert(path.length === 2, function () { return 'URL paths for browserHTTPRequest must have a length of 2, ' +
                ("(actual length is " + path.length + ")."); });
        }
        this.path = path;
        if (requestInit != null && requestInit.body != null) {
            throw new Error('requestInit is expected to have no pre-existing body, but has one.');
        }
        this.requestInit = requestInit || {};
    }
    BrowserHTTPRequest.prototype.save = function (modelArtifacts) {
        return __awaiter(this, void 0, void 0, function () {
            var init, weightsManifest, modelTopologyAndWeightManifest, response;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (modelArtifacts.modelTopology instanceof ArrayBuffer) {
                            throw new Error('BrowserHTTPRequest.save() does not support saving model topology ' +
                                'in binary formats yet.');
                        }
                        init = Object.assign({ method: this.DEFAULT_METHOD }, this.requestInit);
                        init.body = new FormData();
                        weightsManifest = [{
                                paths: ['./model.weights.bin'],
                                weights: modelArtifacts.weightSpecs,
                            }];
                        modelTopologyAndWeightManifest = {
                            modelTopology: modelArtifacts.modelTopology,
                            weightsManifest: weightsManifest
                        };
                        init.body.append('model.json', new Blob([JSON.stringify(modelTopologyAndWeightManifest)], { type: JSON_TYPE }), 'model.json');
                        if (modelArtifacts.weightData != null) {
                            init.body.append('model.weights.bin', new Blob([modelArtifacts.weightData], { type: OCTET_STREAM_MIME_TYPE }), 'model.weights.bin');
                        }
                        return [4 /*yield*/, this.getFetchFunc()(this.path, init)];
                    case 1:
                        response = _a.sent();
                        if (response.ok) {
                            return [2 /*return*/, {
                                    modelArtifactsInfo: io_utils_1.getModelArtifactsInfoForJSON(modelArtifacts),
                                    responses: [response],
                                }];
                        }
                        else {
                            throw new Error("BrowserHTTPRequest.save() failed due to HTTP response status " +
                                (response.status + "."));
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Load model artifacts via HTTP request(s).
     *
     * See the documentation to `browserHTTPRequest` for details on the saved
     * artifacts.
     *
     * @returns The loaded model artifacts (if loading succeeds).
     */
    BrowserHTTPRequest.prototype.load = function () {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                return [2 /*return*/, Array.isArray(this.path) ? this.loadBinaryModel() :
                        this.loadJSONModel()];
            });
        });
    };
    /**
     * Loads the model topology file and build the in memory graph of the model.
     */
    BrowserHTTPRequest.prototype.loadBinaryTopology = function () {
        return __awaiter(this, void 0, void 0, function () {
            var response;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.getFetchFunc()(this.path[0], this.requestInit)];
                    case 1:
                        response = _a.sent();
                        if (!response.ok) {
                            throw new Error("Request to " + this.path[0] + " failed with error: " + response.statusText);
                        }
                        return [4 /*yield*/, response.arrayBuffer()];
                    case 2: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    BrowserHTTPRequest.prototype.loadBinaryModel = function () {
        return __awaiter(this, void 0, void 0, function () {
            var graphPromise, manifestPromise, results, modelTopology, weightsManifestResponse, weightsManifest, weightSpecs, weightData, results_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        graphPromise = this.loadBinaryTopology();
                        return [4 /*yield*/, this.getFetchFunc()(this.path[1], this.requestInit)];
                    case 1:
                        manifestPromise = _a.sent();
                        if (!manifestPromise.ok) {
                            throw new Error("Request to " + this.path[1] + " failed with error: " + manifestPromise.statusText);
                        }
                        return [4 /*yield*/, Promise.all([graphPromise, manifestPromise])];
                    case 2:
                        results = _a.sent();
                        modelTopology = results[0], weightsManifestResponse = results[1];
                        return [4 /*yield*/, weightsManifestResponse.json()];
                    case 3:
                        weightsManifest = _a.sent();
                        if (!(weightsManifest != null)) return [3 /*break*/, 5];
                        return [4 /*yield*/, this.loadWeights(weightsManifest)];
                    case 4:
                        results_1 = _a.sent();
                        weightSpecs = results_1[0], weightData = results_1[1];
                        _a.label = 5;
                    case 5: return [2 /*return*/, { modelTopology: modelTopology, weightSpecs: weightSpecs, weightData: weightData }];
                }
            });
        });
    };
    BrowserHTTPRequest.prototype.loadJSONModel = function () {
        return __awaiter(this, void 0, void 0, function () {
            var modelConfigRequest, modelConfig, modelTopology, weightsManifest, weightSpecs, weightData, weightsManifest_1, results;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.getFetchFunc()(this.path, this.requestInit)];
                    case 1:
                        modelConfigRequest = _a.sent();
                        if (!modelConfigRequest.ok) {
                            throw new Error("Request to " + this.path + " failed with error: " + modelConfigRequest.statusText);
                        }
                        return [4 /*yield*/, modelConfigRequest.json()];
                    case 2:
                        modelConfig = _a.sent();
                        modelTopology = modelConfig['modelTopology'];
                        weightsManifest = modelConfig['weightsManifest'];
                        // We do not allow both modelTopology and weightsManifest to be missing.
                        if (modelTopology == null && weightsManifest == null) {
                            throw new Error("The JSON from HTTP path " + this.path + " contains neither model " +
                                "topology or manifest for weights.");
                        }
                        if (!(weightsManifest != null)) return [3 /*break*/, 4];
                        weightsManifest_1 = modelConfig['weightsManifest'];
                        return [4 /*yield*/, this.loadWeights(weightsManifest_1)];
                    case 3:
                        results = _a.sent();
                        weightSpecs = results[0], weightData = results[1];
                        _a.label = 4;
                    case 4: return [2 /*return*/, { modelTopology: modelTopology, weightSpecs: weightSpecs, weightData: weightData }];
                }
            });
        });
    };
    BrowserHTTPRequest.prototype.loadWeights = function (weightsManifest) {
        return __awaiter(this, void 0, void 0, function () {
            var weightPath, _a, prefix, suffix, pathPrefix, weightSpecs, _i, weightsManifest_2, entry, fetchURLs, buffers;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        weightPath = Array.isArray(this.path) ? this.path[1] : this.path;
                        _a = parseUrl(weightPath), prefix = _a[0], suffix = _a[1];
                        pathPrefix = this.weightPathPrefix || prefix;
                        weightSpecs = [];
                        for (_i = 0, weightsManifest_2 = weightsManifest; _i < weightsManifest_2.length; _i++) {
                            entry = weightsManifest_2[_i];
                            weightSpecs.push.apply(weightSpecs, entry.weights);
                        }
                        fetchURLs = [];
                        weightsManifest.forEach(function (weightsGroup) {
                            weightsGroup.paths.forEach(function (path) {
                                fetchURLs.push(pathPrefix + path + suffix);
                            });
                        });
                        return [4 /*yield*/, weights_loader_1.loadWeightsAsArrayBuffer(fetchURLs, this.requestInit, this.getFetchFunc(), this.onProgress)];
                    case 1:
                        buffers = _b.sent();
                        return [2 /*return*/, [weightSpecs, io_utils_1.concatenateArrayBuffers(buffers)]];
                }
            });
        });
    };
    /**
     * Helper method to get the `fetch`-like function set for this instance.
     *
     * This is mainly for avoiding confusion with regard to what context
     * the `fetch`-like function is bound to. In the default (browser) case,
     * the function will be bound to `window`, instead of `this`.
     */
    BrowserHTTPRequest.prototype.getFetchFunc = function () {
        return this.fetchFunc;
    };
    BrowserHTTPRequest.URL_SCHEME_REGEX = /^https?:\/\//;
    return BrowserHTTPRequest;
}());
exports.BrowserHTTPRequest = BrowserHTTPRequest;
/**
 * Extract the prefix and suffix of the url, where the prefix is the path before
 * the last file, and suffix is the search params after the last file.
 * ```
 * const url = 'http://tfhub.dev/model/1/tensorflowjs_model.pb?tfjs-format=file'
 * [prefix, suffix] = parseUrl(url)
 * // prefix = 'http://tfhub.dev/model/1/'
 * // suffix = '?tfjs-format=file'
 * ```
 * @param url the model url to be parsed.
 */
function parseUrl(url) {
    var lastSlash = url.lastIndexOf('/');
    var lastSearchParam = url.lastIndexOf('?');
    var prefix = url.substring(0, lastSlash);
    var suffix = lastSearchParam > lastSlash ? url.substring(lastSearchParam) : '';
    return [prefix + '/', suffix];
}
exports.parseUrl = parseUrl;
function isHTTPScheme(url) {
    return url.match(BrowserHTTPRequest.URL_SCHEME_REGEX) != null;
}
exports.isHTTPScheme = isHTTPScheme;
exports.httpRequestRouter = function (url, onProgress) {
    if (typeof fetch === 'undefined') {
        // browserHTTPRequest uses `fetch`, if one wants to use it in node.js
        // they have to setup a global fetch polyfill.
        return null;
    }
    else {
        var isHTTP = true;
        if (Array.isArray(url)) {
            isHTTP = url.every(function (urlItem) { return isHTTPScheme(urlItem); });
        }
        else {
            isHTTP = isHTTPScheme(url);
        }
        if (isHTTP) {
            return browserHTTPRequest(url, null, null, null, onProgress);
        }
    }
    return null;
};
router_registry_1.IORouterRegistry.registerSaveRouter(exports.httpRequestRouter);
router_registry_1.IORouterRegistry.registerLoadRouter(exports.httpRequestRouter);
/**
 * Creates an IOHandler subtype that sends model artifacts to HTTP server.
 *
 * An HTTP request of the `multipart/form-data` mime type will be sent to the
 * `path` URL. The form data includes artifacts that represent the topology
 * and/or weights of the model. In the case of Keras-style `tf.Model`, two
 * blobs (files) exist in form-data:
 *   - A JSON file consisting of `modelTopology` and `weightsManifest`.
 *   - A binary weights file consisting of the concatenated weight values.
 * These files are in the same format as the one generated by
 * [tfjs_converter](https://js.tensorflow.org/tutorials/import-keras.html).
 *
 * The following code snippet exemplifies the client-side code that uses this
 * function:
 *
 * ```js
 * const model = tf.sequential();
 * model.add(
 *     tf.layers.dense({units: 1, inputShape: [100], activation: 'sigmoid'}));
 *
 * const saveResult = await model.save(tf.io.browserHTTPRequest(
 *     'http://model-server:5000/upload', {method: 'PUT'}));
 * console.log(saveResult);
 * ```
 *
 * If the default `POST` method is to be used, without any custom parameters
 * such as headers, you can simply pass an HTTP or HTTPS URL to `model.save`:
 *
 * ```js
 * const saveResult = await model.save('http://model-server:5000/upload');
 * ```
 *
 * The following Python code snippet based on the
 * [flask](https://github.com/pallets/flask) server framework implements a
 * server that can receive the request. Upon receiving the model artifacts
 * via the requst, this particular server reconsistutes instances of
 * [Keras Models](https://keras.io/models/model/) in memory.
 *
 * ```python
 * # pip install -U flask flask-cors tensorflow tensorflowjs
 *
 * from __future__ import absolute_import
 * from __future__ import division
 * from __future__ import print_function
 *
 * import io
 *
 * from flask import Flask, Response, request
 * from flask_cors import CORS, cross_origin
 * import tensorflow as tf
 * import tensorflowjs as tfjs
 * import werkzeug.formparser
 *
 * class ModelReceiver(object):
 *
 *   def __init__(self):
 *     self._model = None
 *     self._model_json_bytes = None
 *     self._model_json_writer = None
 *     self._weight_bytes = None
 *     self._weight_writer = None
 *
 *   @property
 *   def model(self):
 *     self._model_json_writer.flush()
 *     self._weight_writer.flush()
 *     self._model_json_writer.seek(0)
 *     self._weight_writer.seek(0)
 *
 *     json_content = self._model_json_bytes.read()
 *     weights_content = self._weight_bytes.read()
 *     return tfjs.converters.deserialize_keras_model(
 *         json_content,
 *         weight_data=[weights_content],
 *         use_unique_name_scope=True)
 *
 *   def stream_factory(self,
 *                      total_content_length,
 *                      content_type,
 *                      filename,
 *                      content_length=None):
 *     # Note: this example code is *not* thread-safe.
 *     if filename == 'model.json':
 *       self._model_json_bytes = io.BytesIO()
 *       self._model_json_writer = io.BufferedWriter(self._model_json_bytes)
 *       return self._model_json_writer
 *     elif filename == 'model.weights.bin':
 *       self._weight_bytes = io.BytesIO()
 *       self._weight_writer = io.BufferedWriter(self._weight_bytes)
 *       return self._weight_writer
 *
 *
 * def main():
 *   app = Flask('model-server')
 *   CORS(app)
 *   app.config['CORS_HEADER'] = 'Content-Type'
 *
 *   model_receiver = ModelReceiver()
 *
 *   @app.route('/upload', methods=['POST'])
 *   @cross_origin()
 *   def upload():
 *     print('Handling request...')
 *     werkzeug.formparser.parse_form_data(
 *         request.environ, stream_factory=model_receiver.stream_factory)
 *     print('Received model:')
 *     with tf.Graph().as_default(), tf.Session():
 *       model = model_receiver.model
 *       model.summary()
 *       # You can perform `model.predict()`, `model.fit()`,
 *       # `model.evaluate()` etc. here.
 *     return Response(status=200)
 *
 *   app.run('localhost', 5000)
 *
 *
 * if __name__ == '__main__':
 *   main()
 * ```
 *
 * @param path A single URL path or an Array of URL  paths.
 *   Currently, only a single URL path is supported. Array input is reserved
 *   for future development.
 *   Can be an absolute HTTP path (e.g.,
 *   'http://localhost:8000/model-upload)') or a relative path (e.g.,
 *   './model-upload').
 * @param requestInit Request configurations to be used when sending
 *    HTTP request to server using `fetch`. It can contain fields such as
 *    `method`, `credentials`, `headers`, `mode`, etc. See
 *    https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
 *    for more information. `requestInit` must not have a body, because the
 * body will be set by TensorFlow.js. File blobs representing the model
 * topology (filename: 'model.json') and the weights of the model (filename:
 * 'model.weights.bin') will be appended to the body. If `requestInit` has a
 * `body`, an Error will be thrown.
 * @param weightPathPrefix Optional, this specifies the path prefix for weight
 *   files, by default this is calculated from the path param.
 * @param fetchFunc Optional, custom `fetch` function. E.g., in Node.js,
 *   the `fetch` from node-fetch can be used here.
 * @param onProgress Optional, progress callback function, fired periodically
 *   before the load is completed.
 * @returns An instance of `IOHandler`.
 */
function browserHTTPRequest(path, requestInit, weightPathPrefix, fetchFunc, onProgress) {
    return new BrowserHTTPRequest(path, requestInit, weightPathPrefix, fetchFunc, onProgress);
}
exports.browserHTTPRequest = browserHTTPRequest;
//# sourceMappingURL=browser_http.js.map