"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DestroyableStream = exports.HeadersImpl = exports.Response = exports.NO_FETCH_TELEMETRY = exports.IFetcherService = void 0;
exports.jsonVerboseError = jsonVerboseError;
exports.isAbortError = isAbortError;
exports.safeGetHostname = safeGetHostname;
const services_1 = require("../../../util/common/services");
exports.IFetcherService = (0, services_1.createServiceIdentifier)('IFetcherService');
/** Use as the callSite value to suppress fetch telemetry for a request (e.g. from the telemetry service itself). */
exports.NO_FETCH_TELEMETRY = 'NO_FETCH_TELEMETRY';
/** A basic version of http://developer.mozilla.org/en-US/docs/Web/API/Response */
class Response {
    get bytesReceived() {
        return this._bytesReceived;
    }
    constructor(status, statusText, headers, body, fetcher, _reportEvent, _internalId, _hostname) {
        this.status = status;
        this.statusText = statusText;
        this.headers = headers;
        this.fetcher = fetcher;
        this._reportEvent = _reportEvent;
        this._internalId = _internalId;
        this._hostname = _hostname;
        this.ok = this.status >= 200 && this.status < 300;
        this._bytesReceived = 0;
        const transformer = {
            transform: (chunk, controller) => {
                this._bytesReceived += chunk.length;
                controller.enqueue(chunk);
            },
            flush: () => {
                this._reportEvent({ internalId: this._internalId, timestamp: Date.now(), outcome: 'success', phase: 'responseStreaming', fetcher: this.fetcher, hostname: this._hostname, bytesReceived: this._bytesReceived });
            },
            cancel: (reason) => {
                const outcome = reason && !isAbortError(reason) ? 'error' : 'cancel';
                this._reportEvent({ internalId: this._internalId, timestamp: Date.now(), outcome, phase: 'responseStreaming', fetcher: this.fetcher, hostname: this._hostname, reason, bytesReceived: this._bytesReceived });
            }
        };
        const countingStream = new TransformStream(transformer);
        const inputStream = body ?? new ReadableStream({ start(c) { c.close(); } });
        this.body = new DestroyableStream(inputStream.pipeThrough(countingStream));
    }
    static fromText(status, statusText, headers, body, fetcher) {
        return new Response(status, statusText, headers, new ReadableStream({
            start(controller) {
                controller.enqueue(new TextEncoder().encode(body));
                controller.close();
            }
        }), fetcher, () => { }, 'in-memory', 'in-memory');
    }
    async text() {
        const chunks = [];
        for await (const chunk of this.body) {
            chunks.push(chunk);
        }
        const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
        const result = new Uint8Array(totalLength);
        let offset = 0;
        for (const chunk of chunks) {
            result.set(chunk, offset);
            offset += chunk.length;
        }
        return new TextDecoder().decode(result);
    }
    async json() {
        return JSON.parse(await this.text());
    }
}
exports.Response = Response;
class HeadersImpl {
    constructor(_record) {
        this._record = _record;
    }
    static fromMap(map) {
        return new HeadersImpl(Object.fromEntries(map));
    }
    get(name) {
        const result = this._record[name];
        return Array.isArray(result) ? result[0] : result ?? null;
    }
    [Symbol.iterator]() {
        const keys = Object.keys(this._record);
        let index = 0;
        return {
            next: () => {
                if (index >= keys.length) {
                    return { done: true, value: undefined };
                }
                const key = keys[index++];
                return { done: false, value: [key, this.get(key)] };
            }
        };
    }
}
exports.HeadersImpl = HeadersImpl;
/**
 * Wraps a ReadableStream to allow cancellation even while a `for await` loop
 * holds the stream locked. Use `destroy()` to safely cancel from an external
 * callback (e.g., `onReturn`) - it cancels through the reader if locked.
 *
 * When `pipeThrough()` is called, destroy() will forward to the piped stream.
 */
class DestroyableStream {
    constructor(stream) {
        this.stream = stream;
    }
    /**
     * Returns the underlying ReadableStream for APIs that require it
     * (e.g., Readable.fromWeb). Use with caution as operations on the
     * returned stream bypass the DestroyableStream's reader tracking.
     */
    toReadableStream() {
        return this.stream;
    }
    /**
     * Pipes this stream through a transform stream.
     * Returns a new DestroyableStream wrapping the transformed stream.
     * Calling destroy() on this stream will forward to the piped stream.
     */
    pipeThrough(transform) {
        const piped = new DestroyableStream(this.stream.pipeThrough(transform));
        this.pipedHead = piped;
        return piped;
    }
    async *[Symbol.asyncIterator]() {
        this.reader = this.stream.getReader();
        try {
            while (true) {
                const { done, value } = await this.reader.read();
                if (done) {
                    break;
                }
                yield value;
            }
        }
        finally {
            this.reader.releaseLock();
            this.reader = undefined;
        }
    }
    destroy() {
        // Forward to piped stream if pipeThrough was called
        if (this.pipedHead) {
            return this.pipedHead.destroy();
        }
        if (this.reader) {
            // Cancels the underlying stream and releases the lock
            return this.reader.cancel();
        }
        else {
            // If stream was consumed and unlocked, cancel() is a no-op
            return this.stream.cancel();
        }
    }
}
exports.DestroyableStream = DestroyableStream;
async function jsonVerboseError(resp) {
    const text = await resp.text();
    try {
        return JSON.parse(text);
    }
    catch (err) {
        const lines = text.split('\n');
        const errText = lines.length > 50 ? [...lines.slice(0, 25), '[...]', ...lines.slice(lines.length - 25)].join('\n') : text;
        err.message = `${err.message}. Response: ${errText}`;
        throw err;
    }
}
function isAbortError(e) {
    // see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
    return e && e.name === 'AbortError';
}
function safeGetHostname(url) {
    try {
        return new URL(url).hostname;
    }
    catch {
        return 'unknown';
    }
}
//# sourceMappingURL=fetcherService.js.map