import { AbortController } from '@solana/event-target-impl';
import type { Commitment } from '@solana/rpc-types';

type Config = Readonly<{
    abortSignal: AbortSignal;
    /**
     * The timeout promise will throw after 30 seconds when the commitment is `processed`, and 60
     * seconds otherwise.
     */
    commitment: Commitment;
}>;

/**
 * When no other heuristic exists to infer that a transaction has expired, you can use this promise
 * factory with a commitment level. It throws after 30 seconds when the commitment is `processed`,
 * and 60 seconds otherwise. You would typically race this with another confirmation strategy.
 *
 * @param config
 *
 * @example
 * ```ts
 * import { safeRace } from '@solana/promises';
 * import { getTimeoutPromise } from '@solana/transaction-confirmation';
 *
 * try {
 *     await safeRace([getCustomTransactionConfirmationPromise(/* ... *\/), getTimeoutPromise({ commitment })]);
 * } catch (e) {
 *     if (e instanceof DOMException && e.name === 'TimeoutError') {
 *         console.log('Could not confirm transaction after a timeout');
 *     }
 *     throw e;
 * }
 * ```
 */
export async function getTimeoutPromise({ abortSignal: callerAbortSignal, commitment }: Config) {
    const abortController = new AbortController();
    let timeoutId: ReturnType<typeof setTimeout> | undefined;
    try {
        return await new Promise((_, reject) => {
            const handleAbort = () => {
                clearTimeout(timeoutId);
                const abortError = new DOMException(callerAbortSignal.reason, 'AbortError');
                reject(abortError);
            };
            callerAbortSignal.addEventListener('abort', handleAbort, { signal: abortController.signal });
            const timeoutMs = commitment === 'processed' ? 30_000 : 60_000;
            const startMs = performance.now();
            timeoutId =
                // We use `setTimeout` instead of `AbortSignal.timeout()` because we want to measure
                // elapsed time instead of active time.
                // See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
                setTimeout(() => {
                    const elapsedMs = performance.now() - startMs;
                    reject(new DOMException(`Timeout elapsed after ${elapsedMs} ms`, 'TimeoutError'));
                }, timeoutMs);
        });
    } finally {
        clearTimeout(timeoutId);
        abortController.abort();
    }
}
