vastRequest/requestAd.js

import {parseXml} from '../xml';
import {
  getWrapperOptions,
  getFirstAd,
  getVASTAdTagURI,
  hasAdPod,
  isInline,
  isWrapper
} from '../vastSelectors';
import fetch from './helpers/fetch';
import {markAdAsRequested} from './helpers/adUtils';

const validateChain = (vastChain, {wrapperLimit = 5}) => {
  if (vastChain.length > wrapperLimit) {
    const error = new Error('Wrapper Limit reached');

    error.code = 304;
    throw error;
  }
};

const fetchAdXML = async (adTag, options) => {
  try {
    const response = await fetch(adTag, options);
    const XML = await response.text();

    return XML;
  } catch (error) {
    error.code = 502;

    throw error;
  }
};

const parseVastXml = (xml) => {
  try {
    return parseXml(xml);
  } catch (error) {
    error.code = 100;
    throw error;
  }
};

const getAd = (parsedXML) => {
  try {
    const ad = getFirstAd(parsedXML);

    if (Boolean(ad)) {
      markAdAsRequested(ad);

      return ad;
    }

    throw new Error('No Ad');
  } catch (error) {
    error.code = 303;
    throw error;
  }
};

const validateResponse = ({ad, parsedXML}, {allowMultipleAds = true, followAdditionalWrappers = true}) => {
  if (!isWrapper(ad) && !isInline(ad)) {
    const error = new Error('Invalid VAST, ad contains neither Wrapper nor Inline');

    error.code = 101;
    throw error;
  }

  if (hasAdPod(parsedXML) && !allowMultipleAds) {
    const error = new Error('Multiple ads are not allowed');

    error.code = 203;
    throw error;
  }

  if (isWrapper(ad) && !followAdditionalWrappers) {
    const error = new Error('To follow additional wrappers is not allowed');

    error.code = 200;
    throw error;
  }
};

const getOptions = (vastChain, options) => {
  const parentAd = vastChain[0];
  const parentAdIsWrapper = Boolean(parentAd) && isWrapper(parentAd.ad);
  const wrapperOptions = parentAdIsWrapper ? getWrapperOptions(parentAd.ad) : {};

  return {
    ...wrapperOptions,
    ...options
  };
};

// eslint-disable-next-line jsdoc/check-tag-names
/**
 * @function requestAd
 *
 * @memberof module:@mailonline/video-ad-sdk
 * @async
 * @static
 * @description Request the ad using the passed ad tag and returns an array with the [VAST responses]{@link VastResponse} needed to get an inline ad.
 *
 * @param {string} adTag - The VAST ad tag request url.
 * @param {Object} options - Options Map. The allowed properties are:
 * @param {number} [options.wrapperLimit] - Sets the maximum number of wrappers allowed in the {@link VastChain}.
 *  Defaults to `5`.
 * @param {boolean} [options.AllowMultipleAds] - Boolean to indicate whether adPods are allowed or not.
 *  Defaults to `true`.
 * @param {number} [options.timeout] - timeout number in milliseconds. If set, the request will timeout if it is not fulfilled before the specified time.
 * @param {VastChain} [vastChain] - Optional vastChain with the previous VAST responses.
 * @returns {Promise.<VastChain>} - Returns a Promise that will resolve with a VastChain with the newest VAST response at the beginning of the array.
 * If the {@link VastChain} had an error. The first VAST response of the array will contain an error and an errorCode entry.
 */
const requestAd = async (adTag, options, vastChain = []) => {
  const VASTAdResponse = {
    ad: null,
    errorCode: null,
    parsedXML: null,
    requestTag: adTag,
    XML: null
  };
  let opts;
  let epoch;
  let timeout;

  try {
    opts = getOptions(vastChain, options);
    validateChain(vastChain, opts);

    let fetchPromise = fetchAdXML(adTag, opts);

    if (typeof opts.timeout === 'number') {
      timeout = opts.timeout;
      epoch = Date.now();
      fetchPromise = Promise.race([
        fetchPromise,
        new Promise((resolve, reject) => {
          setTimeout(() => {
            const error = new Error('RequestAd timeout');

            error.code = 301;
            reject(error);
          }, timeout);
        })
      ]);
    }

    VASTAdResponse.XML = await fetchPromise;
    VASTAdResponse.parsedXML = parseVastXml(VASTAdResponse.XML);
    VASTAdResponse.ad = getAd(VASTAdResponse.parsedXML);

    validateResponse(VASTAdResponse, opts);

    if (isWrapper(VASTAdResponse.ad)) {
      if (epoch) {
        timeout -= Date.now() - epoch;
      }

      return requestAd(
        getVASTAdTagURI(VASTAdResponse.ad),
        {
          ...opts,
          timeout
        },
        [VASTAdResponse, ...vastChain]
      );
    }

    return [VASTAdResponse, ...vastChain];
  } catch (error) {
    /* istanbul ignore if */
    if (!Number.isInteger(error.code)) {
      error.code = 900;
    }

    VASTAdResponse.errorCode = error.code;
    VASTAdResponse.error = error;

    return [VASTAdResponse, ...vastChain];
  }
};

export default requestAd;