/**
 * Stream lifecycle management utilities.
 *
 * Provides utilities for wrapping streams with lifecycle hooks, particularly
 * `onFinally` which runs after stream consumption completes.
 *
 * Key challenge: The `streamText()` function returns immediately, but the
 * streams are consumed asynchronously later. We need to track when consumption
 * actually completes to run cleanup logic.
 *
 * Solution: Wrap streams with `TransformStream` that tracks consumption via
 * the `flush()` method, which runs when the input stream closes.
 */
import { getErrorMeta, getLogger } from "../../../util/logger.js";
import { isNoOutputError } from "../error.js";
/**
 * Wraps a StreamTextResult to add onFinally lifecycle hook.
 *
 * The onFinally callback runs after stream consumption completes, handling:
 * - Normal completion (stream exhausted)
 * - Errors during consumption
 * - Stream cancellation/abandonment
 *
 * Guarantees:
 * - Runs exactly once, even if multiple stream accessors are used
 * - Idempotent (safe to call multiple times)
 * - Works with all consumption patterns (iteration, promises, piping)
 *
 * @param result - The stream result to wrap
 * @param onFinally - Callback to run after consumption
 * @returns Wrapped result with lifecycle tracking
 *
 * @example
 * ```typescript
 * const result = await streamText({ model, messages });
 * const wrapped = withStreamLifecycle(result, async () => {
 *   console.log('Stream consumed!');
 *   await cleanup();
 * });
 *
 * for await (const text of wrapped.textStream) {
 *   console.log(text);
 * }
 * // onFinally runs here ^
 * ```
 */
export function withStreamLifecycle(result, onFinally) {
    // Track whether finalization has run
    let finalized = false;
    /**
     * Ensures onFinally runs exactly once.
     * Safe to call multiple times (idempotent).
     */
    const ensureFinally = async () => {
        if (finalized)
            return;
        finalized = true;
        try {
            await onFinally();
        }
        catch (error) {
            // Log but don't throw - we're in cleanup, don't break the stream
            // This catch is in ensureFinally, which is called from flush()
            getLogger().error("Error in stream lifecycle ensureFinally", getErrorMeta(error));
        }
    };
    // Wrap textStream to track consumption
    const wrappedTextStream = wrapAsyncIterable(result.textStream, ensureFinally);
    // Wrap fullStream to track consumption
    const wrappedFullStream = wrapAsyncIterable(result.fullStream, ensureFinally);
    // Return result with wrapped streams
    // Spread works because adapter materialized getters into enumerable properties
    return {
        ...result,
        textStream: wrappedTextStream,
        fullStream: wrappedFullStream,
    };
}
/**
 * Wraps an async iterable with a finalization callback.
 *
 * Uses TransformStream to intercept the stream lifecycle:
 * - `transform()`: Passes chunks through unchanged
 * - `flush()`: Called when input stream closes (after all chunks processed)
 * - `cancel()`: Called if stream is cancelled/abandoned
 *
 * The finalization callback runs in both flush() and cancel() to ensure
 * cleanup happens whether the stream completes normally or is abandoned.
 *
 * @param source - The async iterable to wrap
 * @param onFinalize - Callback to run after consumption
 * @returns Wrapped async iterable
 */
function wrapAsyncIterable(source, onFinalize) {
    // Convert async iterable to ReadableStream if needed
    let sourceStream;
    if (source instanceof ReadableStream) {
        sourceStream = source;
    }
    else {
        // Manually convert async iterable to ReadableStream
        sourceStream = new ReadableStream({
            async start(controller) {
                try {
                    for await (const chunk of source) {
                        controller.enqueue(chunk);
                    }
                    controller.close();
                }
                catch (error) {
                    controller.error(error);
                }
            },
        });
    }
    // Create transform stream that tracks lifecycle
    const lifecycleTransform = new TransformStream({
        // Pass through all chunks unchanged
        transform(chunk, controller) {
            controller.enqueue(chunk);
        },
        // Called when input stream closes (after all chunks processed)
        async flush(controller) {
            try {
                await onFinalize();
            }
            catch (error) {
                // Handle errors in onFinally, including AI_NoOutputGeneratedError from Vercel AI SDK
                // This can happen when the stream completes without generating output
                // We don't want to propagate these errors as unhandled rejections
                if (isNoOutputError(error)) {
                    getLogger().warn(`AI stream completed without output (this may be expected): ${error instanceof Error ? error.message : String(error)}`);
                }
                else {
                    // For other errors, log and rethrow so they can be handled upstream
                    getLogger().error("Error in stream lifecycle onFinally", getErrorMeta(error));
                    controller.error(error);
                }
            }
        },
    });
    // Pipe source through lifecycle tracker
    const wrappedStream = sourceStream.pipeThrough(lifecycleTransform);
    // Return as AsyncIterableStream (has both stream and iterable interfaces)
    return wrappedStream;
}
//# sourceMappingURL=stream-lifecycle.js.map