type MaybePromise<T> = T | Promise<T>;

export const DEFAULT_ERROR_REPORTING_TIMEOUT_MS = 1000;

export class ErrorReportingTimeoutError extends Error {
  readonly code = "ERROR_REPORTING_TIMEOUT" as const;
  readonly timeoutMs: number;

  constructor(timeoutMs: number) {
    super(`Error reporting timed out after ${timeoutMs}ms.`);
    this.name = "ErrorReportingTimeoutError";
    this.timeoutMs = timeoutMs;
  }
}

export function errorReportingObserverTimeout(
  timeoutMs: number | false,
): number | false {
  return timeoutMs === false || (Number.isFinite(timeoutMs) && timeoutMs > 0)
    ? timeoutMs
    : DEFAULT_ERROR_REPORTING_TIMEOUT_MS;
}

export async function runErrorReportingOperation<T>(
  operation: () => MaybePromise<T>,
  timeoutMs: number | false = DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
): Promise<T> {
  if (timeoutMs === false) return operation();
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
    throw new TypeError("Error reporting timeoutMs must be a positive number.");
  }

  let timeout: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      Promise.resolve().then(operation),
      new Promise<never>((_, reject) => {
        timeout = setTimeout(
          () => reject(new ErrorReportingTimeoutError(timeoutMs)),
          timeoutMs,
        );
      }),
    ]);
  } finally {
    if (timeout !== undefined) clearTimeout(timeout);
  }
}
