"use strict";
/* Socket Lib - Built with rolldown */
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_primordials_error = require('../primordials/error.js');
const require_primordials_math = require('../primordials/math.js');
const require_primordials_array = require('../primordials/array.js');
const require_primordials_date = require('../primordials/date.js');
const require_primordials_json = require('../primordials/json.js');
const require_primordials_promise = require('../primordials/promise.js');
const require_http_request_browser_fetch = require('./browser-fetch.js');

//#region src/http-request/browser.ts
/**
* @file Browser-safe HTTP request layer — mirrors the public surface of
*   `@socketsecurity/lib/http-request` (`httpJson`, `httpText`, `httpRequest`,
*   `HttpResponseError`) but uses the browser's `fetch` API instead of Node's
*   `node:https`. Designed for Chrome MV3 service workers, content scripts,
*   popups, and any other browser context that doesn't have `node:http` /
*   `node:https` / `node:stream`. Consumers import from
*   `@socketsecurity/lib/http-request/browser` directly, OR from
*   `@socketsecurity/lib/http-request` inside a bundler that resolves the
*   `browser` package.json conditional (rolldown, vite, esbuild) — the bundler
*   picks this entry automatically. API parity with the Node side is the goal —
*   same function names, same option shapes (where browsers can support them),
*   same error shape. Caveats:
*
*   - `HttpResponse.body` is `Uint8Array` here, vs Node's `Buffer`. Most callers
*     use `arrayBuffer()` / `text()` / `json()` and don't care.
*   - `HttpResponse.headers` is `Record<string, string>` here, vs Node's
*     `IncomingHttpHeaders` (which has array-valued headers like `set-cookie`).
*     Browser `fetch()` flattens repeated headers per spec.
*   - Hooks (`onRequest` / `onResponse`) are not yet supported in the browser
*     path. Add when needed.
*/
/**
* Browser-side HTTP error. Mirrors the Node-side `HttpResponseError` shape
* (same `.name`, same `.response.status/statusText` access pattern, same
* `instanceof` semantics) but constructs from a `BrowserHttpResponse` instead
* of a Node `HttpResponse`.
*/
var HttpResponseError = class extends Error {
	response;
	constructor(response, message) {
		const statusCode = response.status ?? "unknown";
		const statusMessage = response.statusText || "No status message";
		super(message ?? `HTTP ${statusCode}: ${statusMessage}`);
		this.name = "HttpResponseError";
		this.response = response;
	}
};
/**
* Combine an external `signal` with an internal `timeout`-driven
* AbortController so either can cancel the in-flight fetch.
*/
function combineSignals(external, timeoutMs) {
	if (!timeoutMs) return {
		signal: external,
		cleanup: () => {}
	};
	const controller = new AbortController();
	const timer = setTimeout(() => controller.abort(), timeoutMs);
	let externalListener;
	if (external) if (external.aborted) controller.abort();
	else {
		externalListener = () => controller.abort();
		external.addEventListener("abort", externalListener);
	}
	return {
		signal: controller.signal,
		cleanup: () => {
			clearTimeout(timer);
			if (external && externalListener) external.removeEventListener("abort", externalListener);
		}
	};
}
async function attempt(url, options) {
	const method = options.method ?? "GET";
	const init = { method };
	if (options.headers) init.headers = options.headers;
	if (options.body !== void 0) init.body = options.body;
	if (options.followRedirects === false) init.redirect = "manual";
	const { signal, cleanup } = combineSignals(options.signal, options.timeout);
	if (signal) init.signal = signal;
	const startedAt = require_primordials_date.DateNow();
	if (options.hooks?.onRequest) options.hooks.onRequest({
		method,
		url,
		headers: options.headers,
		timeout: options.timeout
	});
	try {
		const response = await require_http_request_browser_fetch.doFetch(url, init);
		const buffer = await response.arrayBuffer();
		if (options.maxResponseSize !== void 0 && buffer.byteLength > options.maxResponseSize) throw new require_primordials_error.ErrorCtor(`Response body (${buffer.byteLength} bytes) exceeds maxResponseSize (${options.maxResponseSize})`);
		const body = new require_primordials_array.Uint8ArrayCtor(buffer);
		const headers = headersToRecord(response.headers);
		if (options.hooks?.onResponse) options.hooks.onResponse({
			method,
			url,
			duration: require_primordials_date.DateNow() - startedAt,
			status: response.status,
			statusText: response.statusText,
			headers
		});
		return {
			body,
			headers,
			status: response.status,
			statusText: response.statusText,
			ok: response.ok,
			url: response.url,
			arrayBuffer() {
				return buffer;
			},
			text() {
				return decodeText(body);
			},
			json() {
				return require_primordials_json.JSONParse(decodeText(body));
			}
		};
	} catch (err) {
		if (options.hooks?.onResponse) options.hooks.onResponse({
			method,
			url,
			duration: require_primordials_date.DateNow() - startedAt,
			error: err instanceof Error ? err : new require_primordials_error.ErrorCtor(String(err))
		});
		throw err;
	} finally {
		cleanup();
	}
}
function decodeText(bytes) {
	return new TextDecoder("utf-8").decode(bytes);
}
function headersToRecord(headers) {
	const out = {};
	headers.forEach((value, key) => {
		out[key.toLowerCase()] = value;
	});
	return out;
}
/**
* GET / POST a JSON endpoint. Automatically sets `Accept: application/json` and
* `Content-Type: application/json` (when a body is present). Throws
* `HttpResponseError` on non-2xx.
*/
async function httpJson(url, options) {
	const opts = options ?? {};
	const headers = {
		Accept: "application/json",
		...opts.headers ?? {}
	};
	if (opts.body !== void 0 && !("Content-Type" in headers)) headers["Content-Type"] = "application/json";
	return (await httpRequest(url, {
		...opts,
		headers,
		throwOnError: true
	})).json();
}
/**
* Lower-level HTTP request. Use `httpJson` / `httpText` for the common cases.
* Returns the response unconditionally — callers inspect `.ok` or pass
* `throwOnError: true` to get a thrown `HttpResponseError` on non-2xx.
*/
async function httpRequest(url, options) {
	const opts = options ?? {};
	const maxAttempts = (opts.retries ?? 0) + 1;
	const baseDelay = opts.retryDelay ?? 250;
	let lastError;
	for (let i = 0; i < maxAttempts; i++) try {
		const response = await attempt(url, opts);
		if (response.status >= 500 && i + 1 < maxAttempts) {
			await sleep(baseDelay * require_primordials_math.MathPow(2, i));
			continue;
		}
		if (opts.throwOnError && !response.ok) throw new HttpResponseError(response);
		return response;
	} catch (err) {
		lastError = err;
		if (err instanceof HttpResponseError) throw err;
		if (i + 1 < maxAttempts) {
			await sleep(baseDelay * require_primordials_math.MathPow(2, i));
			continue;
		}
	}
	throw lastError instanceof Error ? lastError : new require_primordials_error.ErrorCtor(`HTTP request to ${url} failed`);
}
/**
* GET / POST a text endpoint. Throws `HttpResponseError` on non-2xx.
*/
async function httpText(url, options) {
	return (await httpRequest(url, {
		...options ?? {},
		throwOnError: true
	})).text();
}
function sleep(ms) {
	return new require_primordials_promise.PromiseCtor((resolve) => setTimeout(resolve, ms));
}

//#endregion
exports.HttpResponseError = HttpResponseError;
exports.attempt = attempt;
exports.combineSignals = combineSignals;
exports.decodeText = decodeText;
exports.headersToRecord = headersToRecord;
exports.httpJson = httpJson;
exports.httpRequest = httpRequest;
exports.httpText = httpText;
exports.sleep = sleep;