import grpc, { Metadata, credentials } from 'grpc';
import { isNumber } from 'util';
import fetch from 'node-fetch';
import { parse } from 'url';
import asyncHooks from 'async_hooks';

const ENVOY_DEFAULT_EGRESS_PORT = 12345;
const ENVOY_DEFAULT_EGRESS_ADDR = "127.0.0.1";
const ENVOY_EGRESS_PORT = parseInt(process.env.ENVOY_EGRESS_PORT || `${ENVOY_DEFAULT_EGRESS_PORT}`, 10);
const ENVOY_EGRESS_ADDR = process.env.ENVOY_EGRESS_ADDR || ENVOY_DEFAULT_EGRESS_ADDR;
const X_B3_TRACEID = "x-b3-traceid";
const X_B3_SPANID = "x-b3-spanid";
const X_B3_PARENTSPANID = "x-b3-parentspanid";
const X_B3_SAMPLED = "x-b3-sampled";
const X_B3_FLAGS = "x-b3-flags";
const X_OT_SPAN_CONTEXT = "x-ot-span-context";
const X_REQUEST_ID = "x-request-id";
const X_CLIENT_TRACE_ID = "x-client-trace-id";
const X_ENVOY_EXPECTED_RQ_TIMEOUT_MS = "x-envoy-expected-rq-timeout-ms";
/**
 * the header returned by envoy telling upstream is overloaded
 * @internal
 */
const X_ENVOY_OVERLOADED = "x-envoy-overloaded";
/**
 * the header returned by envoy telling upstream duration
 * @internal
 */
const X_ENVOY_UPSTREAM_SERVICE_TIME = "x-envoy-upstream-service-time";
/**
 * the header set in envoy config for telling this library egress port
 */
const X_TUBI_ENVOY_EGRESS_PORT = "x-tubi-envoy-egress-port";
/**
 * the header set in envoy config for telling this library egress address
 */
const X_TUBI_ENVOY_EGRESS_ADDR = "x-tubi-envoy-egress-addr";
/**
 * the optional header set in envoy config for telling a host is managed by envoy
 * so that this library can route envoy or call directly accordingly
 */
const X_TUBI_ENVOY_MANAGED_HOST = "x-tubi-envoy-managed-host";
/**
 * read value of the key from meata
 * return undefined if not found or empty
 * return first one if multiple values
 * @param meta metadata
 * @param key key
 */
function readMetaAsStringOrUndefined(meta, key) {
    const value = meta.get(key);
    if (value.length > 0) {
        return value[0].toString();
    }
    return undefined;
}
function alwaysReturnArray(input) {
    if (Array.isArray(input))
        return input;
    return [input];
}
/**
 * read value of the key from header
 * return undefined if not found or empty
 * return first one if multiple values
 * @param header the header
 * @param key the key
 */
function readHeaderOrUndefined(header, key) {
    const value = header[key];
    if (!value) {
        return undefined;
    }
    return alwaysReturnArray(value)[0];
}
/**
 * assign key value to header, skip empty value
 * @param header the http header
 * @param key the key
 * @param value the value
 */
function assignHeader(header, key, value) {
    if (value === undefined || value === null)
        return;
    if (isNumber(value)) {
        if (isNaN(value)) {
            return;
        }
        header[key] = `${value}`;
    }
    else {
        header[key] = value;
    }
}
/**
 * some HTTP framework will do a tricky thing: the merge the headers into one string
 * fixing it here
 * @param hosts a list of array
 */
function refineManagedHostArray(hosts) {
    return hosts.reduce((acc, host) => {
        if (host.indexOf(",") >= 0) {
            return acc.concat(host
                .split(",")
                .map(value => value.trim())
                .filter(value => value));
        }
        acc.push(host);
        return acc;
    }, []);
}
function ensureItsEnvoyContextInit(param) {
    // test if this is a grpc.Metadata
    if (param instanceof Metadata) {
        return {
            meta: param
        };
    }
    // this if this is a HttpHeader
    const asInit = param;
    if (!asInit.meta || typeof asInit.meta === "string" || Array.isArray(asInit.meta)) {
        return {
            meta: param
        };
    }
    return asInit;
}
/**
 * EnvoyContext is where all information related to the current envoy environment.
 */
class EnvoyContext {
    /**
     * initialize an EnvoyContext
     * @param options options for init envoy context
     */
    constructor(options) {
        const { meta, envoyEgressPort, envoyEgressAddr, envoyManagedHosts, directMode } = ensureItsEnvoyContextInit(options);
        let expectedRequestTimeoutString;
        let envoyEgressAddrFromHeader;
        let envoyEgressPortStringFromHeader;
        let envoyManagedHostsFromHeader;
        if (meta instanceof Metadata) {
            const metadata = meta;
            this.traceId = readMetaAsStringOrUndefined(metadata, X_B3_TRACEID);
            this.spanId = readMetaAsStringOrUndefined(metadata, X_B3_SPANID);
            this.parentSpanId = readMetaAsStringOrUndefined(metadata, X_B3_PARENTSPANID);
            this.sampled = readMetaAsStringOrUndefined(metadata, X_B3_SAMPLED);
            this.flags = readMetaAsStringOrUndefined(metadata, X_B3_FLAGS);
            this.otSpanContext = readMetaAsStringOrUndefined(metadata, X_OT_SPAN_CONTEXT);
            this.requestId = readMetaAsStringOrUndefined(metadata, X_REQUEST_ID);
            this.clientTraceId = readMetaAsStringOrUndefined(metadata, X_CLIENT_TRACE_ID);
            expectedRequestTimeoutString = readMetaAsStringOrUndefined(metadata, X_ENVOY_EXPECTED_RQ_TIMEOUT_MS);
            envoyEgressAddrFromHeader = readMetaAsStringOrUndefined(metadata, X_TUBI_ENVOY_EGRESS_ADDR);
            envoyEgressPortStringFromHeader = readMetaAsStringOrUndefined(metadata, X_TUBI_ENVOY_EGRESS_PORT);
            const managedHosts = metadata.get(X_TUBI_ENVOY_MANAGED_HOST);
            if (managedHosts && managedHosts.length > 0) {
                envoyManagedHostsFromHeader = managedHosts.map(v => v.toString());
            }
        }
        else {
            const httpHeader = meta;
            this.traceId = readHeaderOrUndefined(httpHeader, X_B3_TRACEID);
            this.spanId = readHeaderOrUndefined(httpHeader, X_B3_SPANID);
            this.parentSpanId = readHeaderOrUndefined(httpHeader, X_B3_PARENTSPANID);
            this.sampled = readHeaderOrUndefined(httpHeader, X_B3_SAMPLED);
            this.flags = readHeaderOrUndefined(httpHeader, X_B3_FLAGS);
            this.otSpanContext = readHeaderOrUndefined(httpHeader, X_OT_SPAN_CONTEXT);
            this.requestId = readHeaderOrUndefined(httpHeader, X_REQUEST_ID);
            this.clientTraceId = readHeaderOrUndefined(httpHeader, X_CLIENT_TRACE_ID);
            expectedRequestTimeoutString = readHeaderOrUndefined(httpHeader, X_ENVOY_EXPECTED_RQ_TIMEOUT_MS);
            envoyEgressAddrFromHeader = readHeaderOrUndefined(httpHeader, X_TUBI_ENVOY_EGRESS_ADDR);
            envoyEgressPortStringFromHeader = readHeaderOrUndefined(httpHeader, X_TUBI_ENVOY_EGRESS_PORT);
            const managedHosts = httpHeader[X_TUBI_ENVOY_MANAGED_HOST];
            if (managedHosts) {
                envoyManagedHostsFromHeader = alwaysReturnArray(managedHosts);
            }
        }
        if (expectedRequestTimeoutString !== undefined && expectedRequestTimeoutString !== "") {
            this.expectedRequestTimeout = parseInt(expectedRequestTimeoutString, 10);
        }
        this.envoyEgressPort =
            envoyEgressPort ||
                (envoyEgressPortStringFromHeader && parseInt(envoyEgressPortStringFromHeader, 10)) ||
                ENVOY_EGRESS_PORT;
        this.envoyEgressAddr = envoyEgressAddr || envoyEgressAddrFromHeader || ENVOY_EGRESS_ADDR;
        if (directMode === undefined) {
            this.directMode =
                process.env.ENVOY_DIRECT_MODE === "true" || process.env.ENVOY_DIRECT_MODE === "1";
        }
        else {
            this.directMode = directMode;
        }
        if (this.directMode) {
            this.envoyManagedHosts = new Set();
        }
        else if (envoyManagedHosts !== undefined) {
            this.envoyManagedHosts = envoyManagedHosts;
        }
        else if (envoyManagedHostsFromHeader !== undefined) {
            this.envoyManagedHosts = new Set(refineManagedHostArray(envoyManagedHostsFromHeader));
        }
    }
    /**
     * Assemble the required tracing headers that required for propagation.
     * See more here <https://www.envoyproxy.io/docs/envoy/v1.5.0/intro/arch_overview/tracing.html#trace-context-propagation>
     */
    assembleTracingHeader() {
        const header = {};
        assignHeader(header, X_B3_TRACEID, this.traceId);
        assignHeader(header, X_B3_SPANID, this.spanId);
        assignHeader(header, X_B3_PARENTSPANID, this.parentSpanId);
        assignHeader(header, X_B3_SAMPLED, this.sampled);
        assignHeader(header, X_B3_FLAGS, this.flags);
        assignHeader(header, X_OT_SPAN_CONTEXT, this.otSpanContext);
        assignHeader(header, X_REQUEST_ID, this.requestId);
        assignHeader(header, X_CLIENT_TRACE_ID, this.clientTraceId);
        return header;
    }
    shouldCallWithoutEnvoy(host) {
        return (this.directMode || (this.envoyManagedHosts !== undefined && !this.envoyManagedHosts.has(host)));
    }
}

var __awaiter = (undefined && undefined.__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());
    });
};
/**
 * the fetch function share most of the signature of the original node-fetch
 * but helps you on setting up the request being send to envoy egress port
 * @param envoyParams the params of envoy context as well as request control params (timeout / retry, etc)
 * @param url the target url, the same as node-fetch's first param
 * @param init the init, the same as node-fetch's second param
 */
function envoyFetch(envoyParams, url$$1, init) {
    return __awaiter(this, void 0, void 0, function* () {
        const { protocol, host, path } = parse(url$$1);
        if (!protocol || !host || !path) {
            throw new Error("Cannot read the URL for envoy to fetch");
        }
        const callDirectly = envoyParams.context.shouldCallWithoutEnvoy(host);
        if (protocol !== "http:" && protocol !== "https:" && !callDirectly) {
            throw new Error(`envoy fetch is designed only for http / https for now, current found: ${url$$1}`);
        }
        const refinedInit = Object.assign({}, init);
        const oldHeaders = {};
        Object.assign(oldHeaders, refinedInit.headers);
        refinedInit.headers = Object.assign({}, oldHeaders, envoyParams.assembleRequestHeaders(), { host });
        const actualUrl = callDirectly
            ? url$$1
            : `http://${envoyParams.context.envoyEgressAddr}:${envoyParams.context.envoyEgressPort}${path}`;
        const response = yield fetch(actualUrl, refinedInit);
        const upstreamTimeHeader = response.headers.get(X_ENVOY_UPSTREAM_SERVICE_TIME);
        const upstreamServiceTime = upstreamTimeHeader === null ? NaN : parseInt(upstreamTimeHeader, 10);
        /* tslint:disable:prefer-object-spread */
        const envoyResponse = Object.assign(response, {
            overloaded: response.headers.has(X_ENVOY_OVERLOADED),
            upstreamServiceTime
        });
        /* tslint:enable:prefer-object-spread */
        return envoyResponse;
    });
}

/**
 * header of envoy max retries setting
 * @internal
 */
const X_ENVOY_MAX_RETRIES = "x-envoy-max-retries";
/**
 * header of envoy request timeout
 * @internal
 */
const X_ENVOY_UPSTREAM_RQ_TIMEOUT_MS = "x-envoy-upstream-rq-timeout-ms";
/**
 * header of envoy timeout per try
 * @internal
 */
const X_ENVOY_UPSTREAM_RQ_PER_TRY_TIMEOUT_MS = "x-envoy-upstream-rq-per-try-timeout-ms";
/**
 * the Common signature of EnvoyRequestParams
 */
class EnvoyRequestParams {
    constructor(context, maxRetries, timeout, perTryTimeout, headers) {
        this.context = context;
        this.maxRetries = maxRetries;
        this.timeout = timeout;
        this.perTryTimeout = perTryTimeout;
        this.customHeaders = headers;
    }
}

const X_ENVOY_RETRY_GRPC_ON = "x-envoy-retry-grpc-on";
/**
 * Setting this header on egress requests will cause Envoy to attempt to retry
 * failed requests (number of retries defaults to 1, and can be controlled by
 * x-envoy-max-retries header or the route config retry policy). gRPC retries
 * are currently only supported for gRPC status codes in response headers. gRPC
 * status codes in trailers will not trigger retry logic.
 */
var GrpcRetryOn;
(function (GrpcRetryOn) {
    /**
     * Envoy will attempt a retry if the gRPC status code in the response headers is
     * “cancelled” (1)
     */
    GrpcRetryOn["CANCELLED"] = "cancelled";
    /**
     * Envoy will attempt a retry if the gRPC status code in the response headers is
     * “deadline-exceeded” (4)
     */
    GrpcRetryOn["DEADLINE_EXCEEDED"] = "deadline-exceeded";
    /**
     * Envoy will attempt a retry if the gRPC status code in the response headers is
     * “resource-exhausted” (8)
     */
    GrpcRetryOn["RESOURCE_EXHAUSTED"] = "resource-exhausted";
})(GrpcRetryOn || (GrpcRetryOn = {}));
/**
 * convert http header to grpc.Metadata
 * @param httpHeader the http header
 * @internal
 */
function httpHeader2Metadata(httpHeader) {
    const metadata = new grpc.Metadata();
    for (const [key, value] of Object.entries(httpHeader)) {
        if (Array.isArray(value)) {
            value.forEach(v => metadata.add(key, v));
        }
        else {
            metadata.add(key, value);
        }
    }
    return metadata;
}
/**
 * the gRPC request params, mainly two parts:
 * 1. EnvoyContext, telling what the situation is
 * 2. request params, like timeout, retry, etc.
 */
class EnvoyGrpcRequestParams extends EnvoyRequestParams {
    /**
     * Setting the retry policies, if empty param is given will not generate any headers but using
     * the default setting in Envoy's config
     * @param params the params for initialize the request params
     */
    constructor(context, params) {
        const { maxRetries, retryOn, timeout, perTryTimeout, headers } = Object.assign({ maxRetries: -1, retryOn: [], timeout: -1, perTryTimeout: -1, headers: {} }, params);
        super(context, maxRetries, timeout, perTryTimeout, headers);
        this.retryOn = retryOn;
    }
    /**
     * assemble the request headers for setting retry.
     */
    assembleRequestMeta() {
        const metadata = httpHeader2Metadata(Object.assign({}, this.context.assembleTracingHeader(), this.customHeaders));
        if (this.maxRetries >= 0) {
            metadata.add(X_ENVOY_MAX_RETRIES, `${this.maxRetries}`);
        }
        if (this.maxRetries > 0) {
            metadata.add(X_ENVOY_RETRY_GRPC_ON, this.retryOn.join(","));
        }
        if (this.timeout > 0) {
            metadata.add(X_ENVOY_UPSTREAM_RQ_TIMEOUT_MS, `${this.timeout}`);
        }
        if (this.perTryTimeout > 0) {
            metadata.add(X_ENVOY_UPSTREAM_RQ_PER_TRY_TIMEOUT_MS, `${this.perTryTimeout}`);
        }
        return metadata;
    }
}

const X_ENVOY_RETRY_ON = "x-envoy-retry-on";
/**
 * Setting this header on egress requests will cause Envoy to attempt to retry failed
 * requests (number of retries defaults to 1 and can be controlled by x-envoy-max-retries
 * header or the route config retry policy). The value to which the x-envoy-retry-on header
 * is set indicates the retry policy.
 */
var HttpRetryOn;
(function (HttpRetryOn) {
    /**
     * Envoy will attempt a retry if the upstream server responds with any 5xx response code,
     * or does not respond at all (disconnect/reset/read timeout). (Includes connect-failure
     * and refused-stream)
     *
     * NOTE: Envoy will not retry when a request exceeds x-envoy-upstream-rq-timeout-ms
     * (resulting in a 504 error code). Use x-envoy-upstream-rq-per-try-timeout-ms if you want
     * to retry when individual attempts take too long. x-envoy-upstream-rq-timeout-ms is an
     * outer time limit for a request, including any retries that take place.
     */
    HttpRetryOn["HTTP_5XX"] = "5xx";
    /**
     * Envoy will attempt a retry if a request is failed because of a connection failure to the
     * upstream server (connect timeout, etc.). (Included in 5xx)
     *
     * NOTE: A connection failure/timeout is a the TCP level, not the request level. This does
     * not include upstream request timeouts specified via x-envoy-upstream-rq-timeout-ms or via
     * route configuration.
     */
    HttpRetryOn["CONNECT_FAILURE"] = "connect-failure";
    /**
     * Envoy will attempt a retry if the upstream server responds with a retriable 4xx response
     * code. Currently, the only response code in this category is 409.
     *
     * NOTE: Be careful turning on this retry type. There are certain cases where a 409 can
     * indicate that an optimistic locking revision needs to be updated. Thus, the caller should
     * not retry and needs to read then attempt another write. If a retry happens in this type of
     * case it will always fail with another 409.
     */
    HttpRetryOn["RETRIABLE_4XX"] = "retriable-4xx";
    /**
     * Envoy will attempt a retry if the upstream server resets the stream with a REFUSED_STREAM
     * error code. This reset type indicates that a request is safe to retry. (Included in 5xx)
     */
    HttpRetryOn["REFUSED_STREAM"] = "refused-stream";
})(HttpRetryOn || (HttpRetryOn = {}));
/**
 * the HTTP request params, mainly two parts:
 * 1. EnvoyContext, telling what the situation is
 * 2. request params, like timeout, retry, etc.
 */
class EnvoyHttpRequestParams extends EnvoyRequestParams {
    /**
     * Setting the retry policies, if empty param is given will not generate any headers but using
     * the default setting in Envoy's config
     * @param params the params for initialize the request params
     */
    constructor(context, params) {
        const { maxRetries, retryOn, timeout, perTryTimeout, headers } = Object.assign({ maxRetries: -1, retryOn: [], timeout: -1, perTryTimeout: -1, headers: {} }, params);
        super(context, maxRetries, timeout, perTryTimeout, headers);
        this.retryOn = retryOn;
    }
    /**
     * assemble the request headers for setting retry.
     * TODO: in direct mode, we may need to modify the tracing headers
     */
    assembleRequestHeaders() {
        const header = Object.assign({}, this.context.assembleTracingHeader(), this.customHeaders);
        if (this.maxRetries >= 0) {
            header[X_ENVOY_MAX_RETRIES] = `${this.maxRetries}`;
        }
        if (this.maxRetries > 0) {
            header[X_ENVOY_RETRY_ON] = this.retryOn.join(",");
        }
        if (this.timeout > 0) {
            header[X_ENVOY_UPSTREAM_RQ_TIMEOUT_MS] = `${this.timeout}`;
        }
        if (this.perTryTimeout > 0) {
            header[X_ENVOY_UPSTREAM_RQ_PER_TRY_TIMEOUT_MS] = `${this.perTryTimeout}`;
        }
        return header;
    }
}

var __awaiter$1 = (undefined && undefined.__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());
    });
};
/**
 * json header
 * @internal
 */
const APPLICATION_JSON = "application/json";
/**
 * a high level http client that will send traffic to Envoy:
 * 1. all HTTP GET / POST ... will be wrapped as async signature
 * 2. doing JSON RESET API common works: accepting object, return objects, only works with `application/json`
 * 3. none 2XX request will be throw as Error
 */
class EnvoyHttpClient {
    /**
     * init the client
     * @param ctx context, you can give either EnvoyContext, Metadata or HTTP Header
     *  the last two will be convert to EnvoyContext
     */
    constructor(ctx) {
        if (ctx instanceof EnvoyContext) {
            this.envoyContext = ctx;
        }
        else {
            this.envoyContext = new EnvoyContext(ctx);
        }
    }
    /**
     * the common logic for json processing and error handling
     * @param response response from fetch
     */
    returnJsonOrError(response) {
        return __awaiter$1(this, void 0, void 0, function* () {
            // TODO considering return the following metadata:
            // response.overloaded
            // response.upstreamServiceTime
            const $statusCode = response.status;
            if ($statusCode === 204) {
                return undefined;
            }
            const contentType = response.headers.get("content-type");
            if (!contentType || (contentType !== "application/json" && !contentType.startsWith("text/"))) {
                const err = new Error(`Unexpected content type: ${contentType}, http status: ${$statusCode}`);
                const body = yield response.text();
                Object.assign(err, { $statusCode, body });
                throw err;
            }
            if (contentType === "application/json") {
                const result = yield response.json();
                if ($statusCode >= 400) {
                    result.$statusCode = result.$statusCode || $statusCode;
                    throw result;
                }
                return result;
            }
            const text = yield response.text();
            if ($statusCode !== 200) {
                const error = new Error(text);
                Object.assign(error, { $statusCode });
                throw error;
            }
            return text;
        });
    }
    actionWithoutBody(method, url$$1, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            const param = new EnvoyHttpRequestParams(this.envoyContext, init);
            const res = yield envoyFetch(param, url$$1, {
                method,
                headers: {
                    accept: APPLICATION_JSON
                }
            });
            return this.returnJsonOrError(res);
        });
    }
    /**
     * send a GET request and expecting return json or empty
     * @param url the URL to get
     * @param init the params for the request, like retry, timeout
     * @throws Error for none 2XX request, a $statusCode will be available in the error object
     */
    get(url$$1, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return this.actionWithoutBody("GET", url$$1, init);
        });
    }
    /**
     * send a DELETE request and expecting return json or empty
     * @param url the URL to get
     * @param init the params for the request, like retry, timeout
     * @throws Error for none 2XX request, a $statusCode will be available in the error object
     */
    delete(url$$1, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return this.actionWithoutBody("DELETE", url$$1, init);
        });
    }
    actionWithBody(method, url$$1, body, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            const param = new EnvoyHttpRequestParams(this.envoyContext, init);
            const res = yield envoyFetch(param, url$$1, {
                method,
                headers: {
                    "content-type": APPLICATION_JSON,
                    // tslint:disable-next-line:object-literal-key-quotes
                    accept: APPLICATION_JSON
                },
                body: JSON.stringify(body)
            });
            return this.returnJsonOrError(res);
        });
    }
    /**
     * send a POST request and expecting return json or empty
     * @param url the URL to get
     * @param body the request object, will be serialize to JSON when sending out
     * @param init the params for the request, like retry, timeout
     * @throws Error for none 2XX request, a $statusCode will be available in the error object
     */
    post(url$$1, body, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return this.actionWithBody("POST", url$$1, body, init);
        });
    }
    /**
     * send a PATCH request and expecting return json or empty
     * @param url the URL to get
     * @param body the request object, will be serialize to JSON when sending out
     * @param init the params for the request, like retry, timeout
     * @throws Error for none 2XX request, a $statusCode will be available in the error object
     */
    patch(url$$1, body, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return this.actionWithBody("PATCH", url$$1, body, init);
        });
    }
    /**
     * send a PUT request and expecting return json or empty
     * @param url the URL to get
     * @param body the request object, will be serialize to JSON when sending out
     * @param init the params for the request, like retry, timeout
     * @throws Error for none 2XX request, a $statusCode will be available in the error object
     */
    put(url$$1, body, init) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return this.actionWithBody("PUT", url$$1, body, init);
        });
    }
}

var __awaiter$2 = (undefined && undefined.__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());
    });
};
/**
 * this function is to assign new method to the decorated original client
 * by assigning new method, user can call the method easier with async signature
 * @param name the function name
 * @internal
 */
function makeAsyncFunc(name) {
    return function (request, options) {
        return __awaiter$2(this, void 0, void 0, function* () {
            const params = new EnvoyGrpcRequestParams(this.envoyContext, options);
            return new Promise((resolve, reject) => {
                Object.getPrototypeOf(Object.getPrototypeOf(this))[name].call(this, request, params.assembleRequestMeta(), {
                    host: this.originalAddress
                }, (error, response) => {
                    if (error) {
                        reject(error);
                        return;
                    }
                    resolve(response);
                });
            });
        });
    };
}
/**
 * this is to wrap the original client stream func, to insert envoy metadata
 * @param name the name of the func
 */
function wrapClientStreamFunc(name) {
    return function (callback, options) {
        const params = new EnvoyGrpcRequestParams(this.envoyContext, options);
        return Object.getPrototypeOf(Object.getPrototypeOf(this))[name].call(this, params.assembleRequestMeta(), {
            host: this.originalAddress
        }, callback);
    };
}
/**
 * this is to wrap the original server stream func, to insert envoy metadata
 * @param name the name of the func
 */
function wrapServerStream(name) {
    return function (request, options) {
        const params = new EnvoyGrpcRequestParams(this.envoyContext, options);
        return Object.getPrototypeOf(Object.getPrototypeOf(this))[name].call(this, request, params.assembleRequestMeta(), {
            host: this.originalAddress
        });
    };
}
/**
 * this is to wrap the original bidirectional stream, to insert envoy metadata
 * @param name the func name
 */
function wrapBidiStream(name) {
    return function (options) {
        const params = new EnvoyGrpcRequestParams(this.envoyContext, options);
        return Object.getPrototypeOf(Object.getPrototypeOf(this))[name].call(this, params.assembleRequestMeta(), {
            host: this.originalAddress
        });
    };
}
/**
 * this method will decorate the client constructor to
 * 1. enable envoy context
 * 2. using async syntax for each call RPC
 *
 * Check `EnvoyClient` for more information
 *
 * TODO: optimize the typing if the typing of gRPC is updated
 * @param constructor Client constructor
 */
function envoyProtoDecorator(constructor, channelFactoryOverride = undefined) {
    const constructorAlias = constructor;
    const { service } = constructorAlias;
    const clazz = class extends constructor {
        constructor(address, ctx) {
            let envoyContext;
            if (ctx instanceof EnvoyContext) {
                envoyContext = ctx;
            }
            else {
                envoyContext = new EnvoyContext(ctx);
            }
            const actualAddr = envoyContext.shouldCallWithoutEnvoy(address)
                ? address
                : `${envoyContext.envoyEgressAddr}:${envoyContext.envoyEgressPort}`;
            super(actualAddr, credentials.createInsecure(), { channelFactoryOverride });
            this.originalAddress = address;
            this.envoyContext = envoyContext;
        }
    };
    const prototype = clazz.prototype;
    for (const name of Object.keys(service)) {
        const method = service[name];
        const { requestStream, responseStream } = method;
        if (!requestStream && !responseStream) {
            // tslint:disable-next-line:only-arrow-functions
            prototype[name] = makeAsyncFunc(name);
        }
        else if (method.requestStream && !method.responseStream) {
            prototype[name] = wrapClientStreamFunc(name);
        }
        else if (!method.requestStream && method.responseStream) {
            prototype[name] = wrapServerStream(name);
        }
        else {
            prototype[name] = wrapBidiStream(name);
        }
        const { originalName } = method;
        /* istanbul ignore next */
        if (originalName) {
            // should alway have
            prototype[originalName] = prototype[name];
        }
    }
    return clazz;
}

/**
 * to easier migrate from http request using request library, you can use this function
 * to refine the request params directly
 * @param params request params, can be url string or params object
 * @param ctx the context, can be EnvoyContext, grpc.Metadata or HttpHeader
 * @param init the extra options for the request
 */
function envoyRequestParamsRefiner(params, ctx, init) {
    if (!ctx) {
        return typeof params === "string" ? { url: params } : params;
    }
    let envoyContext;
    if (ctx instanceof EnvoyContext) {
        envoyContext = ctx;
    }
    else {
        envoyContext = new EnvoyContext(ctx);
    }
    const envoyParams = new EnvoyHttpRequestParams(envoyContext, init);
    let refinedParams;
    if (typeof params === "string") {
        refinedParams = { url: params };
    }
    else {
        refinedParams = Object.assign({}, params);
    }
    const refinedParamsWithUrl = refinedParams;
    const refinedParamsWithUri = refinedParams;
    if (Object.prototype.hasOwnProperty.call(refinedParams, "url")) {
        refinedParamsWithUri.uri = refinedParamsWithUrl.url;
        delete refinedParamsWithUrl.url;
    }
    if (!refinedParamsWithUri.uri) {
        throw new Error("Cannot read url from params");
    }
    if (typeof refinedParamsWithUri.uri === "string") {
        refinedParamsWithUri.uri = parse(refinedParamsWithUri.uri);
    }
    const { protocol, host, path } = refinedParamsWithUri.uri;
    if (!protocol || !host || !path) {
        throw new Error("Cannot read the URL for envoy to fetch");
    }
    const callDirectly = envoyParams.context.shouldCallWithoutEnvoy(host);
    if (protocol !== "http:" && protocol !== "https:") {
        throw new Error(`envoy request is designed only for http / https for now, current found: ${protocol}`);
    }
    const oldHeaders = {};
    Object.assign(oldHeaders, refinedParamsWithUri.headers);
    refinedParamsWithUri.headers = Object.assign({}, oldHeaders, envoyParams.assembleRequestHeaders(), { host });
    if (!callDirectly) {
        refinedParamsWithUri.uri = `http://${envoyParams.context.envoyEgressAddr}:${envoyParams.context.envoyEgressPort}${path}`;
    }
    return refinedParams;
}

/**
 * this will store the information of a node
 */
class NodeInfo {
    constructor(triggerAsyncId) {
        /**
         * the reference count of this info,
         * init will be 1,
         * when this async execution trigger another execution,
         *  this will increase 1,
         * when this execution or one of its child execution is destroyed,
         *  this will decrease 1
         */
        this.referenceCount = 1;
        /**
         * the context set in this execution
         */
        this.context = undefined;
        this.triggerAsyncId = triggerAsyncId;
    }
}
/**
 * Eliminate Store
 * using two map for context storage, to avoid holding too many data
 * it will eliminate the old data
 */
class EliminateStore {
    constructor() {
        this.old = new Map();
        this.current = new Map();
        this.lastEliminateTime = Date.now();
    }
    /**
     * get context
     * @param asyncId asyncId
     */
    get(asyncId) {
        const infoFromCurrent = this.current.get(asyncId);
        if (infoFromCurrent !== undefined) {
            return infoFromCurrent;
        }
        const infoFromOld = this.old.get(asyncId);
        if (infoFromOld !== undefined) {
            this.current.set(asyncId, infoFromOld);
        }
        return infoFromOld;
    }
    /**
     * set context
     * @param asyncId asyncId
     * @param info context info
     */
    set(asyncId, info) {
        this.current.set(asyncId, info);
    }
    /**
     * delete context
     * @param asyncId asyncId
     */
    delete(asyncId) {
        this.current.delete(asyncId);
        this.old.delete(asyncId);
    }
    /**
     * clear all data
     */
    clear() {
        this.old.clear();
        this.current.clear();
    }
    /**
     * eliminate the old data
     */
    eliminate() {
        this.old = this.current;
        this.current = new Map();
        this.lastEliminateTime = Date.now();
    }
    /**
     * get last eliminate time
     */
    getLastEliminateTime() {
        return this.lastEliminateTime;
    }
    /**
     * the current size
     */
    size() {
        return this.current.size;
    }
    /**
     * the old store size
     */
    oldSize() {
        return this.old.size;
    }
}
const store = new EliminateStore();
let enabled = false;
let eliminateInterval = 300 * 1000; // 300s, 5 mins
let notDestroyedHooks = 0;
/**
 * set the store's eliminate interval, context data older than this and not
 * read will be eventually eliminated
 * @param interval time in milliseconds
 */
function setEliminateInterval(interval) {
    eliminateInterval = interval;
}
/**
 * get eliminate interval
 */
function getEliminateInterval() {
    return eliminateInterval;
}
/**
 * clean up will decrease the reference count.
 * if the reference count is 0, it will remove it from the store and decrease its parent's reference count.
 *  and try to see if its parent needs to be clean up as well.
 * @param asyncId the asyncId of the execution needs to be cleaned up
 */
function storeCleanUp(asyncId) {
    const info = store.get(asyncId);
    if (info === undefined) {
        return;
    }
    info.referenceCount--;
    if (info.referenceCount === 0) {
        store.delete(asyncId);
        storeCleanUp(info.triggerAsyncId);
    }
}
const asyncHook = asyncHooks.createHook({
    init(asyncId, type, triggerAsyncId, resource) {
        notDestroyedHooks++;
        /* istanbul ignore next */
        if (Date.now() - store.getLastEliminateTime() > eliminateInterval) {
            store.eliminate();
        }
        let triggerInfo = store.get(triggerAsyncId);
        if (!triggerInfo) {
            triggerInfo = new NodeInfo(-1);
            store.set(triggerAsyncId, triggerInfo);
        }
        triggerInfo.referenceCount++;
        const info = new NodeInfo(triggerAsyncId);
        store.set(asyncId, info);
    },
    destroy(asyncId) {
        notDestroyedHooks--;
        storeCleanUp(asyncId);
    }
});
/**
 * Enable the context store,
 * you should call this function as early as possible,
 * i.e. put it in your application's start.
 */
function enable() {
    if (!enabled) {
        asyncHook.enable();
        enabled = true;
    }
    else {
        console.trace("[envoy-node] You want to enable the enabled store");
    }
}
/**
 * Disable the context store,
 * all data will be clean up as well.
 * This function is not intended to be call in the application life cycle.
 */
function disable() {
    if (enabled) {
        asyncHook.disable();
        store.clear();
        enabled = false;
    }
    else {
        console.trace("[envoy-node] You want to disable the disabled store");
    }
}
function markContext(triggerAsyncId, context) {
    const triggerInfo = store.get(triggerAsyncId);
    if (triggerInfo === undefined) {
        // no trigger info
        return; // skip
    }
    if (triggerInfo.context) {
        // trigger id has context already (reach to the border of the other request)
        return; // done
    }
    triggerInfo.context = context;
    markContext(triggerInfo.triggerAsyncId, context);
}
/**
 * According to the context store design, this function is required to be called exactly once for a request.
 * Setting multiple calls to this function will lead to context corruption.
 * @param context the context you want to set
 */
function set(context) {
    if (!enabled) {
        console.trace("[envoy-node] cannot set context when store is not enabled.");
        return;
    }
    const asyncId = asyncHooks.executionAsyncId();
    const info = store.get(asyncId);
    /* istanbul ignore next */
    if (info === undefined) {
        console.trace("[envoy-node] Cannot find info of current execution, have you enabled the context store correctly?");
        return;
    }
    info.context = context;
    markContext(info.triggerAsyncId, context);
}
/**
 * get context from the execution tree
 * @param asyncId the async id
 */
function getContext(asyncId) {
    const info = store.get(asyncId);
    /* istanbul ignore next */
    if (!info) {
        return undefined;
    }
    if (!info.context) {
        info.context = getContext(info.triggerAsyncId);
    }
    return info.context;
}
/**
 * get the context previous set in the store of the current execution
 */
function get() {
    if (!enabled) {
        console.trace("[envoy-node] cannot get context when store is not enabled.");
        return undefined;
    }
    const asyncId = asyncHooks.executionAsyncId();
    const context = getContext(asyncId);
    /* istanbul ignore next */
    if (context === undefined) {
        console.trace("[envoy-node] Cannot find info of current execution, have you enabled and set the context store correctly?");
    }
    return context;
}
/**
 * return if the store is enabled
 */
function isEnabled() {
    return enabled;
}
/**
 * return the instance of the store
 */
function getStoreImpl() {
    return store;
}
/**
 * return debug info about the store
 */
function getDebugInfo() {
    return `current size: ${store.size()}, old size: ${store.oldSize()}, not destroyed hooks: ${notDestroyedHooks}`;
}
var envoyContextStore = {
    enable,
    disable,
    set,
    get,
    isEnabled,
    getStoreImpl,
    getEliminateInterval,
    setEliminateInterval,
    getDebugInfo
};

export { EnvoyContext, envoyFetch, EnvoyGrpcRequestParams, GrpcRetryOn, EnvoyHttpClient, EnvoyHttpRequestParams, HttpRetryOn, envoyProtoDecorator, envoyRequestParamsRefiner, envoyContextStore };
//# sourceMappingURL=envoy-node.es5.js.map
