import { AsyncCaller } from "../utils/async_caller.js";
import { getEnvironmentVariable } from "../utils/env.js";
import { mergeSignals } from "../utils/signals.js";
import { BytesLineDecoder, SSEDecoder } from "../utils/sse.js";
import { idleReconnectStream, streamWithRetry } from "../utils/stream.js";
//#region src/client/base.ts
function* iterateHeaders(headers) {
	let iter;
	let shouldClear = false;
	if (headers instanceof Headers) {
		const entries = [];
		headers.forEach((value, name) => {
			entries.push([name, value]);
		});
		iter = entries;
	} else if (Array.isArray(headers)) iter = headers;
	else {
		shouldClear = true;
		iter = Object.entries(headers ?? {});
	}
	for (const item of iter) {
		const name = item[0];
		if (typeof name !== "string") throw new TypeError(`Expected header name to be a string, got ${typeof name}`);
		const values = Array.isArray(item[1]) ? item[1] : [item[1]];
		let didClear = false;
		for (const value of values) {
			if (value === void 0) continue;
			if (shouldClear && !didClear) {
				didClear = true;
				yield [name, null];
			}
			yield [name, value];
		}
	}
}
function mergeHeaders(...headerObjects) {
	const outputHeaders = new Headers();
	for (const headers of headerObjects) {
		if (!headers) continue;
		for (const [name, value] of iterateHeaders(headers)) if (value === null) outputHeaders.delete(name);
		else outputHeaders.append(name, value);
	}
	const headerEntries = [];
	outputHeaders.forEach((value, name) => {
		headerEntries.push([name, value]);
	});
	return Object.fromEntries(headerEntries);
}
/**
* Get the API key from the environment.
* Precedence:
*   1. explicit argument (if string)
*   2. LANGGRAPH_API_KEY
*   3. LANGSMITH_API_KEY
*   4. LANGCHAIN_API_KEY
*
* @param apiKey - API key provided as an argument. If null, skips environment lookup. If undefined, tries environment.
* @returns The API key if found, otherwise undefined
*/
function getApiKey(apiKey) {
	if (apiKey === null) return;
	if (apiKey) return apiKey;
	for (const prefix of [
		"LANGGRAPH",
		"LANGSMITH",
		"LANGCHAIN"
	]) {
		const envKey = getEnvironmentVariable(`${prefix}_API_KEY`);
		if (envKey) return envKey.trim().replace(/^["']|["']$/g, "");
	}
}
var BaseClient = class {
	asyncCaller;
	timeoutMs;
	apiUrl;
	defaultHeaders;
	onRequest;
	streamProtocol;
	constructor(config) {
		const callerOptions = {
			maxRetries: 4,
			maxConcurrency: 4,
			...config?.callerOptions
		};
		let defaultApiUrl = "http://localhost:8123";
		if (!config?.apiUrl && typeof globalThis === "object" && globalThis != null) {
			const fetchSmb = Symbol.for("langgraph_api:fetch");
			const urlSmb = Symbol.for("langgraph_api:url");
			const global = globalThis;
			if (global[fetchSmb]) callerOptions.fetch ??= global[fetchSmb];
			if (global[urlSmb]) defaultApiUrl = global[urlSmb];
		}
		this.asyncCaller = new AsyncCaller(callerOptions);
		this.timeoutMs = config?.timeoutMs;
		this.apiUrl = config?.apiUrl?.replace(/\/$/, "") || defaultApiUrl;
		this.defaultHeaders = config?.defaultHeaders || {};
		this.onRequest = config?.onRequest;
		this.streamProtocol = config?.streamProtocol ?? "legacy";
		const apiKey = getApiKey(config?.apiKey);
		if (apiKey) this.defaultHeaders["x-api-key"] = apiKey;
	}
	prepareFetchOptions(path, options) {
		const mutatedOptions = {
			...options,
			headers: mergeHeaders(this.defaultHeaders, options?.headers)
		};
		if (mutatedOptions.json) {
			mutatedOptions.body = JSON.stringify(mutatedOptions.json);
			mutatedOptions.headers = mergeHeaders(mutatedOptions.headers, { "content-type": "application/json" });
			delete mutatedOptions.json;
		}
		if (mutatedOptions.withResponse) delete mutatedOptions.withResponse;
		if ("dedupe" in mutatedOptions) delete mutatedOptions.dedupe;
		let timeoutSignal = null;
		if (typeof options?.timeoutMs !== "undefined") {
			if (options.timeoutMs != null) timeoutSignal = AbortSignal.timeout(options.timeoutMs);
		} else if (this.timeoutMs != null) timeoutSignal = AbortSignal.timeout(this.timeoutMs);
		mutatedOptions.signal = mergeSignals(timeoutSignal, mutatedOptions.signal);
		const targetUrl = new URL(`${this.apiUrl}${path}`);
		if (mutatedOptions.params) {
			for (const [key, value] of Object.entries(mutatedOptions.params)) {
				if (value == null) continue;
				const strValue = typeof value === "string" || typeof value === "number" ? value.toString() : JSON.stringify(value);
				targetUrl.searchParams.append(key, strValue);
			}
			delete mutatedOptions.params;
		}
		return [targetUrl, mutatedOptions];
	}
	async fetch(path, options) {
		const [url, init] = this.prepareFetchOptions(path, options);
		if (options?.dedupe === true && options?.withResponse !== true && options?.signal == null && this.onRequest == null) {
			const body = typeof init.body === "string" ? init.body : "";
			/**
			* The key must capture the FULL request identity, including every
			* prepared header. `inFlightReads` is module-scoped across all
			* `Client` instances, so omitting headers would let two clients
			* pointed at the same URL/thread but using different credentials
			* (Authorization, custom auth headers, tenant-scoping defaults, …)
			* share one in-flight promise — a cross-tenant data leak.
			*/
			const headers = serializeHeaders(init.headers);
			const key = `${init.method ?? "GET"} ${url.toString()} ${body} ${headers}`;
			const existing = inFlightReads.get(key);
			if (existing != null) return existing;
			const promise = this.#performFetch(url, init);
			inFlightReads.set(key, promise);
			const clear = () => {
				if (inFlightReads.get(key) === promise) inFlightReads.delete(key);
			};
			promise.then(clear, clear);
			return promise;
		}
		const [body, response] = await this.#performFetchWithResponse(url, init);
		if (options?.withResponse) return [body, response];
		return body;
	}
	/**
	* Issue the prepared request (applying the `onRequest` hook) and
	* resolve the parsed body. Shared by the deduped and direct paths.
	*/
	async #performFetch(url, init) {
		const [body] = await this.#performFetchWithResponse(url, init);
		return body;
	}
	async #performFetchWithResponse(url, init) {
		let finalInit = init;
		if (this.onRequest) finalInit = await this.onRequest(url, init);
		const response = await this.asyncCaller.fetch(url.toString(), finalInit);
		return [await (async () => {
			if (response.status === 202 || response.status === 204) return;
			return response.json();
		})(), response];
	}
	async *streamWithRetry(config) {
		const makeRequest = async (reconnectParams) => {
			const requestEndpoint = reconnectParams?.reconnectPath || config.endpoint;
			const isReconnect = !!reconnectParams?.reconnectPath;
			const method = isReconnect ? "GET" : config.method || "GET";
			const requestHeaders = isReconnect && reconnectParams?.lastEventId ? {
				...config.headers,
				"Last-Event-ID": reconnectParams.lastEventId
			} : config.headers;
			let [url, init] = this.prepareFetchOptions(requestEndpoint, {
				method,
				timeoutMs: null,
				signal: config.signal,
				headers: requestHeaders,
				params: config.params,
				json: isReconnect ? void 0 : config.json
			});
			if (this.onRequest != null) init = await this.onRequest(url, init);
			const response = await this.asyncCaller.fetch(url.toString(), init);
			if (!response.body) throw new Error("Expected response body from stream endpoint");
			if (!isReconnect && config.onInitialResponse) await config.onInitialResponse(response);
			const idleMode = config.idleReconnect ?? "auto";
			const enableIdle = idleMode === "auto" || idleMode > 0;
			const lines = response.body.pipeThrough(BytesLineDecoder());
			return {
				response,
				stream: (enableIdle ? lines.pipeThrough(idleReconnectStream({ mode: idleMode })) : lines).pipeThrough(SSEDecoder())
			};
		};
		yield* streamWithRetry(makeRequest, {
			maxRetries: config.maxRetries ?? 5,
			signal: config.signal,
			onReconnect: config.onReconnect
		});
	}
};
const REGEX_RUN_METADATA = /(\/threads\/(?<thread_id>.+))?\/runs\/(?<run_id>.+)/;
function getRunMetadataFromResponse(response) {
	const contentLocation = response.headers.get("Content-Location");
	if (!contentLocation) return void 0;
	const match = REGEX_RUN_METADATA.exec(contentLocation);
	if (!match?.groups?.run_id) return void 0;
	return {
		run_id: match.groups.run_id,
		thread_id: match.groups.thread_id || void 0
	};
}
/**
* Module-scoped, in-flight-only coalescing map for idempotent reads.
*
* Two independently-constructed clients (e.g. a React component that
* remounts under Suspense / a reachability state flip, each minting a
* fresh `Client`) can fire the *same* `getState` / `getHistory` read a
* few milliseconds apart, before the first has resolved. Without
* coalescing each pays the full round-trip — the duplicate
* `threads/{id}/state` and `threads/{id}/history` requests seen on
* reconnect.
*
* Keyed by `method + url + body + auth`, entries live only while a
* request is in flight and are removed the moment it settles. This is
* deliberately *not* a result cache: there is no TTL and no stored
* payload, so it cannot serve stale data — it only ever shares a
* promise that is already on the wire. Opt-in per call via
* `{ dedupe: true }`, and skipped whenever the caller supplies its own
* `AbortSignal` (so one consumer aborting can never cancel another's
* read).
*/
const inFlightReads = /* @__PURE__ */ new Map();
/**
* Deterministically serialize a prepared request's headers into a
* stable string for use in the {@link inFlightReads} dedupe key. Header
* names are normalized and sorted so ordering differences never produce
* a different key, and every header (not just `x-api-key`) is included
* so requests carrying different credentials never collide.
*/
function serializeHeaders(headers) {
	const normalized = mergeHeaders(headers);
	return Object.keys(normalized).sort().map((name) => `${name}:${normalized[name]}`).join("\n");
}
//#endregion
export { BaseClient, getApiKey, getRunMetadataFromResponse };

//# sourceMappingURL=base.js.map