import { isNetworkError } from "./error.js";
import { reconnectDelayMs } from "./reconnect.js";
//#region src/utils/stream.ts
/**
* Error thrown when maximum reconnection attempts are exceeded.
*/
var MaxReconnectAttemptsError = class extends Error {
	constructor(maxAttempts, cause) {
		super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);
		this.name = "MaxReconnectAttemptsError";
		this.cause = cause;
	}
};
/**
* Error injected into the stream by {@link idleReconnectStream} when no lines
* arrive within the active idle window. Surfacing this during the read is what
* lets the reconnect loops in `streamWithRetry` and the protocol SSE transport
* recover from a half-open socket — one that was silently dropped (e.g. a hard
* pod kill on a platform revision rollover) without a TCP FIN/RST, so neither
* a `done` nor a thrown network error ever arrives.
*/
var StreamIdleTimeoutError = class extends Error {
	idleTimeoutMs;
	constructor(idleTimeoutMs) {
		super(`No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`);
		this.name = "StreamIdleTimeoutError";
		this.idleTimeoutMs = idleTimeoutMs;
	}
};
/** `":"` — first byte of an SSE comment / keep-alive line. */
const SSE_COMMENT_BYTE = 58;
/**
* Default idle-reconnect policy for SSE streams. Heartbeat-adaptive
* `"auto"` stays dormant unless the server emits keep-alive heartbeats.
* Pass `0` to disable.
*/
const DEFAULT_IDLE_RECONNECT = "auto";
/**
* A pass-through {@link TransformStream} that errors the stream when it goes
* idle, so the surrounding reconnect logic can recover a half-open socket.
*
* MUST sit on the *line* stream — i.e. after
* {@link import("./sse.js").BytesLineDecoder} but before
* {@link import("./sse.js").SSEDecoder} (which discards `:` comment lines).
* Operating at the line level lets the watchdog both (a) reset on any line
* (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines
* to drive `"auto"` mode.
*
* In `"auto"` mode the watchdog is intentionally dormant until it has seen at
* least two heartbeats (so it can measure the cadence). This means a socket
* that dies inside the first heartbeat interval won't be caught until a
* heartbeat would have been due — an acceptable trade for never false-firing
* on heartbeat-less servers. Pass a fixed `number` if you need coverage from
* the very first byte.
*/
function idleReconnectStream(options) {
	const factor = options.timeoutFactor ?? 3;
	const minTimeoutMs = options.minTimeoutMs ?? 6e3;
	const maxTimeoutMs = options.maxTimeoutMs ?? 3e4;
	const fixedTimeoutMs = typeof options.mode === "number" ? options.mode : null;
	let timer;
	let controllerRef;
	let lastHeartbeatAt;
	let derivedTimeoutMs = fixedTimeoutMs;
	const clear = () => {
		if (timer != null) {
			clearTimeout(timer);
			timer = void 0;
		}
	};
	const arm = () => {
		clear();
		const timeoutMs = derivedTimeoutMs;
		if (timeoutMs == null || timeoutMs <= 0) return;
		timer = setTimeout(() => {
			options.onIdle?.({
				timeoutMs,
				source: fixedTimeoutMs != null ? "fixed" : "heartbeat"
			});
			try {
				controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));
			} catch {}
		}, timeoutMs);
		timer.unref?.();
	};
	const noteHeartbeat = () => {
		if (fixedTimeoutMs != null) return;
		const now = Date.now();
		if (lastHeartbeatAt != null) {
			const interval = now - lastHeartbeatAt;
			if (interval > 0) {
				const candidate = Math.min(Math.max(interval * factor, minTimeoutMs), maxTimeoutMs);
				derivedTimeoutMs = derivedTimeoutMs == null ? candidate : Math.max(derivedTimeoutMs, candidate);
			}
		}
		lastHeartbeatAt = now;
	};
	return new TransformStream({
		start(controller) {
			controllerRef = controller;
			arm();
		},
		transform(line, controller) {
			if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) noteHeartbeat();
			arm();
			controller.enqueue(line);
		},
		flush() {
			clear();
		}
	});
}
/**
* Stream with automatic retry logic for SSE connections.
* Implements reconnection behavior similar to the Python SDK.
*
* @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.
*                    When `params.reconnectPath` is provided, it's a reconnection request.
* @param options Configuration options
* @returns AsyncGenerator yielding stream events
*/
async function* streamWithRetry(makeRequest, options = {}) {
	const maxRetries = options.maxRetries ?? 5;
	let attempt = 0;
	let lastEventId;
	let reconnectPath;
	while (true) {
		let shouldRetry = false;
		let lastError;
		let reader;
		try {
			if (options.signal?.aborted) return;
			const { response, stream } = await makeRequest(reconnectPath ? {
				lastEventId,
				reconnectPath
			} : void 0);
			const locationHeader = response.headers.get("location");
			if (locationHeader) reconnectPath = locationHeader;
			const contentType = response.headers.get("content-type")?.split(";")[0];
			if (contentType && !contentType.includes("text/event-stream")) throw new Error(`Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`);
			reader = stream.getReader();
			try {
				while (true) {
					if (options.signal?.aborted) {
						await reader.cancel();
						return;
					}
					const { done, value } = await reader.read();
					if (done) break;
					if (value.id) lastEventId = value.id;
					yield value;
				}
				break;
			} catch (error) {
				if (reconnectPath && !options.signal?.aborted) shouldRetry = true;
				else throw error;
			} finally {
				if (reader) try {
					reader.releaseLock();
				} catch {}
			}
		} catch (error) {
			lastError = error;
			if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) shouldRetry = true;
			else throw error;
		}
		if (shouldRetry) {
			attempt += 1;
			if (attempt > maxRetries) throw new MaxReconnectAttemptsError(maxRetries, lastError);
			options.onReconnect?.({
				attempt,
				lastEventId,
				cause: lastError
			});
			const delay = reconnectDelayMs(attempt);
			await new Promise((resolve) => {
				setTimeout(resolve, delay);
			});
			continue;
		}
		break;
	}
}
var IterableReadableStream = class IterableReadableStream extends ReadableStream {
	reader;
	ensureReader() {
		if (!this.reader) this.reader = this.getReader();
	}
	async next() {
		this.ensureReader();
		try {
			const result = await this.reader.read();
			if (result.done) {
				this.reader.releaseLock();
				return {
					done: true,
					value: void 0
				};
			} else return {
				done: false,
				value: result.value
			};
		} catch (e) {
			this.reader.releaseLock();
			throw e;
		}
	}
	async return() {
		this.ensureReader();
		if (this.locked) {
			const cancelPromise = this.reader.cancel();
			this.reader.releaseLock();
			await cancelPromise;
		}
		return {
			done: true,
			value: void 0
		};
	}
	async throw(e) {
		this.ensureReader();
		if (this.locked) {
			const cancelPromise = this.reader.cancel();
			this.reader.releaseLock();
			await cancelPromise;
		}
		throw e;
	}
	async [Symbol.asyncDispose]() {
		await this.return();
	}
	[Symbol.asyncIterator]() {
		return this;
	}
	static fromReadableStream(stream) {
		const reader = stream.getReader();
		return new IterableReadableStream({
			start(controller) {
				return pump();
				function pump() {
					return reader.read().then(({ done, value }) => {
						if (done) {
							controller.close();
							return;
						}
						controller.enqueue(value);
						return pump();
					});
				}
			},
			cancel() {
				reader.releaseLock();
			}
		});
	}
	static fromAsyncGenerator(generator) {
		return new IterableReadableStream({
			async pull(controller) {
				const { value, done } = await generator.next();
				if (done) controller.close();
				controller.enqueue(value);
			},
			async cancel(reason) {
				await generator.return(reason);
			}
		});
	}
};
//#endregion
export { DEFAULT_IDLE_RECONNECT, IterableReadableStream, idleReconnectStream, streamWithRetry };

//# sourceMappingURL=stream.js.map